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:
+41
-4
@@ -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 (
|
if (
|
||||||
ConfigurationManager.shouldSkipUid(callingUid) &&
|
ConfigurationManager.shouldSkipUid(callingUid) &&
|
||||||
attestationKey == null &&
|
attestationKey == null &&
|
||||||
!isAttestKeyRequest
|
!isAttestKeyRequest &&
|
||||||
|
!hasDeviceIdAttestation
|
||||||
) {
|
) {
|
||||||
logProbe("SKIP")
|
logProbe("SKIP")
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
@@ -702,11 +708,15 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
|
|
||||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
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 =
|
val forceGenerate =
|
||||||
oversized ||
|
oversized ||
|
||||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||||
isAttestKeyRequest ||
|
isAttestKeyRequest ||
|
||||||
attestationKey != null
|
attestationKey != null ||
|
||||||
|
hasDeviceIdAttestation
|
||||||
|
|
||||||
SystemLogger.trace {
|
SystemLogger.trace {
|
||||||
"[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest"
|
"[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")
|
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"
|
var forgePath = "FORGE-bouncycastle"
|
||||||
val keyData =
|
val keyData =
|
||||||
if (NativeCertGen.isAvailable && attestationKey == null) {
|
if (NativeCertGen.isAvailable && attestationKey == null) {
|
||||||
@@ -867,7 +890,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
?: CertificateGenerator.generateAttestedKeyPair(
|
?: CertificateGenerator.generateAttestedKeyPair(
|
||||||
callingUid,
|
callingUid,
|
||||||
keyDescriptor.alias,
|
keyDescriptor.alias,
|
||||||
attestationKey?.alias,
|
attestKeyAlias,
|
||||||
parsedParams,
|
parsedParams,
|
||||||
securityLevel,
|
securityLevel,
|
||||||
)
|
)
|
||||||
@@ -875,7 +898,7 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
CertificateGenerator.generateAttestedKeyPair(
|
CertificateGenerator.generateAttestedKeyPair(
|
||||||
callingUid,
|
callingUid,
|
||||||
keyDescriptor.alias,
|
keyDescriptor.alias,
|
||||||
attestationKey?.alias,
|
attestKeyAlias,
|
||||||
parsedParams,
|
parsedParams,
|
||||||
securityLevel,
|
securityLevel,
|
||||||
)
|
)
|
||||||
@@ -1559,6 +1582,20 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
?.value
|
?.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? {
|
fun findTeeResponseByKeyId(callingUid: Int, nspace: Long?): KeyEntryResponse? {
|
||||||
if (nspace == null || nspace == 0L) return null
|
if (nspace == null || nspace == 0L) return null
|
||||||
return teeResponses.entries
|
return teeResponses.entries
|
||||||
|
|||||||
@@ -103,10 +103,24 @@ object CertificateGenerator {
|
|||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
|
val wantsAttestKey =
|
||||||
|
attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
val attestKeyInfo =
|
val attestKeyInfo =
|
||||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (wantsAttestKey) getAttestationKeyInfo(uid, attestKeyAlias) else null
|
||||||
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) =
|
val (signingKey, issuer) =
|
||||||
attestKeyInfo?.let { it.first to it.second }
|
attestKeyInfo?.let { it.first to it.second }
|
||||||
|
|||||||
Reference in New Issue
Block a user