Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
airdrop
src
identity.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
identity.rs
Blame
History
Raw
//! Who we are on the wire: a certificate chain, its key and, for an Apple //! identity, the validation record Apple signed for it. //! //! Two kinds exist. An *Apple* identity is what `antarctic-sharingd` fetches //! (`apple-crates/sharing`): it is what lets a Contacts Only device recognise //! us. A *self-signed* one is what AirDrop in Everyone mode always used, and //! what we fall back to when no account is signed in. use std::str::FromStr; use std::time::Duration; use rsa::RsaPrivateKey; use rsa::pkcs1v15::{Signature, SigningKey}; use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey}; use rsa::signature::Keypair; use rustls_pki_types::pem::PemObject; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use thiserror::Error; use x509_cert::Certificate; use x509_cert::builder::{Builder, CertificateBuilder, Profile}; use x509_cert::der::{Decode, Encode, pem::LineEnding}; use x509_cert::name::Name; use x509_cert::serial_number::SerialNumber; use x509_cert::spki::SubjectPublicKeyInfoOwned; use x509_cert::time::Validity; use zeroize::Zeroizing; use crate::apple; #[derive(Debug, Error)] pub enum IdentityError { #[error("no certificate in the identity")] NoCertificate, #[error("the identity's key could not be read: {0}")] Key(String), #[error("the key does not belong to the certificate")] KeyMismatch, #[error("could not create a self-signed identity: {0}")] Create(String), } /// The certificate chain (leaf first), key and record we present. #[derive(Clone)] pub struct Identity { chain: Vec<CertificateDer<'static>>, key: Zeroizing<Vec<u8>>, record: Option<Vec<u8>>, } impl std::fmt::Debug for Identity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Identity") .field("certificates", &self.chain.len()) .field("account", &self.account()) .field("record", &self.record.as_ref().map(Vec::len)) .finish_non_exhaustive() } } impl Identity { /// An identity from PEM text. `certificate_pem` may hold the whole chain; /// `intermediate_pem` (possibly empty) is appended when it is not there /// already. The key is PKCS#8 PEM and must belong to the leaf. pub fn from_pem( certificate_pem: &[u8], intermediate_pem: &[u8], key_pem: &[u8], record: Option<Vec<u8>>, ) -> Result<Identity, IdentityError> { let mut chain: Vec<CertificateDer<'static>> = Vec::new(); for pem in [certificate_pem, intermediate_pem] { for cert in CertificateDer::pem_slice_iter(pem) { let cert = cert.map_err(|_| IdentityError::NoCertificate)?; if !chain.contains(&cert) { chain.push(cert); } } } if chain.is_empty() { return Err(IdentityError::NoCertificate); } let key = RsaPrivateKey::from_pkcs8_pem( std::str::from_utf8(key_pem).map_err(|e| IdentityError::Key(e.to_string()))?, ) .map_err(|e| IdentityError::Key(e.to_string()))?; Self::new(chain, &key, record) } fn new( chain: Vec<CertificateDer<'static>>, key: &RsaPrivateKey, record: Option<Vec<u8>>, ) -> Result<Identity, IdentityError> { let leaf = Certificate::from_der(chain[0].as_ref()).map_err(|_| IdentityError::NoCertificate)?; let public = leaf .tbs_certificate .subject_public_key_info .to_der() .map_err(|_| IdentityError::NoCertificate)?; let ours = rsa::pkcs8::EncodePublicKey::to_public_key_der(&key.to_public_key()) .map_err(|e| IdentityError::Key(e.to_string()))?; if ours.as_bytes() != public.as_slice() { return Err(IdentityError::KeyMismatch); } let key = key .to_pkcs8_der() .map_err(|e| IdentityError::Key(e.to_string()))?; Ok(Identity { chain, key: Zeroizing::new(key.as_bytes().to_vec()), record, }) } /// A fresh self-signed identity named `name`, valid for ten years: what /// `openssl req -x509 -newkey rsa:2048` made. Generating the key takes a /// moment; keep the result (see [`Identity::to_pem`]). pub fn self_signed(name: &str) -> Result<Identity, IdentityError> { let create = |e: &dyn std::fmt::Display| IdentityError::Create(e.to_string()); let key = RsaPrivateKey::new(&mut rand_core::OsRng, 2048).map_err(|e| create(&e))?; let signer = SigningKey::<sha2::Sha256>::new(key.clone()); let mut serial = [0u8; 8]; rand_fill(&mut serial); serial[0] &= 0x7f; let subject = Name::from_str(&format!("CN={}", escape_cn(name))).map_err(|e| create(&e))?; let spki = SubjectPublicKeyInfoOwned::from_key(signer.verifying_key()).map_err(|e| create(&e))?; // No KeyUsage: `Profile::Root` adds a critical "Certificate Sign, CRL // Sign", which forbids the digital signature a TLS handshake needs, and // an iPhone answers such a certificate with `illegal_parameter`. What // `openssl req -x509` made (the certificate that always worked) has // only basic constraints and key identifiers. let mut builder = CertificateBuilder::new( Profile::Manual { issuer: None }, SerialNumber::new(&serial).map_err(|e| create(&e))?, Validity::from_now(Duration::from_secs(10 * 365 * 24 * 3600)) .map_err(|e| create(&e))?, subject, spki, &signer, ) .map_err(|e| create(&e))?; builder .add_extension(&x509_cert::ext::pkix::BasicConstraints { ca: true, path_len_constraint: None, }) .map_err(|e| create(&e))?; let cert = builder.build::<Signature>().map_err(|e| create(&e))?; let der = cert.to_der().map_err(|e| create(&e))?; Self::new(vec![CertificateDer::from(der)], &key, None) } /// `(certificate chain PEM, key PEM)`, to keep a self-signed identity. pub fn to_pem(&self) -> Result<(String, Zeroizing<String>), IdentityError> { let mut certs = String::new(); for cert in &self.chain { let parsed = Certificate::from_der(cert.as_ref()) .map_err(|e| IdentityError::Key(e.to_string()))?; certs.push_str( &x509_cert::der::EncodePem::to_pem(&parsed, LineEnding::LF) .map_err(|e| IdentityError::Key(e.to_string()))?, ); } let key = RsaPrivateKey::from_pkcs8_der(&self.key) .map_err(|e| IdentityError::Key(e.to_string()))? .to_pkcs8_pem(LineEnding::LF) .map_err(|e| IdentityError::Key(e.to_string()))?; Ok((certs, Zeroizing::new(key.to_string()))) } /// Replace the validation record (renewed monthly). pub fn with_record(mut self, record: Option<Vec<u8>>) -> Identity { self.record = record; self } /// The certificate chain, leaf first. pub fn chain(&self) -> &[CertificateDer<'static>] { &self.chain } pub fn leaf(&self) -> &CertificateDer<'static> { &self.chain[0] } pub fn key(&self) -> PrivateKeyDer<'static> { PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(self.key.to_vec())) } /// The DER validation record that belongs with the leaf, if there is one. pub fn record(&self) -> Option<&[u8]> { self.record.as_deref() } /// The Apple account the leaf names, if Apple issued it. pub fn account(&self) -> Option<String> { let rest: Vec<&[u8]> = self.chain[1..].iter().map(|c| c.as_ref()).collect(); apple::certificate_account(self.leaf().as_ref(), &rest) } pub fn is_apple(&self) -> bool { self.account().is_some() } } /// `x509_cert`'s name parser takes RFC 4514 text: escape what it reserves. fn escape_cn(name: &str) -> String { let mut out = String::with_capacity(name.len()); for c in name.chars().filter(|c| !c.is_control()) { if matches!(c, ',' | '+' | '"' | '\\' | '<' | '>' | ';' | '=' | '#') { out.push('\\'); } out.push(c); } if out.is_empty() { "airdrop".into() } else { out } } fn rand_fill(buf: &mut [u8]) { use rand_core::RngCore; rand_core::OsRng.fill_bytes(buf); } #[cfg(test)] mod tests { use super::*; #[test] fn a_self_signed_identity_round_trips_and_is_not_apple() { let id = Identity::self_signed("Test, Laptop").unwrap(); assert!(!id.is_apple()); assert_eq!(id.chain().len(), 1); let (certs, key) = id.to_pem().unwrap(); let again = Identity::from_pem(certs.as_bytes(), b"", key.as_bytes(), None).unwrap(); assert_eq!(again.leaf(), id.leaf()); } #[test] fn a_self_signed_certificate_has_no_key_usage_restriction() { let id = Identity::self_signed("t").unwrap(); let cert = Certificate::from_der(id.leaf().as_ref()).unwrap(); let ext = cert.tbs_certificate.extensions.unwrap_or_default(); let key_usage = const_oid::db::rfc5280::ID_CE_KEY_USAGE; assert!(ext.iter().all(|e| e.extn_id != key_usage)); } #[test] fn a_key_that_does_not_match_is_refused() { let a = Identity::self_signed("a").unwrap(); let b = Identity::self_signed("b").unwrap(); let (certs, _) = a.to_pem().unwrap(); let (_, other_key) = b.to_pem().unwrap(); assert!(matches!( Identity::from_pem(certs.as_bytes(), b"", other_key.as_bytes(), None), Err(IdentityError::KeyMismatch) )); } }