diff --git a/native-certgen/src/certbuilder.rs b/native-certgen/src/certbuilder.rs new file mode 100644 index 0000000..a3426ed --- /dev/null +++ b/native-certgen/src/certbuilder.rs @@ -0,0 +1,233 @@ +use crate::error::{CertGenError, Result}; +use crate::keybox::ParsedKeybox; +use crate::types::{CertGenParams, GeneratedKeyPair}; + +use rcgen::{ + BasicConstraints, CertificateParams, CustomExtension, DistinguishedName, DnType, IsCa, + KeyPair, KeyUsagePurpose, SerialNumber, +}; +use time::OffsetDateTime; + +const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17]; + +pub fn build_certificate_chain( + key_pair: &GeneratedKeyPair, + attestation_ext_der: &[u8], + keybox: &ParsedKeybox, + params: &CertGenParams, +) -> Result>> { + let issuer_key = KeyPair::try_from(keybox.signing_key_der.as_slice()) + .map_err(|e| CertGenError::CertBuildFailed(format!("keybox key parse: {e}")))?; + + let issuer_cert = build_issuer_cert(&issuer_key, &keybox.issuer_dn_der)?; + + let subject_key = KeyPair::try_from(key_pair.private_key_pkcs8.as_slice()) + .map_err(|e| CertGenError::CertBuildFailed(format!("subject key parse: {e}")))?; + + let leaf_params = build_leaf_params(attestation_ext_der, keybox, params)?; + + let leaf_cert = leaf_params + .signed_by(&subject_key, &issuer_cert, &issuer_key) + .map_err(|e| CertGenError::CertBuildFailed(format!("signing: {e}")))?; + + let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len()); + chain.push(leaf_cert.der().to_vec()); + for cert_der in &keybox.cert_chain_ders { + chain.push(cert_der.clone()); + } + + Ok(chain) +} + +fn build_issuer_cert( + issuer_key: &KeyPair, + issuer_dn_der: &[u8], +) -> Result { + let mut issuer_params = CertificateParams::default(); + issuer_params.distinguished_name = parse_dn_from_der(issuer_dn_der)?; + issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + // Suppress AKI/SKI generation — we only need this cert as a signing vehicle + issuer_params.key_identifier_method = rcgen::KeyIdMethod::PreSpecified(vec![]); + + issuer_params + .self_signed(issuer_key) + .map_err(|e| CertGenError::CertBuildFailed(format!("issuer self-sign: {e}"))) +} + +fn build_leaf_params( + attestation_ext_der: &[u8], + keybox: &ParsedKeybox, + params: &CertGenParams, +) -> Result { + let mut cp = CertificateParams::default(); + + // Subject DN + cp.distinguished_name = if let Some(ref subject_der) = params.cert_subject { + parse_dn_from_der(subject_der)? + } else { + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, "Android KeyStore Key"); + dn + }; + + // Serial number + cp.serial_number = if let Some(ref serial_bytes) = params.cert_serial { + Some(SerialNumber::from(serial_bytes.clone())) + } else { + Some(SerialNumber::from(vec![1u8])) + }; + + // Validity period + cp.not_before = timestamp_to_datetime(params.cert_not_before)?; + cp.not_after = if params.cert_not_after == -1 { + // Fall back to keybox leaf cert's notAfter, or +1 year + OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after) + .unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365)) + } else { + timestamp_to_datetime(params.cert_not_after)? + }; + + // Suppress AKI/SKI — Android attestation leaf certs don't include these + cp.key_identifier_method = rcgen::KeyIdMethod::PreSpecified(vec![]); + + // KeyUsage from purposes + cp.key_usages = map_key_usages(¶ms.purposes); + + // Attestation extension (non-critical) + let mut attest_ext = CustomExtension::from_oid_content(ATTESTATION_OID, attestation_ext_der.to_vec()); + attest_ext.set_criticality(false); + cp.custom_extensions.push(attest_ext); + + Ok(cp) +} + +fn map_key_usages(purposes: &[i32]) -> Vec { + let mut usages = Vec::new(); + for &purpose in purposes { + match purpose { + 2 | 3 => { + // SIGN or VERIFY -> digitalSignature + if !usages.contains(&KeyUsagePurpose::DigitalSignature) { + usages.push(KeyUsagePurpose::DigitalSignature); + } + } + 0 => { + // ENCRYPT -> keyEncipherment + if !usages.contains(&KeyUsagePurpose::KeyEncipherment) { + usages.push(KeyUsagePurpose::KeyEncipherment); + } + } + 1 => { + // DECRYPT -> dataEncipherment + if !usages.contains(&KeyUsagePurpose::DataEncipherment) { + usages.push(KeyUsagePurpose::DataEncipherment); + } + } + 5 => { + // WRAP_KEY -> keyEncipherment + if !usages.contains(&KeyUsagePurpose::KeyEncipherment) { + usages.push(KeyUsagePurpose::KeyEncipherment); + } + } + 6 => { + // AGREE_KEY -> keyAgreement + if !usages.contains(&KeyUsagePurpose::KeyAgreement) { + usages.push(KeyUsagePurpose::KeyAgreement); + } + } + 7 => { + // ATTEST_KEY -> keyCertSign + if !usages.contains(&KeyUsagePurpose::KeyCertSign) { + usages.push(KeyUsagePurpose::KeyCertSign); + } + } + _ => {} + } + } + usages +} + +fn timestamp_to_datetime(ts: i64) -> Result { + if ts == -1 { + return Ok(OffsetDateTime::now_utc()); + } + // Params use milliseconds for validity timestamps + OffsetDateTime::from_unix_timestamp(ts / 1000) + .map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}"))) +} + +// Parse a DER-encoded X.500 Name into rcgen DistinguishedName. +// We only extract the CN (most common for Android keystore certs). +// If parsing fails, fall back to empty DN. +fn parse_dn_from_der(der: &[u8]) -> Result { + use x509_cert::name::Name; + use der::{Decode, Encode}; + + let name = Name::from_der(der) + .map_err(|e| CertGenError::CertBuildFailed(format!("DN parse: {e}")))?; + + let mut dn = DistinguishedName::new(); + + for rdn in name.0.iter() { + for atv in rdn.0.iter() { + let oid_str = atv.oid.to_string(); + // Map common OIDs to rcgen DnType + let dn_type = match oid_str.as_str() { + "2.5.4.3" => DnType::CommonName, + "2.5.4.6" => DnType::CountryName, + "2.5.4.7" => DnType::LocalityName, + "2.5.4.8" => DnType::StateOrProvinceName, + "2.5.4.10" => DnType::OrganizationName, + "2.5.4.11" => DnType::OrganizationalUnitName, + other => DnType::CustomDnType( + other.split('.').filter_map(|s| s.parse().ok()).collect(), + ), + }; + + // Extract the string value from the AttributeValue (ANY type) + // The value is DER-encoded; try to read it as UTF8String or PrintableString + let value_bytes = atv.value.to_der() + .map_err(|e| CertGenError::CertBuildFailed(format!("DN value encode: {e}")))?; + let value_str = extract_string_from_der_any(&value_bytes); + dn.push(dn_type, value_str); + } + } + + Ok(dn) +} + +// Extract a string from a DER-encoded ASN.1 string type (UTF8String, PrintableString, etc.) +fn extract_string_from_der_any(der: &[u8]) -> String { + if der.len() < 2 { + return String::new(); + } + // Tag byte at [0], length at [1..], then content + let tag = der[0]; + let (content_len, header_len) = if der[1] < 0x80 { + (der[1] as usize, 2) + } else { + let num = (der[1] & 0x7f) as usize; + if num == 0 || 2 + num > der.len() { + return String::new(); + } + let mut len = 0usize; + for i in 0..num { + len = (len << 8) | der[2 + i] as usize; + } + (len, 2 + num) + }; + + let end = header_len + content_len; + if end > der.len() { + return String::new(); + } + let content = &der[header_len..end]; + + match tag { + 0x0C | 0x13 | 0x16 => { + // UTF8String (0x0C), PrintableString (0x13), IA5String (0x16) + String::from_utf8_lossy(content).into_owned() + } + _ => String::from_utf8_lossy(content).into_owned(), + } +} diff --git a/native-certgen/src/lib.rs b/native-certgen/src/lib.rs index 9bbc550..147ae6e 100644 --- a/native-certgen/src/lib.rs +++ b/native-certgen/src/lib.rs @@ -3,4 +3,5 @@ mod types; mod keygen; pub mod keybox; pub mod attestation; +pub mod certbuilder; pub mod logging;