--- a/apple-codesign/src/cli/certificate_source.rs +++ b/apple-codesign/src/cli/certificate_source.rs @@ -16,0 +17 @@ + bytes::Bytes, @@ -19,0 +21 @@ + signature::Signer, @@ -21,2 +23,10 @@ - std::path::PathBuf, - x509_certificate::CapturedX509Certificate, + std::{ + path::PathBuf, + process::Command, + sync::atomic::{AtomicU64, Ordering}, + }, + x509_certificate::{ + CapturedX509Certificate, KeyAlgorithm, Sign, Signature, SignatureAlgorithm, + X509CertificateError, + }, + zeroize::Zeroizing, @@ -36,0 +47,2 @@ +static PKCS11_SIGN_COUNTER: AtomicU64 = AtomicU64::new(0); + @@ -204,0 +214,262 @@ +#[derive(Clone, Debug)] +struct Pkcs11PrivateKey { + tool: String, + module: PathBuf, + slot_id: Option, + token_label: Option, + key_id: Option, + key_label: Option, + pin: String, + cert: CapturedX509Certificate, +} + +impl signature::Signer for Pkcs11PrivateKey { + fn try_sign(&self, message: &[u8]) -> Result { + match self.cert.key_algorithm() { + Some(KeyAlgorithm::Rsa) => {} + Some(algorithm) => { + return Err(signature::Error::from_source(format!( + "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}" + ))); + } + None => { + return Err(signature::Error::from_source( + "failed to resolve PKCS#11 certificate key algorithm", + )); + } + } + + let sequence = PKCS11_SIGN_COUNTER.fetch_add(1, Ordering::Relaxed); + let base = std::env::temp_dir().join(format!( + "rcodesign-pkcs11-{}-{sequence}", + std::process::id() + )); + let input_path = base.with_extension("in"); + let output_path = base.with_extension("sig"); + let openssl_config_path = base.with_extension("openssl.cnf"); + let provider_debug_path = base.with_extension("pkcs11-provider.log"); + + std::fs::write(&input_path, message).map_err(signature::Error::from_source)?; + + let provider_override = + std::env::var("BURROW_APPLE_PKCS11_SIGN_WITH_OPENSSL_PROVIDER") + .or_else(|_| std::env::var("BURROW_PKCS11_SIGN_WITH_OPENSSL_PROVIDER")) + .ok() + .and_then(|value| { + if value.trim().is_empty() { + None + } else { + Some(matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON")) + } + }); + let use_openssl_provider = provider_override.unwrap_or_else(|| { + std::env::var("BURROW_APPLE_PKCS11_BACKEND") + .or_else(|_| std::env::var("BURROW_PKCS11_BACKEND")) + .map(|value| value == "gcp-kms") + .unwrap_or(false) + || self.module.to_string_lossy().contains("kmsp11") + || std::env::var("BURROW_GCP_KMS_PKCS11_CONFIG").is_ok() + || std::env::var("KMS_PKCS11_CONFIG").is_ok() + }); + + if use_openssl_provider { + let token_label = self.token_label.as_ref().ok_or_else(|| { + signature::Error::from_source( + "OpenSSL PKCS#11 provider signing requires --pkcs11-token-label", + ) + })?; + let mut key_uri = format!("pkcs11:token={token_label}"); + if let Some(label) = &self.key_label { + key_uri.push_str(";object="); + key_uri.push_str(label); + } else if let Some(id) = &self.key_id { + key_uri.push_str(";id="); + key_uri.push_str(id); + } else { + return Err(signature::Error::from_source( + "OpenSSL PKCS#11 provider signing requires --pkcs11-key-label or --pkcs11-key-id", + )); + } + key_uri.push_str(";type=private"); + + let pin_path = base.with_extension("pin"); + std::fs::write(&pin_path, &self.pin).map_err(signature::Error::from_source)?; + + let module_path = self.module.to_string_lossy(); + let kms_config = std::env::var("KMS_PKCS11_CONFIG") + .or_else(|_| std::env::var("BURROW_GCP_KMS_PKCS11_CONFIG")) + .ok(); + let needs_kms_config = module_path.contains("kmsp11") || kms_config.is_some(); + if needs_kms_config { + let config_path = kms_config.as_ref().ok_or_else(|| { + signature::Error::from_source( + "OpenSSL PKCS#11 provider signing with Google KMS requires KMS_PKCS11_CONFIG", + ) + })?; + let metadata = std::fs::metadata(config_path).map_err(|err| { + signature::Error::from_source(format!( + "OpenSSL PKCS#11 provider signing could not read KMS_PKCS11_CONFIG at {config_path}: {err}" + )) + })?; + if !metadata.is_file() || metadata.len() == 0 { + return Err(signature::Error::from_source(format!( + "OpenSSL PKCS#11 provider signing requires a non-empty KMS_PKCS11_CONFIG file at {config_path}" + ))); + } + } + + let openssl_modules = std::env::var("BURROW_OPENSSL_MODULES") + .or_else(|_| std::env::var("OPENSSL_MODULES")) + .ok() + .filter(|value| !value.trim().is_empty()); + if let Some(modules) = &openssl_modules { + let provider_extension = if cfg!(target_os = "macos") { + "dylib" + } else { + "so" + }; + let provider_module = PathBuf::from(modules) + .join(format!("pkcs11.{provider_extension}")); + if provider_module.is_file() { + let config = format!( + "HOME = .\nopenssl_conf = openssl_init\n\n[openssl_init]\nproviders = provider_sect\n\n[provider_sect]\ndefault = default_sect\npkcs11 = pkcs11_sect\n\n[default_sect]\nactivate = 1\n\n[pkcs11_sect]\nmodule = {}\npkcs11-module-path = {}\npkcs11-module-token-pin = file:{}\npkcs11-module-cache-pins = cache\npkcs11-module-quirks = no-operation-state no-allowed-mechanisms\nactivate = 1\n", + provider_module.display(), + self.module.display(), + pin_path.display(), + ); + std::fs::write(&openssl_config_path, config) + .map_err(signature::Error::from_source)?; + } + } + + let openssl = std::env::var("BURROW_OPENSSL").unwrap_or_else(|_| "openssl".into()); + let mut command = Command::new(openssl); + command + .env("PKCS11_PROVIDER_MODULE", &self.module) + .arg("dgst") + .arg("-provider") + .arg("pkcs11") + .arg("-provider") + .arg("default") + .arg("-sha256") + .arg("-sign") + .arg(&key_uri) + .arg("-passin") + .arg(format!("file:{}", pin_path.display())) + .arg("-out") + .arg(&output_path) + .arg(&input_path); + if let Some(config_path) = kms_config { + command + .env("KMS_PKCS11_CONFIG", &config_path) + .env("BURROW_GCP_KMS_PKCS11_CONFIG", &config_path); + } + if let Ok(credentials_path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") { + command.env("GOOGLE_APPLICATION_CREDENTIALS", credentials_path); + } + if let Some(modules) = openssl_modules { + command.env("OPENSSL_MODULES", modules); + } + if openssl_config_path.is_file() { + command.env("OPENSSL_CONF", &openssl_config_path); + } + if let Ok(debug) = std::env::var("BURROW_PKCS11_PROVIDER_DEBUG") + .or_else(|_| std::env::var("PKCS11_PROVIDER_DEBUG")) + { + command.env("PKCS11_PROVIDER_DEBUG", debug); + } else { + command.env( + "PKCS11_PROVIDER_DEBUG", + format!("file:{},level:1", provider_debug_path.display()), + ); + } + + let output = command.output().map_err(signature::Error::from_source)?; + let _ = std::fs::remove_file(&input_path); + let _ = std::fs::remove_file(&pin_path); + let _ = std::fs::remove_file(&openssl_config_path); + + if !output.status.success() { + let _ = std::fs::remove_file(&output_path); + let provider_debug = std::fs::read_to_string(&provider_debug_path) + .ok() + .filter(|value| !value.trim().is_empty()) + .map(|value| { + let tail = value + .lines() + .rev() + .take(80) + .collect::>() + .into_iter() + .rev() + .collect::>() + .join("\n"); + format!("\nPKCS#11 provider debug tail:\n{tail}\n") + }) + .unwrap_or_default(); + let _ = std::fs::remove_file(&provider_debug_path); + return Err(signature::Error::from_source(format!( + "OpenSSL PKCS#11 provider signing failed with status {}: {}{}{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + provider_debug + ))); + } + let _ = std::fs::remove_file(&provider_debug_path); + } else { + let mut command = Command::new(&self.tool); + command + .arg("--module") + .arg(&self.module) + .arg("--login") + .arg("--pin") + .arg(&self.pin) + .arg("--sign") + .arg("--mechanism") + .arg("SHA256-RSA-PKCS") + .arg("--input-file") + .arg(&input_path) + .arg("--output-file") + .arg(&output_path); + + if let Some(slot_id) = &self.slot_id { + command.arg("--slot").arg(slot_id); + } + if let Some(token_label) = &self.token_label { + command.arg("--token-label").arg(token_label); + } + if let Some(id) = &self.key_id { + command.arg("--id").arg(id); + } + if let Some(label) = &self.key_label { + command.arg("--label").arg(label); + } + + let output = command.output().map_err(signature::Error::from_source)?; + + let _ = std::fs::remove_file(&input_path); + + if !output.status.success() { + let _ = std::fs::remove_file(&output_path); + return Err(signature::Error::from_source(format!( + "pkcs11-tool signing failed with status {}: {}{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ))); + } + } + + let signature = std::fs::read(&output_path).map_err(signature::Error::from_source)?; + let _ = std::fs::remove_file(&output_path); + + if signature.is_empty() { + return Err(signature::Error::from_source( + "pkcs11-tool produced an empty signature", + )); + } + + Ok(signature.into()) + } +} @@ -204,0 +384,214 @@ +impl Sign for Pkcs11PrivateKey { + fn sign(&self, message: &[u8]) -> Result<(Vec, SignatureAlgorithm), X509CertificateError> { + let algorithm = self.signature_algorithm()?; + + Ok((self.try_sign(message)?.into(), algorithm)) + } + + fn key_algorithm(&self) -> Option { + self.cert.key_algorithm() + } + + fn public_key_data(&self) -> Bytes { + self.cert.public_key_data() + } + + fn signature_algorithm(&self) -> Result { + match self.cert.key_algorithm() { + Some(KeyAlgorithm::Rsa) => Ok(SignatureAlgorithm::RsaSha256), + Some(algorithm) => Err(X509CertificateError::UnknownSignatureAlgorithm(format!( + "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}" + ))), + None => Err(X509CertificateError::UnknownSignatureAlgorithm( + "failed to resolve PKCS#11 certificate key algorithm".into(), + )), + } + } + + fn private_key_data(&self) -> Option>> { + None + } + + fn rsa_primes( + &self, + ) -> Result>, Zeroizing>)>, X509CertificateError> { + Ok(None) + } +} + +impl x509_certificate::KeyInfoSigner for Pkcs11PrivateKey {} + +impl PrivateKey for Pkcs11PrivateKey { + fn as_key_info_signer(&self) -> &dyn x509_certificate::KeyInfoSigner { + self + } + + fn to_public_key_peer_decrypt( + &self, + ) -> Result< + Box, + AppleCodesignError, + > { + Err(AppleCodesignError::CliGeneralError( + "PKCS#11 keys cannot decrypt remote-signing peer setup data".into(), + )) + } + + fn finish(&self) -> Result<(), AppleCodesignError> { + Ok(()) + } +} + +#[derive(Args, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Pkcs11SigningKey { + /// Path to pkcs11-tool + #[arg(id = "pkcs11_tool", long = "pkcs11-tool", default_value = "pkcs11-tool", value_name = "PATH")] + pub tool: String, + + /// Path to the PKCS#11 module to use for signing + #[arg( + long = "pkcs11-library", + alias = "pkcs11-module", + id = "pkcs11_library", + value_name = "PATH" + )] + pub module: Option, + + /// PKCS#11 slot id containing the signing key + #[arg(id = "pkcs11_slot_id", long = "pkcs11-slot-id", value_name = "SLOT")] + pub slot_id: Option, + + /// PKCS#11 token label containing the signing key + #[arg(id = "pkcs11_token_label", long = "pkcs11-token-label", value_name = "LABEL")] + pub token_label: Option, + + /// PKCS#11 private-key object id, usually hex + #[arg(id = "pkcs11_key_id", long = "pkcs11-key-id", value_name = "ID")] + pub key_id: Option, + + /// PKCS#11 private-key object label + #[arg(id = "pkcs11_key_label", long = "pkcs11-key-label", value_name = "LABEL")] + pub key_label: Option, + + /// PIN used to unlock the PKCS#11 token + #[arg(id = "pkcs11_pin", long = "pkcs11-pin", group = "pkcs11-pin-source", value_name = "PIN")] + pub pin: Option, + + /// File containing the PIN used to unlock the PKCS#11 token + #[arg(id = "pkcs11_pin_file", long = "pkcs11-pin-file", group = "pkcs11-pin-source", value_name = "PATH")] + pub pin_path: Option, + + /// Environment variable holding the PIN used to unlock the PKCS#11 token + #[arg(id = "pkcs11_pin_env", long = "pkcs11-pin-env", group = "pkcs11-pin-source", value_name = "ENV")] + #[serde(skip)] + pub pin_env: Option, + + /// DER certificate file corresponding to the PKCS#11 private key + #[arg( + long = "pkcs11-certificate-file", + alias = "pkcs11-certificate-der-file", + id = "pkcs11_certificate_file", + value_name = "PATH" + )] + pub certificate_path: Option, +} + +impl Pkcs11SigningKey { + pub(crate) fn configured(&self) -> bool { + self.module.is_some() + || self.slot_id.is_some() + || self.token_label.is_some() + || self.key_id.is_some() + || self.key_label.is_some() + || self.pin.is_some() + || self.pin_path.is_some() + || self.pin_env.is_some() + || self.certificate_path.is_some() + } + + fn not_configured(&self) -> bool { + !self.configured() + } + + fn resolve_pin(&self) -> Result { + if let Some(pin) = &self.pin { + Ok(pin.clone()) + } else if let Some(path) = &self.pin_path { + Ok(std::fs::read_to_string(path)?.trim().to_string()) + } else if let Some(env) = &self.pin_env { + std::env::var(env).map_err(|_| { + AppleCodesignError::CliGeneralError(format!( + "failed reading PKCS#11 PIN from {env} environment variable" + )) + }) + } else { + Err(AppleCodesignError::CliGeneralError( + "--pkcs11-pin, --pkcs11-pin-file, or --pkcs11-pin-env is required".into(), + )) + } + } +} + +impl KeySource for Pkcs11SigningKey { + fn resolve_certificates(&self) -> Result { + if !self.configured() { + return Ok(Default::default()); + } + + let module = self.module.clone().ok_or_else(|| { + AppleCodesignError::CliGeneralError("--pkcs11-library is required".into()) + })?; + if self.slot_id.is_none() && self.token_label.is_none() { + return Err(AppleCodesignError::CliGeneralError( + "--pkcs11-slot-id or --pkcs11-token-label is required".into(), + )); + } + if self.key_id.is_none() && self.key_label.is_none() { + return Err(AppleCodesignError::CliGeneralError( + "--pkcs11-key-id or --pkcs11-key-label is required".into(), + )); + } + let certificate_path = self.certificate_path.clone().ok_or_else(|| { + AppleCodesignError::CliGeneralError("--pkcs11-certificate-file is required".into()) + })?; + let pin = self.resolve_pin()?; + + warn!( + "reading PKCS#11 certificate data from {}", + certificate_path.display() + ); + let cert = CapturedX509Certificate::from_der(std::fs::read(certificate_path)?)?; + + match cert.key_algorithm() { + Some(KeyAlgorithm::Rsa) => {} + Some(algorithm) => { + return Err(AppleCodesignError::CliGeneralError(format!( + "PKCS#11 signing currently supports RSA certificates, not {algorithm:?}" + ))); + } + None => { + return Err(AppleCodesignError::CliGeneralError( + "failed to resolve PKCS#11 certificate key algorithm".into(), + )); + } + } + + let signer = Pkcs11PrivateKey { + tool: self.tool.clone(), + module, + slot_id: self.slot_id.clone(), + token_label: self.token_label.clone(), + key_id: self.key_id.clone(), + key_label: self.key_label.clone(), + pin, + cert: cert.clone(), + }; + + Ok(SigningCertificates { + keys: vec![Box::new(signer)], + certs: vec![cert], + }) + } +} + @@ -643,0 +1037,8 @@ + rename = "pkcs11", + skip_serializing_if = "Pkcs11SigningKey::not_configured" + )] + pub pkcs11_key: Pkcs11SigningKey, + + #[command(flatten)] + #[serde( + default, @@ -680,0 +1082,2 @@ + res.push(&self.pkcs11_key as &dyn KeySource); + --- a/apple-codesign/src/cli/mod.rs +++ b/apple-codesign/src/cli/mod.rs @@ -1529 +1532,6 @@ - let certs = c.signer.resolve_certificates(true)?; + let signer = if self.certificate.pkcs11_key.configured() { + &self.certificate + } else { + &c.signer + }; + let certs = signer.resolve_certificates(true)?;