fix(keystore): grant plane serves patch-mode keys
Domain.GRANT readback only recognized synthetic keys (generatedKeys), so patch-mode keys (real TEE key whose attestation we patch on read, cached in teeResponses) fell through to the real keystore2 unpatched. Android 16 made KeyStoreManager.grantKeyAccess a public API, so the owner read returned our patched chain while the grant read returned the raw real chain -> duck SELF_/ISOLATED_CHAIN_SPLIT. Gate grant()/ungrant()/resolveGrant on ownsKeyResponse() (synthetic OR patch-mode) so every access plane serves the same cached KeyEntryResponse. Pre-36 still answers PERMISSION_DENIED; no behavior change on Android 15.
This commit is contained in:
+6
-4
@@ -339,11 +339,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
val granteeUid = data.readInt()
|
||||
val accessVector = data.readInt()
|
||||
// Only synthetic keys are ours; real keys fall through to the real keystore2, which
|
||||
// applies the same SELinux gate the platform would.
|
||||
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
|
||||
// coherently so the Domain.GRANT readback returns the same chain the owner read returns.
|
||||
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
|
||||
// gate the platform would.
|
||||
val ownerKeyId =
|
||||
resolveOwnerKeyId(key, callingUid)
|
||||
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
||||
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
|
||||
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
|
||||
@@ -372,7 +374,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val granteeUid = data.readInt()
|
||||
val ownerKeyId =
|
||||
resolveOwnerKeyId(key, callingUid)
|
||||
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
||||
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
|
||||
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
||||
|
||||
+11
-1
@@ -1220,9 +1220,19 @@ class KeyMintSecurityLevelInterceptor(
|
||||
/** Caller-bound resolve: only the designated grantee, only while the key exists. */
|
||||
fun resolveGrant(grantId: Long, callerUid: Int): SoftwareGrant? =
|
||||
softwareGrants[grantId]?.takeIf {
|
||||
it.granteeUid == callerUid && generatedKeys.containsKey(it.ownerKeyId)
|
||||
it.granteeUid == callerUid && ownsKeyResponse(it.ownerKeyId)
|
||||
}
|
||||
|
||||
/**
|
||||
* True when this interceptor holds a coherent [KeyEntryResponse] for [keyId] — synthetic
|
||||
* (`generatedKeys`) OR patch-mode (`teeResponses`, a real TEE key whose attestation we
|
||||
* patched). The grant plane must virtualize both: gating on `generatedKeys` alone left
|
||||
* patch-mode keys' `Domain.GRANT` readback falling through to the real keystore2 unpatched,
|
||||
* splitting the grant chain against the owner's patched read (duck SELF_/ISOLATED_CHAIN_SPLIT,
|
||||
* surfaced once Android 16 made KeyStoreManager.grantKeyAccess a public API).
|
||||
*/
|
||||
fun ownsKeyResponse(keyId: KeyIdentifier): Boolean = getGeneratedKeyResponse(keyId) != null
|
||||
|
||||
fun revokeGrant(ownerKeyId: KeyIdentifier, granteeUid: Int) {
|
||||
softwareGrants.entries
|
||||
.filter { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid }
|
||||
|
||||
Reference in New Issue
Block a user