Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions crates/rattler_build_recipe/src/stage0/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ pub struct Build {
/// Post-processing operations
#[serde(default)]
pub post_process: ConditionalList<PostProcess>,

/// Code signing configuration
#[serde(default)]
pub signing: Signing,
}

impl Default for Build {
Expand All @@ -98,6 +102,7 @@ impl Default for Build {
variant: VariantKeyUsage::default(),
prefix_detection: PrefixDetection::default(),
post_process: ConditionalList::default(),
signing: Signing::default(),
}
}
}
Expand Down Expand Up @@ -189,6 +194,84 @@ pub struct PrefixDetection {
pub ignore_binary_files: Value<bool>,
}

/// Code signing configuration for native binaries
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
pub struct Signing {
/// macOS code signing configuration
#[serde(default)]
pub macos: Option<MacOsSigning>,
/// Windows code signing configuration
#[serde(default)]
pub windows: Option<WindowsSigning>,
}

/// macOS code signing configuration using `codesign`
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct MacOsSigning {
/// Signing identity (e.g., "Developer ID Application: Company (TEAMID)")
/// Use "-" for ad-hoc signing
pub identity: Value<String>,
/// Path to the keychain containing the signing certificate
#[serde(default)]
pub keychain: Option<Value<String>>,
/// Entitlements plist file path
#[serde(default)]
pub entitlements: Option<Value<String>>,
/// Additional codesign options (e.g., "runtime" for hardened runtime)
#[serde(default)]
pub options: ConditionalList<String>,
}

/// Windows code signing configuration.
///
/// Supports two signing methods (exactly one must be configured):
/// 1. **Local certificate** (`signtool`): Configure the `signtool` sub-object.
/// 2. **Azure Trusted Signing**: Configure the `azure_trusted_signing` sub-object.
///
/// Shared settings (`timestamp_url`, `digest_algorithm`) live at the top level.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct WindowsSigning {
/// Local certificate signing via `signtool`
#[serde(default)]
pub signtool: Option<SigntoolConfig>,
/// Azure Trusted Signing configuration
#[serde(default)]
pub azure_trusted_signing: Option<AzureTrustedSigningConfig>,

// --- Shared settings ---
/// RFC 3161 timestamp server URL
#[serde(default)]
pub timestamp_url: Option<Value<String>>,
/// Digest algorithm (default: sha256)
#[serde(default)]
pub digest_algorithm: Option<Value<String>>,
}

/// Local certificate signing configuration for `signtool`
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SigntoolConfig {
/// Path to the certificate file (.pfx / .p12)
pub certificate_file: Value<String>,
/// Name of the environment variable containing the certificate password.
/// The password is read from this env var at build time to avoid leaking
/// secrets into the rendered recipe.
#[serde(default)]
pub certificate_password_env: Option<Value<String>>,
}

/// Azure Trusted Signing configuration
///
/// Requires `az login` (OIDC) for authentication.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AzureTrustedSigningConfig {
/// Azure Trusted Signing endpoint URL
pub endpoint: Value<String>,
/// Azure Trusted Signing account name
pub account_name: Value<String>,
/// Azure Trusted Signing certificate profile name
pub certificate_profile: Value<String>,
}

/// Post-processing operations using regex replacements
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PostProcess {
Expand Down Expand Up @@ -277,6 +360,7 @@ impl Build {
variant,
prefix_detection,
post_process,
signing,
} = self;

let mut vars = Vec::new();
Expand Down Expand Up @@ -411,6 +495,37 @@ impl Build {
vars.extend(post_process.used_variables());
collect_post_process_vars(post_process.iter(), &mut vars);

// Signing
if let Some(macos) = &signing.macos {
vars.extend(macos.identity.used_variables());
if let Some(keychain) = &macos.keychain {
vars.extend(keychain.used_variables());
}
if let Some(entitlements) = &macos.entitlements {
vars.extend(entitlements.used_variables());
}
vars.extend(macos.options.used_variables());
}
if let Some(windows) = &signing.windows {
if let Some(signtool) = &windows.signtool {
vars.extend(signtool.certificate_file.used_variables());
if let Some(password) = &signtool.certificate_password_env {
vars.extend(password.used_variables());
}
}
if let Some(azure) = &windows.azure_trusted_signing {
vars.extend(azure.endpoint.used_variables());
vars.extend(azure.account_name.used_variables());
vars.extend(azure.certificate_profile.used_variables());
}
if let Some(timestamp_url) = &windows.timestamp_url {
vars.extend(timestamp_url.used_variables());
}
if let Some(digest_algorithm) = &windows.digest_algorithm {
vars.extend(digest_algorithm.used_variables());
}
}

vars.sort();
vars.dedup();
vars
Expand Down
146 changes: 139 additions & 7 deletions crates/rattler_build_recipe/src/stage0/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ use crate::{
Package as Stage0Package, Requirements as Stage0Requirements, Source as Stage0Source,
Stage0Recipe, TestType as Stage0TestType,
build::{
AzureTrustedSigningConfig as Stage0AzureTrustedSigningConfig,
BinaryRelocation as Stage0BinaryRelocation, DynamicLinking as Stage0DynamicLinking,
ForceFileType as Stage0ForceFileType, PostProcess as Stage0PostProcess,
PrefixDetection as Stage0PrefixDetection, PrefixIgnore as Stage0PrefixIgnore,
PythonBuild as Stage0PythonBuild, VariantKeyUsage as Stage0VariantKeyUsage,
ForceFileType as Stage0ForceFileType, MacOsSigning as Stage0MacOsSigning,
PostProcess as Stage0PostProcess, PrefixDetection as Stage0PrefixDetection,
PrefixIgnore as Stage0PrefixIgnore, PythonBuild as Stage0PythonBuild,
Signing as Stage0Signing, SigntoolConfig as Stage0SigntoolConfig,
VariantKeyUsage as Stage0VariantKeyUsage, WindowsSigning as Stage0WindowsSigning,
},
requirements::{
IgnoreRunExports as Stage0IgnoreRunExports, RunExports as Stage0RunExports,
Expand All @@ -68,10 +71,13 @@ use crate::{
Extra as Stage1Extra, GlobVec, Package as Stage1Package, Recipe as Stage1Recipe,
Requirements as Stage1Requirements, Rpaths,
build::{
Build as Stage1Build, BuildString, DynamicLinking as Stage1DynamicLinking,
ForceFileType as Stage1ForceFileType, PostProcess as Stage1PostProcess,
PrefixDetection as Stage1PrefixDetection, PythonBuild as Stage1PythonBuild,
VariantKeyUsage as Stage1VariantKeyUsage,
AzureTrustedSigningConfig as Stage1AzureTrustedSigningConfig, Build as Stage1Build,
BuildString, DynamicLinking as Stage1DynamicLinking,
ForceFileType as Stage1ForceFileType, MacOsSigning as Stage1MacOsSigning,
PostProcess as Stage1PostProcess, PrefixDetection as Stage1PrefixDetection,
PythonBuild as Stage1PythonBuild, Signing as Stage1Signing,
SigntoolConfig as Stage1SigntoolConfig, VariantKeyUsage as Stage1VariantKeyUsage,
WindowsSigning as Stage1WindowsSigning,
},
requirements::{
IgnoreRunExports as Stage1IgnoreRunExports, RunExports as Stage1RunExports,
Expand Down Expand Up @@ -1975,6 +1981,120 @@ impl Evaluate for Stage0DynamicLinking {
}
}

impl Evaluate for Stage0MacOsSigning {
type Output = Stage1MacOsSigning;

fn evaluate(&self, context: &EvaluationContext) -> Result<Self::Output, ParseError> {
let identity = evaluate_string_value(&self.identity, context)?;
let keychain = self
.keychain
.as_ref()
.map(|v| evaluate_string_value(v, context))
.transpose()?;
let entitlements = self
.entitlements
.as_ref()
.map(|v| evaluate_string_value(v, context))
.transpose()?;
let options = evaluate_string_list(&self.options, context)?;

Ok(Stage1MacOsSigning {
identity,
keychain,
entitlements,
options,
})
}
}

impl Evaluate for Stage0SigntoolConfig {
type Output = Stage1SigntoolConfig;

fn evaluate(&self, context: &EvaluationContext) -> Result<Self::Output, ParseError> {
let certificate_file = evaluate_string_value(&self.certificate_file, context)?;
let certificate_password_env = self
.certificate_password_env
.as_ref()
.map(|v| evaluate_string_value(v, context))
.transpose()?;

Ok(Stage1SigntoolConfig {
certificate_file,
certificate_password_env,
})
}
}

impl Evaluate for Stage0AzureTrustedSigningConfig {
type Output = Stage1AzureTrustedSigningConfig;

fn evaluate(&self, context: &EvaluationContext) -> Result<Self::Output, ParseError> {
let endpoint = evaluate_string_value(&self.endpoint, context)?;
let account_name = evaluate_string_value(&self.account_name, context)?;
let certificate_profile = evaluate_string_value(&self.certificate_profile, context)?;

Ok(Stage1AzureTrustedSigningConfig {
endpoint,
account_name,
certificate_profile,
})
}
}

impl Evaluate for Stage0WindowsSigning {
type Output = Stage1WindowsSigning;

fn evaluate(&self, context: &EvaluationContext) -> Result<Self::Output, ParseError> {
let signtool = self
.signtool
.as_ref()
.map(|s| s.evaluate(context))
.transpose()?;
let azure_trusted_signing = self
.azure_trusted_signing
.as_ref()
.map(|a| a.evaluate(context))
.transpose()?;
let timestamp_url = self
.timestamp_url
.as_ref()
.map(|v| evaluate_string_value(v, context))
.transpose()?;
let digest_algorithm = self
.digest_algorithm
.as_ref()
.map(|v| evaluate_string_value(v, context))
.transpose()?
.unwrap_or_else(|| "sha256".to_string());

Ok(Stage1WindowsSigning {
signtool,
azure_trusted_signing,
timestamp_url,
digest_algorithm,
})
}
}

impl Evaluate for Stage0Signing {
type Output = Stage1Signing;

fn evaluate(&self, context: &EvaluationContext) -> Result<Self::Output, ParseError> {
let macos = self
.macos
.as_ref()
.map(|m| m.evaluate(context))
.transpose()?;
let windows = self
.windows
.as_ref()
.map(|w| w.evaluate(context))
.transpose()?;

Ok(Stage1Signing { macos, windows })
}
}

impl Evaluate for Stage0Build {
type Output = Stage1Build;

Expand Down Expand Up @@ -2098,6 +2218,9 @@ impl Evaluate for Stage0Build {
false,
)?;

// Evaluate signing configuration
let signing = self.signing.evaluate(context)?;

Ok(Stage1Build {
number,
string,
Expand All @@ -2113,6 +2236,7 @@ impl Evaluate for Stage0Build {
variant,
prefix_detection,
post_process,
signing,
})
}
}
Expand Down Expand Up @@ -2927,6 +3051,13 @@ fn merge_stage1_build(
output.post_process
};

// Signing: use output if not default, otherwise inherit from top-level
let signing = if output.signing.is_default() {
toplevel.signing
} else {
output.signing
};

stage1::Build {
script,
number,
Expand All @@ -2942,6 +3073,7 @@ fn merge_stage1_build(
variant,
prefix_detection,
post_process,
signing,
}
}

Expand Down
Loading