Compare commits

...
4 Commits
Author SHA1 Message Date
Enginex0 90ff59e0aa fix(interception): check generatedKeys before deletedSoftwareKeys on getKeyEntry
The deletion guard must not shadow re-generated keys. If an app
deletes a key then re-creates it, getKeyEntry was still returning
KEY_NOT_FOUND because deletedSoftwareKeys was checked first.
2026-03-16 22:36:02 +01:00
Enginex0 8bdf0d59fa docs(release): bump to v4.5 with detection hardening changelog 2026-03-16 22:07:56 +01:00
Enginex0 6ab09f4889 fix(interception): prevent ghost key responses after software key deletion
After deleting a software-generated key, getKeyEntry was falling
through to the real keystore2 service which could return a stale
hardware key with the same alias. The post-transact live-patch
fallback would then resurrect the key with a patched chain —
detectors flag this as binder inconsistency.

Track deleted software key aliases and return KEY_NOT_FOUND (7) for
subsequent getKeyEntry calls. Also always invoke cleanupKeyData on
delete to clear stale patchedChains entries for hardware keys.
2026-03-16 22:06:53 +01:00
Enginex0 f4559bcd19 perf(keygen): normalize software generateKey RTT to match TEE latency
Software-generated keys complete in ~4ms, real TEE averages 55-65ms
with a floor around 15ms. Detectors measure this RTT to distinguish
software from hardware paths. Gaussian delay sampling (mean=55ms,
σ=12ms, floor=15ms) brings total RTT into the expected range.
2026-03-16 22:06:38 +01:00
6 changed files with 44 additions and 10 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v4.4"
val verName = "v4.5"
android {
namespace = "org.matrix.TEESimulator"
@@ -89,5 +89,5 @@ object AttestationConstants {
// --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
const val CHALLENGE_LENGTH_LIMIT = 128
}
@@ -10,6 +10,7 @@ import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -54,6 +55,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
private const val RESPONSE_KEY_NOT_FOUND = 7
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
override val serviceName = "android.system.keystore2.IKeystoreService/default"
override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
@@ -156,8 +160,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) {
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
if (wasSoftwareKey) {
deletedSoftwareKeys.add(keyId)
SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
)
@@ -166,9 +172,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.ContinueAndSkipPost
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response == null) {
if (deletedSoftwareKeys.remove(keyId)) {
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
}
return TransactionResult.Continue
}
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key")
@@ -317,6 +317,7 @@ class KeyMintSecurityLevelInterceptor(
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
val startNs = System.nanoTime()
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
@@ -350,6 +351,10 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey = isAttestKeyRequest,
)
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = sampleTeeLatencyMs() - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
@@ -542,6 +547,9 @@ class KeyMintSecurityLevelInterceptor(
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
private const val BURST_WINDOW_MS = 30_000L
private const val TEE_LATENCY_MEAN_MS = 55.0
private const val TEE_LATENCY_STDDEV_MS = 12.0
private const val TEE_LATENCY_FLOOR_MS = 15L
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
@@ -570,6 +578,11 @@ class KeyMintSecurityLevelInterceptor(
}
}
private fun sampleTeeLatencyMs(): Long {
val sample = TEE_LATENCY_MEAN_MS + secureRandom.nextGaussian() * TEE_LATENCY_STDDEV_MS
return sample.toLong().coerceAtLeast(TEE_LATENCY_FLOOR_MS)
}
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION =
+10
View File
@@ -1,3 +1,13 @@
## TEESimulator v4.5: Detection Hardening
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
---
## TEESimulator v4.4: AOSP Conformance
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
+3 -3
View File
@@ -1,6 +1,6 @@
{
"version": "v4.4",
"versionCode": 109,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.4/TEESimulator-v4.4-Release.zip",
"version": "v4.5",
"versionCode": 111,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
}