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.
This commit is contained in:
@@ -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<String, Mode>()
|
||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||
@Volatile private var isTeeBroken: Boolean? = null
|
||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||
|
||||
@@ -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.
|
||||
|
||||
+9
-4
@@ -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,
|
||||
|
||||
+90
-61
@@ -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,39 +463,24 @@ 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()
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
val isAuto = ConfigurationManager.isAutoMode(callingUid)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
|
||||
InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED)
|
||||
@@ -608,6 +588,85 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||
}
|
||||
|
||||
private fun raceTeePatch(
|
||||
callingUid: Int,
|
||||
keyDescriptor: KeyDescriptor,
|
||||
attestationKey: KeyDescriptor?,
|
||||
rawParams: Array<KeyParameter>,
|
||||
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<Int, AtomicInteger>()
|
||||
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
||||
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
||||
|
||||
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 =
|
||||
|
||||
Reference in New Issue
Block a user