Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
apple-codesign-quick
src
rustcrypto_cms.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
rustcrypto_cms.rs
Blame
History
Raw
use crate::error::{CodeSignError, Result}; use crate::signature::{CmsSigner, CmsSigningRequest, HashAlgorithm}; use cms::builder::{SignedDataBuilder, SignerInfoBuilder}; use cms::cert::{CertificateChoices, IssuerAndSerialNumber}; use cms::signed_data::{EncapsulatedContentInfo, SignerIdentifier}; use const_oid::ObjectIdentifier; use der::asn1::{Any, GeneralizedTime, OctetStringRef, SetOfVec, UtcTime}; use der::{DateTime, Decode, Encode, Sequence}; use plist::{Dictionary, Value}; use rsa::pkcs1::DecodeRsaPrivateKey; use rsa::pkcs1v15::SigningKey; use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey}; use rsa::{RsaPrivateKey, RsaPublicKey}; use spki::AlgorithmIdentifierOwned; use std::time::SystemTime; use x509_cert::Certificate; use x509_cert::attr::{Attribute, AttributeValue}; const APPLE_CODE_DIRECTORY_HASHES_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113635.100.9.1"); const APPLE_CODE_DIRECTORY_DIGEST_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113635.100.9.2"); /// Issues Apple Development and Distribution certificates, valid until 2030. const APPLE_WWDR_G3_CERTIFICATE_DER: &[u8] = include_bytes!("../assets/AppleWWDRCAG3.cer"); const APPLE_ROOT_CERTIFICATE_DER: &[u8] = include_bytes!("../assets/AppleIncRootCertificate.cer"); type RsaSha256SigningKey = SigningKey<sha2_010::Sha256>; /// CMS signer for Apple code signatures, using RustCrypto's CMS, DER and X.509 crates. /// It makes detached SignedData over the primary CodeDirectory, with the CodeDirectory hash attributes AMFI expects. pub struct RustCryptoCmsSigner { signing_key: RsaSha256SigningKey, signer_certificate: Certificate, certificate_chain: Vec<Certificate>, signing_time: Option<SystemTime>, } impl RustCryptoCmsSigner { pub fn new( private_key: RsaPrivateKey, signer_certificate: Certificate, certificate_chain: Vec<Certificate>, ) -> Self { RustCryptoCmsSigner { signing_key: RsaSha256SigningKey::new(private_key), signer_certificate, certificate_chain, signing_time: Some(SystemTime::now()), } } /// Decode a PKCS#8 or PKCS#1 key and its certificate, and chain it up to Apple's root with the /// candidates given here and the bundled WWDR G3 and root certificates. pub fn from_der<I: IntoIterator<Item = C>, C: AsRef<[u8]>>( private_key_der: &[u8], signer_certificate_der: &[u8], certificate_chain_der: I, ) -> Result<Self> { let private_key = decode_rsa_private_key(private_key_der)?; let signer_certificate = Certificate::from_der(signer_certificate_der).map_err(|error| { CodeSignError::cms(format!("Cannot decode the signer certificate: {error}")) })?; ensure_key_matches_certificate(&private_key, &signer_certificate)?; let bundled = [APPLE_WWDR_G3_CERTIFICATE_DER, APPLE_ROOT_CERTIFICATE_DER].map(Certificate::from_der); let mut candidates: Vec<Certificate> = Vec::new(); for certificate in certificate_chain_der .into_iter() .map(|certificate_der| Certificate::from_der(certificate_der.as_ref())) .chain(bundled) { let certificate = certificate.map_err(|error| { CodeSignError::cms(format!("Cannot decode a chain certificate: {error}")) })?; if !candidates.contains(&certificate) { candidates.push(certificate); } } let certificate_chain = certificate_chain_for_signer(&signer_certificate, &candidates)?; Ok(RustCryptoCmsSigner::new( private_key, signer_certificate, certificate_chain, )) } pub fn with_signing_time(mut self, signing_time: SystemTime) -> Self { self.signing_time = Some(signing_time); self } pub fn without_signing_time(mut self) -> Self { self.signing_time = None; self } pub fn signer_certificate(&self) -> &Certificate { &self.signer_certificate } pub fn certificate_chain(&self) -> &[Certificate] { &self.certificate_chain } } impl CmsSigner for RustCryptoCmsSigner { fn sign(&self, request: CmsSigningRequest<'_>) -> Result<Vec<u8>> { // Apple signs the first CodeDirectory, which is the SHA-1 one, with a SHA-256 digest. let primary_code_directory = request.code_directories.first().ok_or_else(|| { CodeSignError::cms("Cannot build a CMS signature without a CodeDirectory") })?; let message_digest = HashAlgorithm::Sha256.digest(primary_code_directory.bytes); let encapsulated_content_info = EncapsulatedContentInfo { econtent_type: const_oid::db::rfc5911::ID_DATA, econtent: None, }; let digest_algorithm = AlgorithmIdentifierOwned { oid: const_oid::db::rfc5912::ID_SHA_256, parameters: Some(Any::null()), }; let signer_identifier = SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { issuer: self.signer_certificate.tbs_certificate.issuer.clone(), serial_number: self .signer_certificate .tbs_certificate .serial_number .clone(), }); let mut signer_info = SignerInfoBuilder::new( &self.signing_key, signer_identifier, digest_algorithm.clone(), &encapsulated_content_info, Some(&message_digest), ) .map_err(CodeSignError::cms)?; let signing_time = self.signing_time.map(signing_time_attribute).transpose()?; for attribute in signing_time.into_iter().chain([ code_directory_digest_attribute(&request)?, code_directory_hashes_attribute(&request)?, ]) { signer_info .add_signed_attribute(attribute) .map_err(CodeSignError::cms)?; } let mut builder = SignedDataBuilder::new(&encapsulated_content_info); builder .add_digest_algorithm(digest_algorithm) .map_err(CodeSignError::cms)?; // The builder panics on duplicate certificates, and the signer may be in its own chain. let mut added: Vec<&Certificate> = Vec::new(); for certificate in self .certificate_chain .iter() .chain([&self.signer_certificate]) { if !added.contains(&certificate) { builder .add_certificate(CertificateChoices::Certificate(certificate.clone())) .map_err(CodeSignError::cms)?; added.push(certificate); } } builder .add_signer_info::<RsaSha256SigningKey, rsa::pkcs1v15::Signature>(signer_info) .map_err(CodeSignError::cms)?; builder .build() .map_err(CodeSignError::cms)? .to_der() .map_err(CodeSignError::cms) } } #[derive(Sequence)] struct CodeDirectoryDigest<'a> { algorithm: ObjectIdentifier, digest: OctetStringRef<'a>, } fn decode_rsa_private_key(der: &[u8]) -> Result<RsaPrivateKey> { RsaPrivateKey::from_pkcs8_der(der).or_else(|pkcs8_error| { RsaPrivateKey::from_pkcs1_der(der).map_err(|pkcs1_error| { CodeSignError::cms(format!( "Cannot decode the RSA private key as PKCS#8 ({pkcs8_error}) or PKCS#1 ({pkcs1_error})" )) }) }) } // A stale key next to a renewed certificate would otherwise only fail on the device. fn ensure_key_matches_certificate( private_key: &RsaPrivateKey, certificate: &Certificate, ) -> Result<()> { let certificate_key = certificate .tbs_certificate .subject_public_key_info .to_der() .map_err(CodeSignError::cms)?; let certificate_key = RsaPublicKey::from_public_key_der(&certificate_key).map_err(|error| { CodeSignError::cms(format!( "Cannot read the signer certificate's RSA key: {error}" )) })?; if RsaPublicKey::from(private_key) == certificate_key { Ok(()) } else { Err(CodeSignError::cms( "The private key does not match the signer certificate", )) } } /// The issuers of `signer_certificate` up to a self-signed root, the signer excluded. fn certificate_chain_for_signer( signer_certificate: &Certificate, candidates: &[Certificate], ) -> Result<Vec<Certificate>> { let mut certificate_chain: Vec<Certificate> = Vec::new(); let mut certificate = signer_certificate; while certificate.tbs_certificate.subject != certificate.tbs_certificate.issuer { let issuer = &certificate.tbs_certificate.issuer; certificate = candidates .iter() .find(|candidate| candidate.tbs_certificate.subject == *issuer) .ok_or_else(|| { CodeSignError::cms(format!("The certificate chain misses the issuer {issuer}")) })?; if certificate_chain.contains(certificate) { return Err(CodeSignError::cms("The certificate chain has a cycle")); } certificate_chain.push(certificate.clone()); } Ok(certificate_chain) } fn signing_time_attribute(signing_time: SystemTime) -> Result<Attribute> { let date_time = DateTime::from_system_time(signing_time).map_err(CodeSignError::cms)?; // RFC 5652 wants UTCTime between 1950 and 2049. let time_der = if (1950..2050).contains(&date_time.year()) { UtcTime::from_date_time(date_time) .and_then(|time| time.to_der()) .map_err(CodeSignError::cms)? } else { GeneralizedTime::from_date_time(date_time) .to_der() .map_err(CodeSignError::cms)? }; single_value_attribute( const_oid::db::rfc5911::ID_SIGNING_TIME, AttributeValue::from_der(&time_der).map_err(CodeSignError::cms)?, ) } /// The full digest of every CodeDirectory, each with its hash algorithm. fn code_directory_digest_attribute(request: &CmsSigningRequest<'_>) -> Result<Attribute> { if !request .code_directories .iter() .any(|code_directory| code_directory.algorithm == HashAlgorithm::Sha256) { return Err(CodeSignError::cms( "Cannot build a CMS signature without a SHA-256 CodeDirectory", )); } let values: Result<Vec<_>> = request .code_directories .iter() .map(|code_directory| { let digest = code_directory.algorithm.digest(code_directory.bytes); Any::encode_from(&CodeDirectoryDigest { algorithm: match code_directory.algorithm { HashAlgorithm::Sha1 => const_oid::db::rfc5912::ID_SHA_1, HashAlgorithm::Sha256 => const_oid::db::rfc5912::ID_SHA_256, }, digest: OctetStringRef::new(&digest).map_err(CodeSignError::cms)?, }) .map_err(CodeSignError::cms) }) .collect(); Ok(Attribute { oid: APPLE_CODE_DIRECTORY_DIGEST_OID, values: SetOfVec::try_from(values?).map_err(CodeSignError::cms)?, }) } /// A plist of the CodeDirectories' hashes, truncated to 20 bytes like cdhashes are, in request order. fn code_directory_hashes_attribute(request: &CmsSigningRequest<'_>) -> Result<Attribute> { let cdhashes = request .code_directories .iter() .map(|code_directory| { let mut cdhash = code_directory.algorithm.digest(code_directory.bytes); cdhash.truncate(20); Value::Data(cdhash) }) .collect(); let plist = Dictionary::from_iter([("cdhashes", Value::Array(cdhashes))]); let mut xml = Vec::new(); plist::to_writer_xml(&mut xml, &plist)?; if xml.last() != Some(&b'\n') { xml.push(b'\n'); } let value = Any::from(OctetStringRef::new(&xml).map_err(CodeSignError::cms)?); single_value_attribute(APPLE_CODE_DIRECTORY_HASHES_OID, value) } fn single_value_attribute(oid: ObjectIdentifier, value: AttributeValue) -> Result<Attribute> { Ok(Attribute { oid, values: SetOfVec::try_from(vec![value]).map_err(CodeSignError::cms)?, }) } #[cfg(test)] mod tests { use super::*; use crate::signature::EncodedCodeDirectory; #[test] fn apple_code_directory_digest_requires_sha256_code_directory() { let code_directory = EncodedCodeDirectory { algorithm: HashAlgorithm::Sha1, bytes: b"primary", }; let request = CmsSigningRequest { code_directories: &[code_directory], }; assert!(code_directory_digest_attribute(&request).is_err()); } #[test] fn apple_code_directory_digest_contains_a_bare_hash_oid() { let code_directory = EncodedCodeDirectory { algorithm: HashAlgorithm::Sha256, bytes: b"alternate", }; let request = CmsSigningRequest { code_directories: &[code_directory], }; let attribute = code_directory_digest_attribute(&request).unwrap(); let value_der = attribute.values.get(0).unwrap().to_der().unwrap(); assert_eq!( &value_der[..15], &[ 0x30, 0x2d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x04, 0x20, ] ); assert_eq!(value_der.len(), 47); } #[test] fn apple_code_directory_hashes_are_xml_plist_octet_string() { let code_directories = [ EncodedCodeDirectory { algorithm: HashAlgorithm::Sha1, bytes: b"primary", }, EncodedCodeDirectory { algorithm: HashAlgorithm::Sha256, bytes: b"alternate", }, ]; let request = CmsSigningRequest { code_directories: &code_directories, }; let attribute = code_directory_hashes_attribute(&request).unwrap(); assert_eq!(attribute.oid, APPLE_CODE_DIRECTORY_HASHES_OID); let value = attribute.values.get(0).unwrap(); let plist_xml = OctetStringRef::from_der(value.to_der().unwrap().as_slice()) .unwrap() .as_bytes() .to_vec(); assert!(plist_xml.starts_with(br#"<?xml version="1.0""#)); assert!(plist_xml.ends_with(b"\n")); } #[test] fn apple_code_directory_digest_includes_every_code_directory() { let code_directories = [ EncodedCodeDirectory { algorithm: HashAlgorithm::Sha1, bytes: b"primary", }, EncodedCodeDirectory { algorithm: HashAlgorithm::Sha256, bytes: b"alternate", }, ]; let request = CmsSigningRequest { code_directories: &code_directories, }; let attribute = code_directory_digest_attribute(&request).unwrap(); let sha1_value_der = attribute.values.get(0).unwrap().to_der().unwrap(); assert_eq!(attribute.values.len(), 2); assert_eq!( &sha1_value_der[..11], &[ 0x30, 0x1d, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x04, 0x14 ] ); } #[test] fn bundled_apple_chain_links_wwdr_g3_to_root() { let wwdr = Certificate::from_der(APPLE_WWDR_G3_CERTIFICATE_DER).unwrap(); let root = Certificate::from_der(APPLE_ROOT_CERTIFICATE_DER).unwrap(); let candidates = [wwdr.clone(), root.clone()]; let chain = certificate_chain_for_signer(&wwdr, &candidates).unwrap(); assert_eq!(chain, vec![root]); } }