From e02bae6f4331b8ca4e5f8635af1586e6f2618849 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Mon, 9 Mar 2026 21:52:06 +0100 Subject: [PATCH] fix(attestation): reject oversized challenges and rewrite cert DER encoding DuckDetector flagged two issues: 1. Oversized challenge accepted, 256-byte attestation challenge should return INVALID_INPUT_LENGTH (-21) like real KeyMint. Added early check in handleGenerateKey before any path decision. 2. Issuer/subject chain mismatch, rcgen's HashMap loses DN attribute ordering and converts PrintableString to UTF8String, producing different DER bytes. Replaced rcgen with manual DER assembly that injects raw keybox issuer_dn_der bytes directly. Verified on device: TX_ID 315 rejects 256-byte challenge, TX_ID 501 generates valid 4-cert chain with correct issuer linkage. --- .../interception/keystore/InterceptorUtils.kt | 11 + .../shim/KeyMintSecurityLevelInterceptor.kt | 9 + native-certgen/Cargo.lock | 1166 +++++++++++++++++ native-certgen/Cargo.toml | 1 - native-certgen/src/certbuilder.rs | 642 ++++++--- native-certgen/src/error.rs | 7 - 6 files changed, 1649 insertions(+), 187 deletions(-) create mode 100644 native-certgen/Cargo.lock diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index 7a527cd..d187f1e 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -12,6 +12,17 @@ data class KeyIdentifier(val uid: Int, val alias: String) /** A collection of utility functions to support binder interception. */ object InterceptorUtils { + private const val EX_SERVICE_SPECIFIC = -8 + + fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = Parcel.obtain().apply { + writeInt(EX_SERVICE_SPECIFIC) + writeInt(errorCode) + writeString(null) + } + return BinderInterceptor.TransactionResult.OverrideReply(parcel) + } + /** * Uses reflection to get the integer transaction code for a given method name from a Stub * class. This is necessary for older Android versions where codes are not public constants. diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index ed07d64..30fe8e6 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -18,6 +18,7 @@ import java.security.spec.PKCS8EncodedKeySpec import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import org.matrix.TEESimulator.attestation.AttestationBuilder +import org.matrix.TEESimulator.attestation.AttestationConstants import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -246,6 +247,13 @@ class KeyMintSecurityLevelInterceptor( ) val params = data.createTypedArray(KeyParameter.CREATOR)!! val parsedParams = KeyMintAttestation(params) + + val challenge = parsedParams.attestationChallenge + if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) { + SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})") + return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH) + } + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val isAttestKeyRequest = parsedParams.isAttestKey() @@ -506,6 +514,7 @@ class KeyMintSecurityLevelInterceptor( // Maximum alias length to prevent binder buffer exhaustion (Issue #109) // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead private const val MAX_ALIAS_LENGTH = 256 * 1024 + private const val KEYMINT_INVALID_INPUT_LENGTH = -21 private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2 // Sliding window: max hardware keygen permits per UID within the burst window private const val MAX_HW_KEYGEN_PER_WINDOW = 2 diff --git a/native-certgen/Cargo.lock b/native-certgen/Cargo.lock new file mode 100644 index 0000000..e256cb3 --- /dev/null +++ b/native-certgen/Cargo.lock @@ -0,0 +1,1166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "certgen" +version = "0.1.0" +dependencies = [ + "anyhow", + "const-oid", + "der", + "jni", + "libc", + "pkcs8", + "rand", + "ring", + "rsa", + "serde_json", + "time", + "tracing", + "tracing-subscriber", + "x509-cert", + "zip", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "zerocopy" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/native-certgen/Cargo.toml b/native-certgen/Cargo.toml index 619e515..484efb1 100644 --- a/native-certgen/Cargo.toml +++ b/native-certgen/Cargo.toml @@ -13,7 +13,6 @@ ring = "0.17.14" rsa = { version = "0.9", features = ["sha2"] } pkcs8 = { version = "0.10", features = ["alloc"] } rand = "0.8" -rcgen = { version = "0.13.2", default-features = false, features = ["ring"] } der = { version = "0.7.10", features = ["alloc", "oid"] } const-oid = "0.9.6" x509-cert = { version = "0.2.5", features = ["pem"] } diff --git a/native-certgen/src/certbuilder.rs b/native-certgen/src/certbuilder.rs index 4027fec..77d87a8 100644 --- a/native-certgen/src/certbuilder.rs +++ b/native-certgen/src/certbuilder.rs @@ -1,37 +1,29 @@ use crate::error::{CertGenError, Result}; use crate::keybox::ParsedKeybox; -use crate::types::{CertGenParams, GeneratedKeyPair}; +use crate::types::{Algorithm, 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]; +// Signature algorithm OIDs +const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2]; +const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3]; +const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11]; + +// Extension OIDs +const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15]; + 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 leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?; let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len()); - chain.push(leaf_cert.der().to_vec()); + chain.push(leaf_der); for cert_der in &keybox.cert_chain_ders { chain.push(cert_der.clone()); } @@ -39,201 +31,493 @@ pub fn build_certificate_chain( 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( +fn build_leaf_cert( + key_pair: &GeneratedKeyPair, 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 - }; +) -> Result> { + let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?; + let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?; // Serial number - cp.serial_number = if let Some(ref serial_bytes) = params.cert_serial { - Some(SerialNumber::from(serial_bytes.clone())) + let serial_bytes = if let Some(ref serial) = params.cert_serial { + serial.clone() } else { - Some(SerialNumber::from(vec![1u8])) + 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 + // Subject DN + let subject_dn_der = if let Some(ref subject) = params.cert_subject { + subject.clone() + } else { + encode_simple_cn_dn("Android KeyStore Key") + }; + + // Validity + let not_before = timestamp_to_datetime(params.cert_not_before)?; + let not_after = if params.cert_not_after == -1 { 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)? }; - // rcgen 0.13.2: IsCa::NoCa (the default) emits neither BasicConstraints nor SKI - // extension. This matches real Android attestation leaf certs which include neither. - // No explicit suppression needed — NoCa is a no-op in the extension writer. + // Extensions + let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?; - // KeyUsage from purposes - cp.key_usages = map_key_usages(¶ms.purposes); + // TBS Certificate + let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2])); + let serial_der = encode_der_integer(&serial_bytes); + let validity_der = encode_validity(¬_before, ¬_after); + let extensions_tagged = encode_der_explicit_tag(3, &extensions_der); - // 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); + let tbs_der = encode_der_sequence(&[ + &version_der, + &serial_der, + &sig_alg_der, + &keybox.issuer_dn_der, // RAW bytes — no re-encoding + &validity_der, + &subject_dn_der, + &spki_der, + &extensions_tagged, + ]); - Ok(cp) + // Sign the TBS + let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?; + let signature_bit_string = encode_der_bit_string(&signature_bytes); + + // Final certificate: SEQUENCE { TBS, sigAlgorithm, signature } + let cert_der = encode_der_sequence(&[ + &tbs_der, + &sig_alg_der, + &signature_bit_string, + ]); + + Ok(cert_der) } -/// Maps KeyPurpose values to X.509 KeyUsage bits per KeyCreationResult.aidl spec. -/// Only SIGN, DECRYPT, WRAP_KEY, AGREE_KEY, and ATTEST_KEY produce KeyUsage bits. -/// ENCRYPT and VERIFY are intentionally excluded (matches Kotlin CertificateGenerator). -fn map_key_usages(purposes: &[i32]) -> Vec { - let mut usages = Vec::new(); +fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result> { + match algorithm { + Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der), + Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der), + } +} + +fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result> { + // Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier + let alg = detect_ec_signing_algorithm(signing_key_der)?; + + let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new()) + .map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?; + + let rng = ring::rand::SystemRandom::new(); + let sig = key_pair.sign(&rng, tbs_der) + .map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?; + + Ok(sig.as_ref().to_vec()) +} + +fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> { + use der::Decode; + let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der) + .map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?; + + let params_oid = info.algorithm.parameters_oid() + .map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?; + + let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse() + .map_err(|_| CertGenError::SigningFailed("OID parse".into()))?; + let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse() + .map_err(|_| CertGenError::SigningFailed("OID parse".into()))?; + + if params_oid == p256_oid { + Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING) + } else if params_oid == p384_oid { + Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) + } else { + Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}"))) + } +} + +fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result> { + use rsa::pkcs8::DecodePrivateKey; + use rsa::signature::{SignatureEncoding, SignerMut}; + use rsa::pkcs1v15::SigningKey; + use rsa::sha2::Sha256; + + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der) + .map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?; + + let mut signing_key = SigningKey::::new(private_key); + let signature = signing_key.sign(tbs_der); + + Ok(signature.to_vec()) +} + +fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result> { + match algorithm { + Algorithm::Ec => { + let ring_alg = detect_ec_signing_algorithm(signing_key_der)?; + // Determine OID from the algorithm used + let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) { + OID_SHA384_WITH_ECDSA + } else { + OID_SHA256_WITH_ECDSA + }; + let oid_der = encode_der_oid(oid); + Ok(encode_der_sequence(&[&oid_der])) + } + Algorithm::Rsa => { + let oid_der = encode_der_oid(OID_SHA256_WITH_RSA); + let null_der = vec![0x05, 0x00]; + Ok(encode_der_sequence(&[&oid_der, &null_der])) + } + } +} + +fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result> { + use der::Decode; + + let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der) + .map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?; + + // Reconstruct SPKI from AlgorithmIdentifier + public key + // For EC: derive public key from private key via ring + // For RSA: derive from rsa crate + let alg_id_oid = info.algorithm.oid; + let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse() + .map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?; + + if alg_id_oid == ec_oid { + extract_ec_spki(pkcs8_der, &info) + } else { + extract_rsa_spki(pkcs8_der) + } +} + +fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result> { + use ring::signature::KeyPair as _; + let params_oid = info.algorithm.parameters_oid() + .map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?; + + let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse() + .map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?; + let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse() + .map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?; + + let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec) = if params_oid == p256_oid { + (&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7])) + } else if params_oid == p384_oid { + (&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34])) + } else { + return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}"))); + }; + + let kp = ring::signature::EcdsaKeyPair::from_pkcs8( + ring_alg, + pkcs8_der, + &ring::rand::SystemRandom::new(), + ).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?; + let ec_kp = kp.public_key().as_ref().to_vec(); + + // SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) } + // AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID } + let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]); + let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]); + let pub_key_bits = encode_der_bit_string(&ec_kp); + + Ok(encode_der_sequence(&[&alg_id, &pub_key_bits])) +} + +fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result> { + use rsa::pkcs8::DecodePrivateKey; + + let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der) + .map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?; + + let public_key = rsa::RsaPublicKey::from(&private_key); + + // Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER } + use rsa::traits::PublicKeyParts; + let n_bytes = public_key.n().to_bytes_be(); + let e_bytes = public_key.e().to_bytes_be(); + let rsa_pub_der = encode_der_sequence(&[ + &encode_der_integer(&n_bytes), + &encode_der_integer(&e_bytes), + ]); + + // SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) } + let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]); + let null_der = vec![0x05, 0x00]; + let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]); + let pub_key_bits = encode_der_bit_string(&rsa_pub_der); + + Ok(encode_der_sequence(&[&alg_id, &pub_key_bits])) +} + +fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result> { + let mut extensions: Vec> = Vec::new(); + + // KeyUsage extension (critical) + let ku_byte = map_key_usage_byte(purposes); + if ku_byte != 0 { + let ku_ext = build_key_usage_extension(ku_byte); + extensions.push(ku_ext); + } + + // Attestation extension (non-critical) + let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der); + extensions.push(attest_ext); + + Ok(encode_der_sequence_of(&extensions)) +} + +fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec { + let value_octet_string = encode_der_octet_string(value_der); + if critical { + let critical_der = encode_der_boolean(true); + encode_der_sequence(&[oid_der, &critical_der, &value_octet_string]) + } else { + encode_der_sequence(&[oid_der, &value_octet_string]) + } +} + +fn build_key_usage_extension(ku_byte: u8) -> Vec { + // DER BIT STRING: minimal encoding requires trimming trailing zero bits + let unused_bits = ku_byte.trailing_zeros().min(7) as u8; + + // BIT STRING = tag (0x03) + length(2) + unused_bits + byte + let bit_string = vec![0x03, 0x02, unused_bits, ku_byte]; + + let oid_der = encode_der_oid(OID_KEY_USAGE); + let value_octet_string = encode_der_octet_string(&bit_string); + let critical_der = encode_der_boolean(true); + + encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string]) +} + +// KeyUsage BIT STRING byte layout (RFC 5280): +// byte[0] bit 7 = digitalSignature (0x80) +// byte[0] bit 6 = nonRepudiation (0x40) +// byte[0] bit 5 = keyEncipherment (0x20) +// byte[0] bit 4 = dataEncipherment (0x10) +// byte[0] bit 3 = keyAgreement (0x08) +// byte[0] bit 2 = keyCertSign (0x04) +// byte[0] bit 1 = cRLSign (0x02) +// byte[0] bit 0 = encipherOnly (0x01) +// byte[1] bit 7 = decipherOnly (0x80) +fn map_key_usage_byte(purposes: &[i32]) -> u8 { + let mut bits: u8 = 0; for &purpose in purposes { match purpose { - 2 => { - // SIGN -> digitalSignature - if !usages.contains(&KeyUsagePurpose::DigitalSignature) { - usages.push(KeyUsagePurpose::DigitalSignature); - } - } - 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); - } - } + 2 => bits |= 0x80, // SIGN -> digitalSignature + 1 => bits |= 0x10, // DECRYPT -> dataEncipherment + 5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment + 6 => bits |= 0x08, // AGREE_KEY -> keyAgreement + 7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign _ => {} } } - usages + bits +} + +fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec { + let nb = encode_time(not_before); + let na = encode_time(not_after); + encode_der_sequence(&[&nb, &na]) +} + +fn encode_time(dt: &OffsetDateTime) -> Vec { + let year = dt.year(); + if (1950..2050).contains(&year) { + encode_utctime(dt) + } else { + encode_gentime(dt) + } +} + +fn encode_utctime(dt: &OffsetDateTime) -> Vec { + // UTCTime: YYMMDDHHMMSSZ + let year = dt.year() % 100; + let s = format!( + "{:02}{:02}{:02}{:02}{:02}{:02}Z", + year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second() + ); + let mut out = Vec::with_capacity(2 + s.len()); + out.push(0x17); // UTCTime tag + out.extend_from_slice(&encode_der_length_bytes(s.len())); + out.extend_from_slice(s.as_bytes()); + out +} + +fn encode_gentime(dt: &OffsetDateTime) -> Vec { + // GeneralizedTime: YYYYMMDDHHMMSSZ + let s = format!( + "{:04}{:02}{:02}{:02}{:02}{:02}Z", + dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second() + ); + let mut out = Vec::with_capacity(2 + s.len()); + out.push(0x18); // GeneralizedTime tag + out.extend_from_slice(&encode_der_length_bytes(s.len())); + out.extend_from_slice(s.as_bytes()); + out +} + +fn encode_simple_cn_dn(cn: &str) -> Vec { + // Name = SEQUENCE OF RelativeDistinguishedName + // RDN = SET OF AttributeTypeAndValue + // ATV = SEQUENCE { OID, UTF8String } + let cn_oid = encode_der_oid(&[2, 5, 4, 3]); + let cn_value = encode_der_utf8string(cn); + let atv = encode_der_sequence(&[&cn_oid, &cn_value]); + let rdn = encode_der_set(&[&atv]); + encode_der_sequence(&[&rdn]) } 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}; +// --------------------------------------------------------------------------- +// DER encoding primitives +// --------------------------------------------------------------------------- - 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) +fn encode_der_length_bytes(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 { - 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 | 0x1A => { - // UTF8String (0x0C), PrintableString (0x13), IA5String (0x16), VisibleString (0x1A) - String::from_utf8_lossy(content).into_owned() - } - 0x1E => { - // BMPString (UTF-16BE) - let chars: Vec = content - .chunks_exact(2) - .map(|c| u16::from_be_bytes([c[0], c[1]])) - .collect(); - String::from_utf16_lossy(&chars) - } - _ => String::from_utf8_lossy(content).into_owned(), + vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8] } } + +fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec { + let mut out = Vec::with_capacity(1 + 4 + content.len()); + out.push(tag); + out.extend_from_slice(&encode_der_length_bytes(content.len())); + out.extend_from_slice(content); + out +} + +fn encode_der_sequence(items: &[&[u8]]) -> Vec { + let total: usize = items.iter().map(|i| i.len()).sum(); + let mut content = Vec::with_capacity(total); + for item in items { + content.extend_from_slice(item); + } + encode_der_tag_length_value(0x30, &content) +} + +fn encode_der_sequence_of(items: &[Vec]) -> Vec { + let total: usize = items.iter().map(|i| i.len()).sum(); + let mut content = Vec::with_capacity(total); + for item in items { + content.extend_from_slice(item); + } + encode_der_tag_length_value(0x30, &content) +} + +fn encode_der_set(items: &[&[u8]]) -> Vec { + let total: usize = items.iter().map(|i| i.len()).sum(); + let mut content = Vec::with_capacity(total); + for item in items { + content.extend_from_slice(item); + } + encode_der_tag_length_value(0x31, &content) +} + +fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec { + encode_der_tag_length_value(0xA0 | tag_num, content) +} + +fn encode_der_integer(value: &[u8]) -> Vec { + // DER INTEGER must have minimal encoding and leading 0x00 if high bit set + if value.is_empty() { + return encode_der_tag_length_value(0x02, &[0x00]); + } + + // Strip leading zeros (but keep at least one byte) + let mut start = 0; + while start < value.len() - 1 && value[start] == 0 { + start += 1; + } + let trimmed = &value[start..]; + + // Add leading 0x00 if high bit is set (positive integer) + if trimmed[0] & 0x80 != 0 { + let mut padded = Vec::with_capacity(1 + trimmed.len()); + padded.push(0x00); + padded.extend_from_slice(trimmed); + encode_der_tag_length_value(0x02, &padded) + } else { + encode_der_tag_length_value(0x02, trimmed) + } +} + +fn encode_der_bit_string(bits: &[u8]) -> Vec { + // BIT STRING: tag 0x03, length, unused_bits (0), content + let mut content = Vec::with_capacity(1 + bits.len()); + content.push(0x00); // 0 unused bits + content.extend_from_slice(bits); + encode_der_tag_length_value(0x03, &content) +} + +fn encode_der_octet_string(content: &[u8]) -> Vec { + encode_der_tag_length_value(0x04, content) +} + +fn encode_der_utf8string(s: &str) -> Vec { + encode_der_tag_length_value(0x0C, s.as_bytes()) +} + +fn encode_der_boolean(val: bool) -> Vec { + encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }]) +} + +fn encode_der_oid(components: &[u64]) -> Vec { + if components.len() < 2 { + return encode_der_tag_length_value(0x06, &[]); + } + + let mut content = Vec::new(); + // First two components encoded as 40 * c[0] + c[1] + content.push((components[0] * 40 + components[1]) as u8); + + for &c in &components[2..] { + encode_oid_subidentifier(&mut content, c); + } + + encode_der_tag_length_value(0x06, &content) +} + +fn encode_oid_subidentifier(buf: &mut Vec, mut value: u64) { + if value == 0 { + buf.push(0); + return; + } + + // Encode in base-128 with continuation bits + let mut bytes = Vec::new(); + while value > 0 { + bytes.push((value & 0x7F) as u8); + value >>= 7; + } + bytes.reverse(); + + // Set high bit on all but the last byte + for i in 0..bytes.len() - 1 { + bytes[i] |= 0x80; + } + + buf.extend_from_slice(&bytes); +} diff --git a/native-certgen/src/error.rs b/native-certgen/src/error.rs index 74ab02d..d036a99 100644 --- a/native-certgen/src/error.rs +++ b/native-certgen/src/error.rs @@ -11,7 +11,6 @@ pub enum CertGenError { KeyboxParseFailed(String), AttestationBuildFailed(String), DerError(der::Error), - RcgenError(rcgen::Error), EmptyKeyboxChain, ChallengeTooLong(usize), InvalidParameter(String), @@ -31,7 +30,6 @@ impl fmt::Display for CertGenError { Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg), Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg), Self::DerError(e) => write!(f, "DER error: {}", e), - Self::RcgenError(e) => write!(f, "rcgen error: {}", e), Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"), Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len), Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg), @@ -73,10 +71,5 @@ impl From for CertGenError { } } -impl From for CertGenError { - fn from(e: rcgen::Error) -> Self { - Self::RcgenError(e) - } -} pub type Result = std::result::Result;