Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
airdrop
src
testpki.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
testpki.rs
Blame
History
Raw
//! A throwaway Apple-shaped PKI for tests: a root, an intermediate, a record //! signer, and AirDrop client certificates, plus a signer for validation //! records. Keys are 1024-bit so the fixture builds in a moment; nothing here //! is ever used outside tests. use std::str::FromStr; use std::sync::OnceLock; use std::time::{Duration, SystemTime}; use cms::builder::{SignedDataBuilder, SignerInfoBuilder}; use cms::cert::{CertificateChoices, IssuerAndSerialNumber}; use cms::signed_data::{EncapsulatedContentInfo, SignerIdentifier}; use const_oid::db::rfc5911::ID_DATA; use der::asn1::OctetString; use der::{Any, Encode}; use rsa::RsaPrivateKey; use rsa::pkcs1v15::{Signature, SigningKey}; use rsa::signature::Keypair; use sha2::Sha256; use spki::AlgorithmIdentifierOwned; use x509_cert::Certificate; use x509_cert::builder::{Builder, CertificateBuilder, Profile}; use x509_cert::name::Name; use x509_cert::serial_number::SerialNumber; use x509_cert::spki::SubjectPublicKeyInfoOwned; use x509_cert::time::Validity; use crate::apple::{Anchors, CN_PREFIX}; pub struct Party { pub key: RsaPrivateKey, pub cert: Certificate, } pub struct Pki { pub root: Party, pub intermediate: Party, pub record_signer: Party, } fn key() -> RsaPrivateKey { RsaPrivateKey::new(&mut rand_core::OsRng, 1024).unwrap() } fn name(cn: &str) -> Name { Name::from_str(&format!("CN={cn}")).unwrap() } fn issue( profile: Profile, subject: &str, key: &RsaPrivateKey, signer: &RsaPrivateKey, ) -> Certificate { static SERIAL: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1); let serial = SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let signer = SigningKey::<Sha256>::new(signer.clone()); let spki = SubjectPublicKeyInfoOwned::from_key(SigningKey::<Sha256>::new(key.clone()).verifying_key()) .unwrap(); CertificateBuilder::new( profile, SerialNumber::from(serial), Validity::from_now(Duration::from_secs(365 * 24 * 3600)).unwrap(), name(subject), spki, &signer, ) .unwrap() .build::<Signature>() .unwrap() } impl Pki { pub fn new() -> Pki { let root_key = key(); let root = issue(Profile::Root, "Test Root", &root_key, &root_key); let int_key = key(); let intermediate = issue( Profile::SubCA { issuer: name("Test Root"), path_len_constraint: Some(1), }, "Test Intermediate", &int_key, &root_key, ); let signer_key = key(); let record_signer = issue( Profile::Leaf { issuer: name("Test Intermediate"), enable_key_agreement: false, enable_key_encipherment: false, include_subject_key_identifier: true, }, "TestRecordSigning", &signer_key, &int_key, ); Pki { root: Party { key: root_key, cert: root, }, intermediate: Party { key: int_key, cert: intermediate, }, record_signer: Party { key: signer_key, cert: record_signer, }, } } /// The shared instance: building one costs a few key generations. pub fn shared() -> &'static Pki { static PKI: OnceLock<Pki> = OnceLock::new(); PKI.get_or_init(Pki::new) } pub fn anchors(&self) -> Anchors { Anchors::new( vec![self.root.cert.clone()], vec![self.intermediate.cert.clone()], ) } /// An AirDrop client certificate for `account`, issued by the intermediate. pub fn client(&self, account: &str) -> (RsaPrivateKey, Certificate) { let k = key(); let cert = issue( Profile::Leaf { issuer: name("Test Intermediate"), enable_key_agreement: false, enable_key_encipherment: false, include_subject_key_identifier: true, }, &format!("{CN_PREFIX}{account}"), &k, &self.intermediate.key, ); (k, cert) } /// A signed validation record for `account` listing `hashes`, valid from /// `valid_as_of` for `duration_secs`; the signer's certificate and the /// intermediate are included, as Apple's are. pub fn record( &self, account: &str, hashes: &[String], valid_as_of: SystemTime, duration_secs: i64, ) -> Vec<u8> { let mut dict = plist::Dictionary::new(); dict.insert("altDsID".into(), plist::Value::String(account.into())); dict.insert( "ValidatedEmailHashes".into(), plist::Value::Array(hashes.iter().cloned().map(plist::Value::String).collect()), ); dict.insert( "ValidAsOf".into(), plist::Value::Date(plist::Date::from(valid_as_of)), ); dict.insert( "SuggestValidDuration".into(), plist::Value::Integer(duration_secs.into()), ); let mut content = Vec::new(); plist::Value::Dictionary(dict) .to_writer_binary(&mut content) .unwrap(); self.sign(&content) } /// CMS over arbitrary content. pub fn sign(&self, content: &[u8]) -> Vec<u8> { let encap = EncapsulatedContentInfo { econtent_type: ID_DATA, econtent: Some(Any::encode_from(&OctetString::new(content.to_vec()).unwrap()).unwrap()), }; let signer = SigningKey::<Sha256>::new(self.record_signer.key.clone()); let digest = AlgorithmIdentifierOwned { oid: const_oid::db::rfc5912::ID_SHA_256, parameters: None, }; let sid = SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { issuer: self.record_signer.cert.tbs_certificate.issuer.clone(), serial_number: self .record_signer .cert .tbs_certificate .serial_number .clone(), }); let info = SignerInfoBuilder::new(&signer, sid, digest.clone(), &encap, None).unwrap(); let mut builder = SignedDataBuilder::new(&encap); builder .add_digest_algorithm(digest) .unwrap() .add_certificate(CertificateChoices::Certificate( self.record_signer.cert.clone(), )) .unwrap() .add_certificate(CertificateChoices::Certificate( self.intermediate.cert.clone(), )) .unwrap() .add_signer_info::<SigningKey<Sha256>, Signature>(info) .unwrap(); builder.build().unwrap().to_der().unwrap() } }