feat(native-certgen): implement X.509 certificate chain builder

Builds v3 leaf certificate with attestation extension and KeyUsage,
signs with keybox private key via rcgen 0.13.2. Assembles full chain
(leaf + keybox intermediates + root). Supports EC and RSA keybox
signing keys. Uses rcgen's signed_by() with a synthesized issuer
Certificate — no manual DER fallback needed.
This commit is contained in:
Enginex0
2026-03-09 15:55:15 +01:00
parent 0725aed094
commit e76f5115e0
2 changed files with 234 additions and 0 deletions
+233
View File
@@ -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<Vec<Vec<u8>>> {
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<rcgen::Certificate> {
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<CertificateParams> {
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(&params.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<KeyUsagePurpose> {
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<OffsetDateTime> {
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<DistinguishedName> {
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(),
}
}
+1
View File
@@ -3,4 +3,5 @@ mod types;
mod keygen;
pub mod keybox;
pub mod attestation;
pub mod certbuilder;
pub mod logging;