From b3aa7950c5c9e5675cb96ff3ac7474167c14cb52 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Fri, 20 Mar 2026 04:44:52 +0100 Subject: [PATCH] feat(interception): add AUTO mode TEE race for G10 attestation consistency AUTO mode now races TEE hardware against software generation via CompletableFuture. If TEE succeeds, the cert chain is patched and cached in teeResponses before returning, making attestation stress-resilient. If TEE fails, software fallback is used. ConfigurationManager no longer resolves AUTO at config time; it passes Mode.AUTO through to KeyMintSecurityLevelInterceptor for runtime dispatch. shouldPatch() returns true for both PATCH and AUTO modes. TEE status file persistence removed entirely. Aligns handleGenerateKey with upstream PR #157 three-way dispatch: forceGenerate, raceTeePatch, or hardware forwarding with post-patch. Hardware keygen rate limiting removed (replaced by raceTeePatch for AUTO, plain Continue for PATCH). Attest key override in Keystore2Interceptor now patches authorizations and uses null-safe nspace assignment. --- .../config/ConfigurationManager.kt | 48 ++---- .../keystore/Keystore2Interceptor.kt | 13 +- .../shim/KeyMintSecurityLevelInterceptor.kt | 151 +++++++++++------- 3 files changed, 108 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt index 4d4b8a5..77b698c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -7,7 +7,6 @@ import android.os.IBinder import android.os.ServiceManager import java.io.File import java.util.concurrent.ConcurrentHashMap -import org.matrix.TEESimulator.attestation.DeviceAttestationService import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.pki.KeyBoxManager @@ -31,7 +30,6 @@ object ConfigurationManager { // --- Configuration Paths --- const val CONFIG_PATH = "/data/adb/tricky_store" private const val TARGET_PACKAGES_FILE = "target.txt" - private const val TEE_STATUS_FILE = "tee_status.txt" private const val PATCH_LEVEL_FILE = "security_patch.txt" private const val DEFAULT_KEYBOX_FILE = "keybox.xml" private val configRoot = File(CONFIG_PATH) @@ -39,7 +37,6 @@ object ConfigurationManager { // --- In-Memory Configuration State --- @Volatile private var packageModes = mapOf() @Volatile private var packageKeyboxes = mapOf() - @Volatile private var isTeeBroken: Boolean? = null @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null @Volatile private var packagePatchLevels = mapOf() @@ -68,8 +65,6 @@ object ConfigurationManager { // Initial load of all configuration files. loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) - storeTeeStatus() // Check and store the current TEE status. - // Start watching for any subsequent file changes. ConfigObserver.startWatching() SystemLogger.info("Configuration initialized and file observer started.") @@ -87,33 +82,31 @@ object ConfigurationManager { return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE } - /** Determines if the certificate for a given UID needs to be patched. */ - fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH + fun shouldPatch(uid: Int): Boolean { + val mode = getPackageModeForUid(uid) + return mode == Mode.PATCH || mode == Mode.AUTO + } /** Determines if a new certificate needs to be generated for a given UID. */ fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE - /** Determines if no operation is needed for a given UID. */ fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null - /** Resolves the operating mode for a given UID based on its packages and the TEE status. */ + fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO + private fun getPackageModeForUid(uid: Int): Mode? { val packages = getPackagesForUid(uid) if (packages.isEmpty()) return null - // Lazily load TEE status if it hasn't been checked yet. - if (isTeeBroken == null) loadTeeStatus() - - // Find the first configured mode for any of the UID's packages. for (pkg in packages) { when (packageModes[pkg]) { Mode.GENERATE -> return Mode.GENERATE Mode.PATCH -> return Mode.PATCH - Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH - null -> continue // No config for this package, check the next one. + Mode.AUTO -> return Mode.AUTO + null -> continue } } - return null // No configuration found for this UID. + return null } /** @@ -280,29 +273,6 @@ object ConfigurationManager { } } - /** Checks the device's TEE status and writes the result to a file for persistence. */ - private fun storeTeeStatus() { - val statusFile = File(configRoot, TEE_STATUS_FILE) - isTeeBroken = !DeviceAttestationService.isTeeFunctional - try { - statusFile.writeText("tee_broken=$isTeeBroken") - SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken") - } catch (e: Exception) { - SystemLogger.error("Failed to write TEE status to file.", e) - } - } - - /** Loads the TEE status from the file. */ - private fun loadTeeStatus() { - val statusFile = File(configRoot, TEE_STATUS_FILE) - isTeeBroken = - if (statusFile.exists()) { - statusFile.readText().trim() == "tee_broken=true" - } else { - null // Status is unknown. - } - } - /** * A FileObserver that monitors the configuration directory for changes and triggers reloads of * the relevant settings. 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 4bf9c9e..1e61f24 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 @@ -358,14 +358,19 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { keyData.second.toTypedArray(), ) .getOrThrow() + response.metadata.authorizations = + InterceptorUtils.patchAuthorizations( + response.metadata.authorizations, + callingUid, + ) - keyDescriptor.nspace = SecureRandom().nextLong() - response.metadata.key.nspace = keyDescriptor.nspace + val newNspace = SecureRandom().nextLong() + response.metadata.key?.let { it.nspace = newNspace } KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( keyData.first, null, - keyDescriptor.nspace, + newNspace, response, parsedParameters, ) @@ -374,7 +379,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { GeneratedKeyPersistence.save( keyId = keyId, keyPair = keyData.first, - nspace = keyDescriptor.nspace, + nspace = newNspace, securityLevel = response.metadata.keySecurityLevel, certChain = keyData.second, algorithm = parsedParameters.algorithm, 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 57209f4..e198d9c 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 @@ -20,6 +20,7 @@ import java.security.SecureRandom import java.security.cert.Certificate import java.security.cert.CertificateFactory import java.security.spec.PKCS8EncodedKeySpec +import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.Executors @@ -116,12 +117,6 @@ class KeyMintSecurityLevelInterceptor( reply: Parcel?, resultCode: Int, ): TransactionResult { - if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) { - val remaining = hardwareKeygenCount(callingUid).decrementAndGet() - SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}") - } - - // We only care about successful transactions. if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) return TransactionResult.SkipTransaction @@ -468,38 +463,23 @@ class KeyMintSecurityLevelInterceptor( val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val isAttestKeyRequest = parsedParams.isAttestKey() - val needsSoftwareGeneration = + val forceGenerate = ConfigurationManager.shouldGenerate(callingUid) || (ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || (attestationKey != null && isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) - if (needsSoftwareGeneration) { - return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) - } else if (parsedParams.attestationChallenge != null) { - val windowUsed = hardwareKeygenWindowCount(callingUid) - val concurrentUsed = hardwareKeygenCount(callingUid).get() + val isAuto = ConfigurationManager.isAutoMode(callingUid) - // Sliding window rate limit - if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) { - SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback") - return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) + when { + forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) + isAuto && !teeFunctional -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest) + parsedParams.attestationChallenge != null -> TransactionResult.Continue + else -> { + cleanupKeyData(keyId) + TransactionResult.ContinueAndSkipPost } - // Concurrent cap - if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) { - hardwareKeygenCount(callingUid).decrementAndGet() - SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback") - return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) - } - // Both checks passed — commit the window permit and forward to hardware TEE - recordHardwareKeygen(callingUid) - hardwareKeygenTxIds.add(txId) - SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE") - return TransactionResult.Continue } - - cleanupKeyData(keyId) - TransactionResult.ContinueAndSkipPost } .getOrElse { SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) @@ -608,6 +588,85 @@ class KeyMintSecurityLevelInterceptor( return InterceptorUtils.createTypedObjectReply(response.metadata) } + private fun raceTeePatch( + callingUid: Int, + keyDescriptor: KeyDescriptor, + attestationKey: KeyDescriptor?, + rawParams: Array, + parsedParams: KeyMintAttestation, + keyId: KeyIdentifier, + isAttestKeyRequest: Boolean, + ): TransactionResult { + SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}") + + val teeDescriptor = KeyDescriptor().apply { + domain = keyDescriptor.domain + nspace = keyDescriptor.nspace + alias = keyDescriptor.alias + blob = keyDescriptor.blob + } + val teeAttestKey = attestationKey?.let { + KeyDescriptor().apply { + domain = it.domain + nspace = it.nspace + alias = it.alias + blob = it.blob + } + } + + val threadA = CompletableFuture.supplyAsync { + original.generateKey(teeDescriptor, teeAttestKey, rawParams, 0, byteArrayOf()) + } + + val swDescriptor = KeyDescriptor().apply { + domain = keyDescriptor.domain + nspace = secureRandom.nextLong() + alias = keyDescriptor.alias + blob = keyDescriptor.blob + } + val swKeyId = KeyIdentifier(callingUid, keyDescriptor.alias) + + val threadB = CompletableFuture.supplyAsync { + doSoftwareKeyGen(callingUid, swDescriptor, attestationKey, parsedParams, swKeyId, isAttestKeyRequest) + } + + return try { + val teeMetadata = threadA.join() + threadB.cancel(true) + teeFunctional = true + SystemLogger.info("AUTO: TEE succeeded for ${keyDescriptor.alias}, marked functional.") + + val originalChain = CertificateHelper.getCertificateChain(teeMetadata) + if (originalChain != null && originalChain.size > 1) { + val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) + CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow() + teeMetadata.authorizations = + InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid) + cleanupKeyData(keyId) + patchedChains[keyId] = newChain + } + + teeResponses[keyId] = KeyEntryResponse().apply { + this.metadata = teeMetadata + iSecurityLevel = original + } + + InterceptorUtils.createTypedObjectReply(teeMetadata) + } catch (_: Exception) { + SystemLogger.info("AUTO: TEE failed for ${keyDescriptor.alias}, using software result.") + try { + threadB.join() + } catch (e: Exception) { + SystemLogger.error("AUTO: both paths failed for ${keyDescriptor.alias}.", e) + val code = + if (e.cause is android.os.ServiceSpecificException) + (e.cause as android.os.ServiceSpecificException).errorCode + else SECURE_HW_COMMUNICATION_FAILED + InterceptorUtils.createServiceSpecificErrorReply(code) + } + } + } + private fun generateAttestedKeyPairNative( callingUid: Int, params: KeyMintAttestation, @@ -811,6 +870,7 @@ class KeyMintSecurityLevelInterceptor( companion object { private val secureRandom = SecureRandom() + @Volatile var teeFunctional = false // Maximum alias length to prevent binder buffer exhaustion (Issue #109) // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead @@ -830,43 +890,12 @@ class KeyMintSecurityLevelInterceptor( private const val MAX_CONCURRENT_OPS_PER_UID = 15 private const val STRONGBOX_MAX_CONCURRENT_OPS = 4 private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s - private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2 - // 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 val uidHardwareKeygenCount = ConcurrentHashMap() - private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet() - private val uidKeygenTimestamps = ConcurrentHashMap>() - private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) { Algorithm.RSA -> params.keySize <= 2048 Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256 else -> true } - private fun hardwareKeygenCount(uid: Int): AtomicInteger = - uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) } - - private fun hardwareKeygenWindowCount(uid: Int): Int { - val now = System.currentTimeMillis() - val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() } - synchronized(timestamps) { - timestamps.removeAll { now - it > BURST_WINDOW_MS } - if (timestamps.isEmpty()) { - uidKeygenTimestamps.remove(uid, timestamps) - uidHardwareKeygenCount.remove(uid) - } - return timestamps.size - } - } - - private fun recordHardwareKeygen(uid: Int) { - val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() } - synchronized(timestamps) { - timestamps.add(System.currentTimeMillis()) - } - } - private val GENERATE_KEY_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") private val IMPORT_KEY_TRANSACTION =