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.
This commit is contained in:
Enginex0
2026-06-04 19:07:42 +01:00
parent 7f33bd737d
commit 59a2357312
2 changed files with 58 additions and 7 deletions
@@ -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
@@ -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 }