From 59a23573121162ebb271b0bf7babe612cec8d969 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 4 Jun 2026 19:07:42 +0100 Subject: [PATCH] fix(keystore): repair attestation generation gaps Two gaps in attestation generation surfaced by a tester's Key Attestation app runs on build 259. Device-ID attestation (IMEI/serial) via Shizuku arrives as a privileged UID (shell/system) absent from target.txt, so it was skipped and the real TEE rejected it with CANNOT_ATTEST_IDS (-66). Stop skipping requests that carry device-ID tags, and force the forge path for them (the real TEE cannot attest IDs, so there is no chain to patch). The permission gate still rejects ordinary apps, mirroring a real device. 'Use attest key' produced WRONG_PUBLIC_KEY_TYPE: a reused persistent attest key is designated by KEY_ID with a null alias, so the lookup missed and the leaf was silently re-rooted under the keybox, double-rooting the chain the caller assembles. Resolve the attest key by KEY_ID as well as alias, and refuse to emit a leaf rather than fall back to the keybox when a designated attest key cannot be resolved. --- .../shim/KeyMintSecurityLevelInterceptor.kt | 45 +++++++++++++++++-- .../TEESimulator/pki/CertificateGenerator.kt | 20 +++++++-- 2 files changed, 58 insertions(+), 7 deletions(-) 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 28ae35f..005dbaf 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 @@ -591,10 +591,16 @@ class KeyMintSecurityLevelInterceptor( ) } + // Device-ID attestation (IMEI/MEID/serial) is a factory-provisioned capability the + // real TEE frequently cannot satisfy (it returns CANNOT_ATTEST_IDS). A privileged + // caller — shell/system, e.g. the Key Attestation app via Shizuku — is entitled to + // request it, but arrives un-targeted, so don't skip it: it must reach the forge + // path below. The permission gate still rejects ordinary apps further down. if ( ConfigurationManager.shouldSkipUid(callingUid) && attestationKey == null && - !isAttestKeyRequest + !isAttestKeyRequest && + !hasDeviceIdAttestation ) { logProbe("SKIP") return TransactionResult.ContinueAndSkipPost @@ -702,11 +708,15 @@ class KeyMintSecurityLevelInterceptor( val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + // Device-ID attestation must be forged, not patched: the real TEE returns + // CANNOT_ATTEST_IDS, so there is no real chain to patch — only a synthetic one + // carrying the requested IDs and rooted under the keybox will satisfy the caller. val forceGenerate = oversized || ConfigurationManager.shouldGenerate(callingUid) || isAttestKeyRequest || - attestationKey != null + attestationKey != null || + hasDeviceIdAttestation SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" @@ -858,6 +868,19 @@ class KeyMintSecurityLevelInterceptor( return InterceptorUtils.createTypedObjectReply(metadata, diagnosticTag = "gen-mode-sym") } + // The framework can designate the attest key by alias OR — for a persistent key the caller + // reuses across sessions — by KEY_ID with a null alias. Resolve both: the leaf must be + // signed by the attest key the caller chains to via getCertChain, never silently re-rooted + // under the keybox (which double-roots the assembled chain and fails verification). + val attestKeyAlias: String? = + attestationKey?.let { it.alias ?: findGeneratedAliasByKeyId(callingUid, it.nspace) } + if (attestationKey != null && attestKeyAlias == null) { + throw android.os.ServiceSpecificException( + KEYMINT_INVALID_ARGUMENT, + "Designated attest key not resolvable (nspace=${attestationKey.nspace}) for uid $callingUid", + ) + } + var forgePath = "FORGE-bouncycastle" val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { @@ -867,7 +890,7 @@ class KeyMintSecurityLevelInterceptor( ?: CertificateGenerator.generateAttestedKeyPair( callingUid, keyDescriptor.alias, - attestationKey?.alias, + attestKeyAlias, parsedParams, securityLevel, ) @@ -875,7 +898,7 @@ class KeyMintSecurityLevelInterceptor( CertificateGenerator.generateAttestedKeyPair( callingUid, keyDescriptor.alias, - attestationKey?.alias, + attestKeyAlias, parsedParams, securityLevel, ) @@ -1559,6 +1582,20 @@ class KeyMintSecurityLevelInterceptor( ?.value } + /** + * Resolves the alias of a generated key addressed by KEY_ID. The framework hands a reused + * (persistent) attest key to generateKey as a KEY_ID descriptor with a null alias; this + * maps it back to the alias our cache is keyed by, so the leaf is signed by the attest key + * the caller will chain to rather than silently re-rooted under the keybox. + */ + fun findGeneratedAliasByKeyId(callingUid: Int, nspace: Long?): String? { + if (nspace == null || nspace == 0L) return null + return generatedKeys.entries + .firstOrNull { (keyId, info) -> keyId.uid == callingUid && info.nspace == nspace } + ?.key + ?.alias + } + fun findTeeResponseByKeyId(callingUid: Int, nspace: Long?): KeyEntryResponse? { if (nspace == null || nspace == 0L) return null return teeResponses.entries diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt index 488b69a..16bbe8f 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -103,10 +103,24 @@ object CertificateGenerator { val keybox = getKeyboxForAlgorithm(uid, params.algorithm) + val wantsAttestKey = + attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val attestKeyInfo = - if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - getAttestationKeyInfo(uid, attestKeyAlias) - } else null + if (wantsAttestKey) getAttestationKeyInfo(uid, attestKeyAlias) else null + + // When the caller designates an attest key, the leaf MUST be signed by it and returned + // alone (the caller appends the attest key's own chain). Re-rooting under the keybox + // here instead yields a self-rooted leaf that, concatenated with the attest key chain, + // double-roots and fails verification (WRONG_PUBLIC_KEY_TYPE). Refuse rather than emit + // a + // broken chain. + if (wantsAttestKey && attestKeyInfo == null) { + SystemLogger.error( + "Designated attest key '$attestKeyAlias' not found for uid $uid; refusing to " + + "emit a keybox-rooted leaf that would break the caller's chain." + ) + return null + } val (signingKey, issuer) = attestKeyInfo?.let { it.first to it.second }