From ab58b11e4df41a7fbbb744313ff2251084ce208d Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Sat, 30 May 2026 03:11:20 +0100 Subject: [PATCH] fix(keystore): evict stale chains on key mutation Two synthetic-cache staleness gaps let getKeyEntry replay a pre-mutation attestation: - importKey now drops teeResponses and patchedChains for the alias, not only generatedKeys. A successful import replaces the real key, so the retained patched chain was a tell (duck STALE_GENERATED_AFTER_IMPORT). - After updateSubcomponent re-keys a patched chain, getKeyEntry on a patch-mode key evicts the cached TEE response by KEY_ID or APP so the read falls through to the updated real keystore2 (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). Refs Phase 9 .omc/plans/tee-fingerprint-phase-9-grant-plane-coherence.md --- .../keystore/Keystore2Interceptor.kt | 12 +++++++ .../shim/KeyMintSecurityLevelInterceptor.kt | 31 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index 0eb867c..94d0504 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -635,6 +635,18 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } if (generatedKeyInfo == null) { + // Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies + // the update, so drop our stale cached chain. Otherwise getKeyEntry replays the + // pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). + when (descriptor.domain) { + Domain.KEY_ID -> + KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace) + Domain.APP -> + descriptor.alias?.let { + KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it)) + } + else -> {} + } descriptor.alias?.let { val kid = KeyIdentifier(callingUid, it) userUpdatedKeys.add(kid) 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 318a69c..bb74d6d 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 @@ -126,13 +126,18 @@ class KeyMintSecurityLevelInterceptor( val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.SkipTransaction - // Evict generated key data but retain patched chains so detectors - // can't use importKey to force unpatched getKeyEntry responses. + // A successful importKey replaces the alias's key in the real keystore2, so any prior + // generate/patch cache for this alias is stale. Drop it: a non-attested import then + // falls through to the real keystore2 (origin=IMPORTED, imported leaf), and the + // attested-import branch below re-caches the fresh patched chain. Without this, + // getKeyEntry replays the prior generated attestation (duck STALE_GENERATED_AFTER_IMPORT). val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) if (generatedKeys.remove(keyId) != null) { SystemLogger.debug("Remove generated key on importKey $keyId") GeneratedKeyPersistence.delete(keyId) } + teeResponses.remove(keyId) + patchedChains.remove(keyId) attestationKeys.remove(keyId) importedKeys.add(keyId) SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" } @@ -1232,12 +1237,32 @@ class KeyMintSecurityLevelInterceptor( ?.value } + /** + * Drops the cached TEE/patched response (and patched chain) addressed by KEY_ID so a + * post-mutation getKeyEntry falls through to the now-updated real keystore2 key. Used + * after updateSubcomponent re-keys a patched chain (duck + * STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). + */ + fun evictTeeResponseByKeyId(callingUid: Int, nspace: Long?) { + if (nspace == null || nspace == 0L) return + teeResponses.entries + .filter { (keyId, _) -> keyId.uid == callingUid } + .find { (_, response) -> response.metadata?.key?.nspace == nspace } + ?.let { evictTeeResponse(it.key) } + } + + /** Alias-addressed counterpart of [evictTeeResponseByKeyId]. */ + fun evictTeeResponse(keyId: KeyIdentifier) { + teeResponses.remove(keyId) + patchedChains.remove(keyId) + } + fun getPatchedChain(keyId: KeyIdentifier): Array? = patchedChains[keyId] fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId) fun cleanupKeyData(keyId: KeyIdentifier) { - purgeGrantsForKey(keyId) // grants die with the key (re-key orphans them too) + purgeGrantsForKey(keyId) // grants die with the key (Android 16 path; no-op pre-36) if (generatedKeys.remove(keyId) != null) { SystemLogger.debug("Remove generated key ${keyId}") GeneratedKeyPersistence.delete(keyId)