From 9d61af6ce8ecaf2f3257eda9d5cd6ccf60b62b0f Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Mon, 9 Mar 2026 15:32:31 +0100 Subject: [PATCH] feat(native-certgen): implement ASN.1 attestation extension encoder DER encoder for Android KeyMint attestation extension (OID 1.3.6.1.4.1.11129.2.1.17). SecurityLevel and VerifiedBootState as ENUMERATED, EXPLICIT context-specific tagging with long-form for tags >= 31, sorted AuthorizationList fields, RootOfTrust with BOOLEAN TRUE=0xFF, SET OF INTEGER with DER sort, DO_NOT_REPORT sentinel omission. --- native-certgen/src/attestation.rs | 644 ++++++++++++++++++++++++++++++ native-certgen/src/lib.rs | 1 + 2 files changed, 645 insertions(+) create mode 100644 native-certgen/src/attestation.rs diff --git a/native-certgen/src/attestation.rs b/native-certgen/src/attestation.rs new file mode 100644 index 0000000..d379d89 --- /dev/null +++ b/native-certgen/src/attestation.rs @@ -0,0 +1,644 @@ +use crate::error::Result; +use crate::types::CertGenParams; + +const DO_NOT_REPORT: i32 = -1; + +pub fn build_attestation_extension(params: &CertGenParams) -> Result> { + let sw = build_software_enforced(params)?; + let tee = build_tee_enforced(params)?; + + let mut inner = Vec::new(); + // attestationVersion — INTEGER + inner.extend_from_slice(&enc_integer(params.attest_version as i64)); + // attestationSecurityLevel — ENUMERATED, not INTEGER + inner.extend_from_slice(&enc_enumerated(params.security_level)); + // keymintVersion — INTEGER + inner.extend_from_slice(&enc_integer(params.keymaster_version as i64)); + // keymintSecurityLevel — ENUMERATED, not INTEGER + inner.extend_from_slice(&enc_enumerated(params.security_level)); + // attestationChallenge — OCTET STRING + inner.extend_from_slice(&enc_octet_string( + params.attestation_challenge.as_deref().unwrap_or(&[]), + )); + // uniqueId — OCTET STRING (always empty) + inner.extend_from_slice(&enc_octet_string(&[])); + // softwareEnforced + inner.extend_from_slice(&sw); + // teeEnforced + inner.extend_from_slice(&tee); + + Ok(enc_sequence(&inner)) +} + +fn build_software_enforced(params: &CertGenParams) -> Result> { + let mut fields: Vec<(u32, Vec)> = Vec::new(); + + // Tag 701: CREATION_DATETIME — INTEGER (milliseconds) + fields.push((701, enc_integer(params.creation_datetime))); + + // Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING + // The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING + // by the Kotlin layer. We wrap them in an EXPLICIT tag. + if !params.attestation_application_id.is_empty() { + fields.push((709, enc_octet_string(¶ms.attestation_application_id))); + } + + // Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400) + if params.attest_version >= 400 { + if let Some(ref hash) = params.module_hash { + fields.push((724, enc_octet_string(hash))); + } + } + + Ok(build_authorization_list(&mut fields)) +} + +fn build_tee_enforced(params: &CertGenParams) -> Result> { + let mut fields: Vec<(u32, Vec)> = Vec::new(); + + // Tag 1: PURPOSE — SET OF INTEGER + if !params.purposes.is_empty() { + fields.push((1, build_set_of_integer(¶ms.purposes))); + } + + // Tag 2: ALGORITHM — INTEGER + fields.push((2, enc_integer(params.algorithm as i32 as i64))); + + // Tag 3: KEY_SIZE — INTEGER + fields.push((3, enc_integer(params.key_size as i64))); + + // Tag 5: DIGEST — SET OF INTEGER + if !params.digests.is_empty() { + fields.push((5, build_set_of_integer(¶ms.digests))); + } + + // Tag 10: EC_CURVE — INTEGER (only for EC keys) + if let Some(curve) = params.ec_curve { + fields.push((10, enc_integer(curve as i32 as i64))); + } + + // Tag 503: NO_AUTH_REQUIRED — NULL (presence = true) + fields.push((503, enc_null())); + + // Tag 702: ORIGIN — INTEGER 0 (GENERATED) + fields.push((702, enc_integer(0))); + + // Tag 704: ROOT_OF_TRUST — SEQUENCE + fields.push((704, build_root_of_trust(params))); + + // Tag 705: OS_VERSION — INTEGER + if params.os_version != DO_NOT_REPORT { + fields.push((705, enc_integer(params.os_version as i64))); + } + + // Tag 706: OS_PATCHLEVEL — INTEGER + if params.os_patch_level != DO_NOT_REPORT { + fields.push((706, enc_integer(params.os_patch_level as i64))); + } + + // Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional) + if let Some(ref v) = params.id_brand { + fields.push((710, enc_octet_string(v))); + } + if let Some(ref v) = params.id_device { + fields.push((711, enc_octet_string(v))); + } + if let Some(ref v) = params.id_product { + fields.push((712, enc_octet_string(v))); + } + if let Some(ref v) = params.id_serial { + fields.push((713, enc_octet_string(v))); + } + if let Some(ref v) = params.id_imei { + fields.push((714, enc_octet_string(v))); + } + if let Some(ref v) = params.id_meid { + fields.push((715, enc_octet_string(v))); + } + if let Some(ref v) = params.id_manufacturer { + fields.push((716, enc_octet_string(v))); + } + if let Some(ref v) = params.id_model { + fields.push((717, enc_octet_string(v))); + } + + // Tag 718: VENDOR_PATCHLEVEL — INTEGER + if params.vendor_patch_level != DO_NOT_REPORT { + fields.push((718, enc_integer(params.vendor_patch_level as i64))); + } + + // Tag 719: BOOT_PATCHLEVEL — INTEGER + if params.boot_patch_level != DO_NOT_REPORT { + fields.push((719, enc_integer(params.boot_patch_level as i64))); + } + + // Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300) + if params.attest_version >= 300 { + if let Some(ref v) = params.id_second_imei { + fields.push((723, enc_octet_string(v))); + } + } + + Ok(build_authorization_list(&mut fields)) +} + +fn build_root_of_trust(params: &CertGenParams) -> Vec { + let mut inner = Vec::new(); + // verifiedBootKey — OCTET STRING (32 bytes) + inner.extend_from_slice(&enc_octet_string(¶ms.boot_key)); + // deviceLocked — BOOLEAN TRUE (0xFF, not 0x01) + inner.extend_from_slice(&enc_boolean(true)); + // verifiedBootState — ENUMERATED 0 (Verified), not INTEGER + inner.extend_from_slice(&enc_enumerated(0)); + // verifiedBootHash — OCTET STRING (32 bytes) + inner.extend_from_slice(&enc_octet_string(¶ms.boot_hash)); + enc_sequence(&inner) +} + +fn build_authorization_list(fields: &mut Vec<(u32, Vec)>) -> Vec { + fields.sort_by_key(|(tag, _)| *tag); + let mut inner = Vec::new(); + for (tag, value) in fields.iter() { + inner.extend_from_slice(&enc_explicit_tag(*tag, value)); + } + enc_sequence(&inner) +} + +fn build_set_of_integer(values: &[i32]) -> Vec { + // DER SET OF: elements sorted by encoded byte value + let mut encoded: Vec> = values.iter().map(|v| enc_integer(*v as i64)).collect(); + encoded.sort(); + let mut inner = Vec::new(); + for e in &encoded { + inner.extend_from_slice(e); + } + enc_set(&inner) +} + +// --- DER primitives --- + +fn enc_length(len: usize) -> Vec { + if len < 0x80 { + vec![len as u8] + } else if len <= 0xFF { + vec![0x81, len as u8] + } else if len <= 0xFFFF { + vec![0x82, (len >> 8) as u8, len as u8] + } else if len <= 0xFF_FFFF { + vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8] + } else { + vec![ + 0x84, + (len >> 24) as u8, + (len >> 16) as u8, + (len >> 8) as u8, + len as u8, + ] + } +} + +fn enc_integer(value: i64) -> Vec { + // DER INTEGER: tag 0x02, minimal two's complement big-endian + let bytes = integer_bytes(value); + let mut out = vec![0x02]; + out.extend_from_slice(&enc_length(bytes.len())); + out.extend_from_slice(&bytes); + out +} + +fn integer_bytes(value: i64) -> Vec { + if value == 0 { + return vec![0x00]; + } + let raw = value.to_be_bytes(); + // Find first significant byte + let mut start = 0; + if value > 0 { + while start < 7 && raw[start] == 0x00 { + start += 1; + } + // If high bit set, need leading 0x00 to keep positive + if raw[start] & 0x80 != 0 { + let mut out = vec![0x00]; + out.extend_from_slice(&raw[start..]); + return out; + } + } else { + while start < 7 && raw[start] == 0xFF { + start += 1; + } + // If high bit clear, need leading 0xFF to keep negative + if raw[start] & 0x80 == 0 { + let mut out = vec![0xFF]; + out.extend_from_slice(&raw[start..]); + return out; + } + } + raw[start..].to_vec() +} + +fn enc_enumerated(value: i32) -> Vec { + // DER ENUMERATED: tag 0x0A, same value encoding as INTEGER + let bytes = integer_bytes(value as i64); + let mut out = vec![0x0A]; + out.extend_from_slice(&enc_length(bytes.len())); + out.extend_from_slice(&bytes); + out +} + +fn enc_octet_string(data: &[u8]) -> Vec { + let mut out = vec![0x04]; + out.extend_from_slice(&enc_length(data.len())); + out.extend_from_slice(data); + out +} + +fn enc_null() -> Vec { + vec![0x05, 0x00] +} + +fn enc_boolean(value: bool) -> Vec { + // DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00 + vec![0x01, 0x01, if value { 0xFF } else { 0x00 }] +} + +fn enc_sequence(contents: &[u8]) -> Vec { + let mut out = vec![0x30]; + out.extend_from_slice(&enc_length(contents.len())); + out.extend_from_slice(contents); + out +} + +fn enc_set(contents: &[u8]) -> Vec { + let mut out = vec![0x31]; + out.extend_from_slice(&enc_length(contents.len())); + out.extend_from_slice(contents); + out +} + +fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec { + // EXPLICIT context-specific constructed tag + let mut out = Vec::new(); + if tag_number < 31 { + // Short form: single byte 0xA0 | tag_number + out.push(0xA0 | tag_number as u8); + } else { + // Long form: 0xBF followed by base-128 encoding of tag number + out.push(0xBF); + enc_base128_tag(&mut out, tag_number); + } + out.extend_from_slice(&enc_length(inner.len())); + out.extend_from_slice(inner); + out +} + +fn enc_base128_tag(out: &mut Vec, tag: u32) { + // Base-128 with continuation bits: MSB first, bit 7 set on all but last byte + let mut digits = Vec::new(); + let mut val = tag; + digits.push((val & 0x7F) as u8); + val >>= 7; + while val > 0 { + digits.push((val & 0x7F) as u8 | 0x80); + val >>= 7; + } + // Written MSB first + for b in digits.iter().rev() { + out.push(*b); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{Algorithm, EcCurve}; + + #[test] + fn test_enc_integer_zero() { + assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]); + } + + #[test] + fn test_enc_integer_small_positive() { + assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]); + assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]); + } + + #[test] + fn test_enc_integer_needs_leading_zero() { + // 128 = 0x80, high bit set so needs 0x00 prefix + assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]); + assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]); + } + + #[test] + fn test_enc_integer_multi_byte() { + // 140000 = 0x02_22_E0 + assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]); + } + + #[test] + fn test_enc_integer_large() { + // 20250301 = 0x01_34_FE_BD + assert_eq!( + enc_integer(20250301), + vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD] + ); + } + + #[test] + fn test_enc_enumerated() { + // SecurityLevel TEE = 1 + assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]); + // VerifiedBootState Verified = 0 + assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]); + } + + #[test] + fn test_enc_boolean_true() { + // DER: TRUE = 0xFF + assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]); + } + + #[test] + fn test_enc_null() { + assert_eq!(enc_null(), vec![0x05, 0x00]); + } + + #[test] + fn test_enc_octet_string_empty() { + assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]); + } + + #[test] + fn test_enc_explicit_tag_short() { + // Tag 1 wrapping INTEGER 2: A1 03 02 01 02 + let inner = enc_integer(2); + let tagged = enc_explicit_tag(1, &inner); + assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]); + } + + #[test] + fn test_enc_explicit_tag_10() { + // Tag 10: 0xAA + let inner = enc_integer(1); + let tagged = enc_explicit_tag(10, &inner); + assert_eq!(tagged[0], 0xAA); + } + + #[test] + fn test_enc_explicit_tag_503() { + // Tag 503: 0xBF 0x83 0x77 + // 503 = 3*128 + 119 => 0x83 0x77 + let inner = enc_null(); + let tagged = enc_explicit_tag(503, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]); + } + + #[test] + fn test_enc_explicit_tag_704() { + // Tag 704: 0xBF 0x85 0x40 + // 704 = 5*128 + 64 => 0x85 0x40 + let inner = enc_sequence(&[]); + let tagged = enc_explicit_tag(704, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]); + } + + #[test] + fn test_enc_explicit_tag_718() { + // Tag 718: 0xBF 0x85 0x4E + let inner = enc_integer(20250301); + let tagged = enc_explicit_tag(718, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]); + } + + #[test] + fn test_enc_explicit_tag_719() { + // Tag 719: 0xBF 0x85 0x4F + let inner = enc_integer(20250301); + let tagged = enc_explicit_tag(719, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]); + } + + #[test] + fn test_enc_explicit_tag_701() { + // Tag 701: 0xBF 0x85 0x3D + let inner = enc_integer(1000); + let tagged = enc_explicit_tag(701, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]); + } + + #[test] + fn test_enc_explicit_tag_709() { + // Tag 709: 0xBF 0x85 0x45 + let inner = enc_octet_string(&[0x01]); + let tagged = enc_explicit_tag(709, &inner); + assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]); + } + + #[test] + fn test_build_set_of_integer_sorted() { + // SET OF INTEGER must sort by encoded bytes + let result = build_set_of_integer(&[3, 2]); + // Expect sorted: INTEGER 2 before INTEGER 3 + let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]); + assert_eq!(result, expected); + } + + #[test] + fn test_root_of_trust_structure() { + let params = make_test_params(); + let rot = build_root_of_trust(¶ms); + // Should be a SEQUENCE (0x30) + assert_eq!(rot[0], 0x30); + // Find BOOLEAN TRUE inside + let rot_inner = &rot[2..]; // skip tag+length + // First: OCTET STRING (32 bytes boot key) + assert_eq!(rot_inner[0], 0x04); + assert_eq!(rot_inner[1], 0x20); // 32 bytes + // After boot key (34 bytes): BOOLEAN TRUE + assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag + assert_eq!(rot_inner[35], 0x01); // length 1 + assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF + // Then ENUMERATED 0 (verifiedBootState) + assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02 + assert_eq!(rot_inner[38], 0x01); + assert_eq!(rot_inner[39], 0x00); + } + + #[test] + fn test_do_not_report_omits_fields() { + let mut params = make_test_params(); + params.os_patch_level = DO_NOT_REPORT; + params.vendor_patch_level = DO_NOT_REPORT; + params.boot_patch_level = DO_NOT_REPORT; + let tee = build_tee_enforced(¶ms).unwrap(); + let hex = hex_string(&tee); + // Tags 706, 718, 719 should not appear + // Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F + assert!(!hex.contains("bf8542"), "os_patch_level should be omitted"); + assert!( + !hex.contains("bf854e"), + "vendor_patch_level should be omitted" + ); + assert!( + !hex.contains("bf854f"), + "boot_patch_level should be omitted" + ); + } + + #[test] + fn test_key_description_security_level_is_enumerated() { + let params = make_test_params(); + let ext = build_attestation_extension(¶ms).unwrap(); + // KeyDescription is a SEQUENCE: 0x30 ... + assert_eq!(ext[0], 0x30); + // Skip SEQUENCE tag + length to get to inner fields + let inner = skip_tlv_header(&ext); + // Field 0: attestationVersion — INTEGER (0x02) + assert_eq!(inner[0], 0x02); + let (_, rest) = skip_one_tlv(inner); + // Field 1: attestationSecurityLevel — ENUMERATED (0x0A) + assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED"); + let (_, rest) = skip_one_tlv(rest); + // Field 2: keymintVersion — INTEGER (0x02) + assert_eq!(rest[0], 0x02); + let (_, rest) = skip_one_tlv(rest); + // Field 3: keymintSecurityLevel — ENUMERATED (0x0A) + assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED"); + } + + #[test] + fn test_authorization_list_sorted_by_tag() { + let params = make_test_params(); + let tee = build_tee_enforced(¶ms).unwrap(); + let inner = skip_tlv_header(&tee); + let tags = extract_tag_numbers(inner); + let mut sorted = tags.clone(); + sorted.sort(); + assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number"); + } + + #[test] + fn test_full_extension_roundtrip() { + let params = make_test_params(); + let ext = build_attestation_extension(¶ms).unwrap(); + // Must be valid DER: starts with SEQUENCE tag + assert_eq!(ext[0], 0x30); + // Length must account for all inner bytes + let (header_len, total_content_len) = parse_tlv_lengths(&ext); + assert_eq!(ext.len(), header_len + total_content_len); + } + + // --- test helpers --- + + fn make_test_params() -> CertGenParams { + CertGenParams { + algorithm: Algorithm::Ec, + key_size: 256, + ec_curve: Some(EcCurve::P256), + rsa_public_exponent: 0, + attestation_challenge: Some(vec![0xAB; 32]), + purposes: vec![2, 3], + digests: vec![4], + cert_serial: None, + cert_subject: None, + cert_not_before: -1, + cert_not_after: -1, + keybox_private_key: vec![], + keybox_cert_chain: vec![], + security_level: 1, + attest_version: 200, + keymaster_version: 200, + os_version: 140000, + os_patch_level: 202503, + vendor_patch_level: 20250301, + boot_patch_level: 20250301, + boot_key: vec![0x01; 32], + boot_hash: vec![0x02; 32], + creation_datetime: 1709913600000, + attestation_application_id: vec![0xDE, 0xAD], + module_hash: None, + id_brand: None, + id_device: None, + id_product: None, + id_serial: None, + id_imei: None, + id_meid: None, + id_manufacturer: None, + id_model: None, + id_second_imei: None, + } + } + + fn hex_string(data: &[u8]) -> String { + data.iter().map(|b| format!("{:02x}", b)).collect() + } + + fn skip_tlv_header(data: &[u8]) -> &[u8] { + let (header_len, _) = parse_tlv_lengths(data); + &data[header_len..] + } + + fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) { + let (header_len, content_len) = parse_tlv_lengths(data); + let total = header_len + content_len; + (total, &data[total..]) + } + + fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) { + // Returns (header_bytes, content_bytes) + let tag_len = tag_byte_len(data); + let len_start = tag_len; + if data[len_start] < 0x80 { + (len_start + 1, data[len_start] as usize) + } else { + let num_len_bytes = (data[len_start] & 0x7F) as usize; + let mut content_len = 0usize; + for i in 0..num_len_bytes { + content_len = (content_len << 8) | data[len_start + 1 + i] as usize; + } + (len_start + 1 + num_len_bytes, content_len) + } + } + + fn tag_byte_len(data: &[u8]) -> usize { + if data[0] & 0x1F != 0x1F { + 1 + } else { + let mut i = 1; + while data[i] & 0x80 != 0 { + i += 1; + } + i + 1 + } + } + + fn extract_tag_numbers(mut data: &[u8]) -> Vec { + let mut tags = Vec::new(); + while !data.is_empty() { + let tag = read_tag_number(data); + tags.push(tag); + let (_, rest) = skip_one_tlv(data); + data = rest; + } + tags + } + + fn read_tag_number(data: &[u8]) -> u32 { + if data[0] & 0x1F != 0x1F { + (data[0] & 0x1F) as u32 + } else { + let mut val = 0u32; + let mut i = 1; + loop { + val = (val << 7) | (data[i] & 0x7F) as u32; + if data[i] & 0x80 == 0 { + break; + } + i += 1; + } + val + } + } +} diff --git a/native-certgen/src/lib.rs b/native-certgen/src/lib.rs index 2dfacc9..4335d6c 100644 --- a/native-certgen/src/lib.rs +++ b/native-certgen/src/lib.rs @@ -2,3 +2,4 @@ mod error; mod types; mod keygen; pub mod keybox; +pub mod attestation;