fix(keystore): gate synthetic grant to Android 16
KeyStoreManager.grantKeyAccess() became a public app API only in Android 16 (API 36). Before that grant was a hidden API and SELinux denied untrusted_app, so a real Android 15 device answers a private- binder grant() with PERMISSION_DENIED. The virtualized grant plane (5579b16) issued a synthetic grant on every SDK, exposing a capability a real Android 15 app does not have. Gate grant() and ungrant() on SDK_INT >= 36: pre-36 returns PERMISSION_DENIED for synthetic keys (matching the real device, which Duck then marks UNAVAILABLE rather than a tell); 36+ keeps the coherent virtualized grant. On-device 23106RN0DA (SDK 35): all four grant rows report UNAVAILABLE, not RED; tamper score unaffected. Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md
This commit is contained in:
+29
-12
@@ -62,6 +62,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
|
|
||||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||||
private const val RESPONSE_PERMISSION_DENIED = 6
|
private const val RESPONSE_PERMISSION_DENIED = 6
|
||||||
|
|
||||||
|
// KeyStoreManager.grantKeyAccess() became a public app API in Android 16 (API 36). Before that,
|
||||||
|
// grant was a hidden API and SELinux denied untrusted_app, so a synthetic-key grant must answer
|
||||||
|
// PERMISSION_DENIED pre-36 and a coherent virtualized grant on 36+.
|
||||||
|
private const val GRANT_PUBLIC_API_SDK = 36
|
||||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||||
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
|
||||||
@@ -191,25 +196,23 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
// A Domain.GRANT read is served for ANY grantee uid — including isolated
|
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
|
||||||
// services (bindIsolatedService) that have no package mapping, so
|
// including isolated services (bindIsolatedService) with no package mapping — so resolve
|
||||||
// shouldSkipUid would otherwise drop them to the real keystore2. Resolve it
|
// it before the package-scoped skip; caller-binding in resolveGrant() is the real access
|
||||||
// before the package-scoped skip: caller-binding in resolveGrant() is the
|
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is
|
||||||
// real access gate, mirroring keystore2 (a grant row is keyed on grantee+id,
|
// empty and this falls through to the real keystore2.
|
||||||
// independent of the caller's policy).
|
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
|
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
|
||||||
val grant =
|
val grant =
|
||||||
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
|
||||||
if (grant == null) {
|
if (grant == null) {
|
||||||
// Ours but wrong caller -> KEY_NOT_FOUND (#57 probe 4, caller-binding);
|
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2.
|
||||||
// not ours -> fall through to the real keystore2.
|
|
||||||
return if (
|
return if (
|
||||||
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
|
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
|
||||||
)
|
)
|
||||||
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||||
else TransactionResult.ContinueAndSkipPost
|
else TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
if ((grant.accessVector and 0x4) == 0) { // GET_INFO = 0x4 (#57 probe 3)
|
if ((grant.accessVector and 0x4) == 0) { // GET_INFO = 0x4 (access-vector gate)
|
||||||
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
}
|
}
|
||||||
val response =
|
val response =
|
||||||
@@ -283,8 +286,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
return InterceptorUtils.createTypedObjectReply(teeResp)
|
return InterceptorUtils.createTypedObjectReply(teeResp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Domain.GRANT is handled earlier (before the package-scoped skip),
|
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
|
||||||
// so an alias-less read that reaches here is KEY_ID or unknown.
|
// read reaching here is KEY_ID or unknown, so it falls through to the real keystore2.
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
@@ -314,10 +317,20 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
val granteeUid = data.readInt()
|
val granteeUid = data.readInt()
|
||||||
val accessVector = 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.
|
||||||
val ownerKeyId =
|
val ownerKeyId =
|
||||||
resolveOwnerKeyId(key, callingUid)
|
resolveOwnerKeyId(key, callingUid)
|
||||||
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
||||||
?: return TransactionResult.ContinueAndSkipPost // real key -> real keystore2
|
?: 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
|
||||||
|
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
|
||||||
|
// issue a coherent, caller-bound, access-vector-carrying grant whose Domain.GRANT read
|
||||||
|
// returns the owner's chain.
|
||||||
|
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
val grantId =
|
val grantId =
|
||||||
KeyMintSecurityLevelInterceptor.issueGrant(ownerKeyId, granteeUid, accessVector)
|
KeyMintSecurityLevelInterceptor.issueGrant(ownerKeyId, granteeUid, accessVector)
|
||||||
val reply =
|
val reply =
|
||||||
@@ -339,6 +352,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
resolveOwnerKeyId(key, callingUid)
|
resolveOwnerKeyId(key, callingUid)
|
||||||
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) }
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: 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) {
|
||||||
|
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
|
||||||
|
}
|
||||||
KeyMintSecurityLevelInterceptor.revokeGrant(ownerKeyId, granteeUid)
|
KeyMintSecurityLevelInterceptor.revokeGrant(ownerKeyId, granteeUid)
|
||||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+5
-3
@@ -1167,9 +1167,11 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grant plane (duck PR #38 / #57). A grant is caller-bound and carries an
|
* Grant plane for the public `KeyStoreManager.grantKeyAccess()` API (Android 16, API 36+).
|
||||||
* access vector; resolving one yields the owner's own KeyEntryResponse so
|
* On Android <= 15 grant was a hidden API denied to untrusted_app, so this state stays
|
||||||
* every access plane returns a coherent certificate chain.
|
* empty there (the GRANT_TRANSACTION handler returns PERMISSION_DENIED for synthetic keys
|
||||||
|
* pre-36). A grant is caller-bound and carries an access vector; resolving one yields the
|
||||||
|
* owner's own KeyEntryResponse so every access plane returns a coherent certificate chain.
|
||||||
*/
|
*/
|
||||||
data class SoftwareGrant(
|
data class SoftwareGrant(
|
||||||
val ownerKeyId: KeyIdentifier,
|
val ownerKeyId: KeyIdentifier,
|
||||||
|
|||||||
Reference in New Issue
Block a user