From fef17c07ec8526d27ec40034c1f0435ecca954ca Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 19 Mar 2026 09:40:48 +0100 Subject: [PATCH] feat(interception): close remaining PR #157 compliance gaps Full diff analysis against upstream's 50 commits revealed 8 functional gaps after v5.0. These are detectable by conformance tests or detector apps inspecting KeyMetadata authorizations and operation semantics. KeyMetadata authorizations: - Add 9 TEE-enforced tags (CALLER_NONCE, MIN_MAC_LENGTH, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL) - Fix CREATION_DATETIME to SOFTWARE security level via createSwAuth - Add SOFTWARE-enforced date enforcement, USAGE_COUNT_LIMIT, UNLOCKED_DEVICE_REQUIRED Symmetric key support: - Generate AES/HMAC keys in software via javax.crypto.KeyGenerator - GeneratedKeyInfo expanded with nullable keyPair + secretKey fields - CipherPrimitive accepts java.security.Key for symmetric operations - SoftwareOperation routes ENCRYPT/DECRYPT to secretKey when available Operation compliance: - beginParameters property replaces manual IV wrapping for GCM - KeyAgreementPrimitive for ECDH AGREE_KEY operations - handleCreateOperation wrapped in runCatching (crash prevention) - SECURE_HW_COMMUNICATION_FAILED on software gen failure Certificate patching: - Import key cert chain + authorization patching in onPostTransact - patchAuthorizations added to post-generateKey PATCH mode path --- .../keystore/Keystore2Interceptor.kt | 1 + .../keystore/shim/GeneratedKeyPersistence.kt | 3 +- .../shim/KeyMintSecurityLevelInterceptor.kt | 185 ++++++++++++++---- .../keystore/shim/SoftwareOperation.kt | 69 +++++-- 4 files changed, 205 insertions(+), 53 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 37a5d88..4bf9c9e 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 @@ -364,6 +364,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( keyData.first, + null, keyDescriptor.nspace, response, parsedParameters, diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt index 07c57d8..3f2e76e 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt @@ -303,9 +303,10 @@ object GeneratedKeyPersistence { return } + val keyPair = generatedKeyInfo.keyPair ?: return save( keyId = keyId, - keyPair = generatedKeyInfo.keyPair, + keyPair = keyPair, nspace = generatedKeyInfo.nspace, securityLevel = secLevel, certChain = newChain.toList(), 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 795ac73..959287c 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 @@ -46,7 +46,8 @@ class KeyMintSecurityLevelInterceptor( ) : BinderInterceptor() { data class GeneratedKeyInfo( - val keyPair: KeyPair, + val keyPair: KeyPair?, + val secretKey: javax.crypto.SecretKey?, val nspace: Long, val response: KeyEntryResponse, val keyParams: KeyMintAttestation? = null, @@ -136,6 +137,22 @@ class KeyMintSecurityLevelInterceptor( } attestationKeys.remove(keyId) importedKeys.add(keyId) + + if (!ConfigurationManager.shouldSkipUid(callingUid)) { + val metadata: KeyMetadata = + reply.readTypedObject(KeyMetadata.CREATOR) + ?: return TransactionResult.SkipTransaction + val originalChain = CertificateHelper.getCertificateChain(metadata) + if (originalChain != null && originalChain.size > 1) { + val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) + CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() + metadata.authorizations = + InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) + patchedChains[keyId] = newChain + SystemLogger.debug("Cached patched certificate chain for imported key $keyId.") + return InterceptorUtils.createTypedObjectReply(metadata) + } + } } else if (code == CREATE_OPERATION_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) @@ -189,6 +206,8 @@ class KeyMintSecurityLevelInterceptor( val key = metadata.key!! val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() + metadata.authorizations = + InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) // We must clean up cached generated keys before storing the patched chain cleanupKeyData(keyId) @@ -237,7 +256,7 @@ class KeyMintSecurityLevelInterceptor( txId: Long, callingUid: Int, data: Parcel, - ): TransactionResult { + ): TransactionResult = runCatching { SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}") data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! @@ -287,11 +306,14 @@ class KeyMintSecurityLevelInterceptor( val params = data.createTypedArray(KeyParameter.CREATOR)!! val parsedParams = KeyMintAttestation(params).let { p -> if (p.algorithm != 0) p - else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) { - "EC", "ECDSA" -> Algorithm.EC - "RSA" -> Algorithm.RSA - else -> p.algorithm - }) + else { + val keyAlgo = generatedKeyInfo.keyPair?.private?.algorithm + p.copy(algorithm = when (keyAlgo) { + "EC", "ECDSA" -> Algorithm.EC + "RSA" -> Algorithm.RSA + else -> generatedKeyInfo.keyParams?.algorithm ?: p.algorithm + }) + } } val forced = data.readBoolean() @@ -318,7 +340,7 @@ class KeyMintSecurityLevelInterceptor( } else parsedParams val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, effectiveParams, opLatency) + val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, generatedKeyInfo.secretKey, effectiveParams, opLatency) if (keyParams?.usageCountLimit != null) { val limit = keyParams.usageCountLimit @@ -328,7 +350,7 @@ class KeyMintSecurityLevelInterceptor( if (remaining.get() <= 0) { cleanupKeyData(resolvedKeyId) usageCounters.remove(resolvedKeyId) - throw android.os.ServiceSpecificException(RESPONSE_KEY_NOT_FOUND) + return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_KEY_NOT_FOUND) } softwareOperation.onFinishCallback = { if (remaining.decrementAndGet() <= 0) { @@ -347,19 +369,13 @@ class KeyMintSecurityLevelInterceptor( CreateOperationResponse().apply { iOperation = operationBinder operationChallenge = null - softwareOperation.iv?.let { iv -> - parameters = KeyParameters().apply { - keyParameter = arrayOf( - KeyParameter().apply { - tag = Tag.NONCE - value = KeyParameterValue.blob(iv) - } - ) - } - } + parameters = softwareOperation.beginParameters } - return InterceptorUtils.createTypedObjectReply(response) + InterceptorUtils.createTypedObjectReply(response) + }.getOrElse { + SystemLogger.error("Error during createOperation for UID $callingUid.", it) + InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR) } private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { @@ -427,11 +443,6 @@ class KeyMintSecurityLevelInterceptor( parsedParams.algorithm == Algorithm.HMAC || parsedParams.algorithm == Algorithm.TRIPLE_DES - if (isSymmetric) { - SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL") - return TransactionResult.ContinueAndSkipPost - } - if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) { SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection") return TransactionResult.ContinueAndSkipPost @@ -475,7 +486,7 @@ class KeyMintSecurityLevelInterceptor( } .getOrElse { SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) - TransactionResult.ContinueAndSkipPost + InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED) } } @@ -487,10 +498,55 @@ class KeyMintSecurityLevelInterceptor( keyId: KeyIdentifier, isAttestKeyRequest: Boolean, ): TransactionResult { - val startNs = System.nanoTime() + val genStartNanos = System.nanoTime() keyDescriptor.nspace = secureRandom.nextLong() SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].") + cleanupKeyData(keyId) + + val isSymmetric = parsedParams.algorithm != Algorithm.EC && + parsedParams.algorithm != Algorithm.RSA + + if (isSymmetric) { + val algoName = when (parsedParams.algorithm) { + Algorithm.AES -> "AES" + Algorithm.HMAC -> "HmacSHA256" + else -> throw android.os.ServiceSpecificException( + SECURE_HW_COMMUNICATION_FAILED, + "Unsupported symmetric algorithm: ${parsedParams.algorithm}", + ) + } + val keyGen = javax.crypto.KeyGenerator.getInstance(algoName) + keyGen.init(parsedParams.keySize) + val secretKey = keyGen.generateKey() + + val metadata = KeyMetadata().apply { + keySecurityLevel = securityLevel + key = KeyDescriptor().apply { + domain = Domain.KEY_ID + nspace = keyDescriptor.nspace + alias = null + blob = null + } + certificate = null + certificateChain = null + authorizations = parsedParams.toAuthorizations(callingUid, securityLevel) + modificationTimeMs = System.currentTimeMillis() + } + val response = KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + generatedKeys[keyId] = GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams) + + val elapsedMs = (System.nanoTime() - genStartNanos) / 1_000_000 + val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS + val delayMs = floor - elapsedMs + if (delayMs > 0) Thread.sleep(delayMs) + + return InterceptorUtils.createTypedObjectReply(metadata) + } + val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { generateAttestedKeyPairNative(callingUid, parsedParams) ?: CertificateGenerator.generateAttestedKeyPair( @@ -502,9 +558,8 @@ class KeyMintSecurityLevelInterceptor( ) } ?: throw Exception("Both native and BouncyCastle cert gen failed.") - cleanupKeyData(keyId) val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response, parsedParams) + generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams) if (isAttestKeyRequest) attestationKeys.add(keyId) GeneratedKeyPersistence.save( @@ -521,7 +576,7 @@ class KeyMintSecurityLevelInterceptor( isAttestationKey = isAttestKeyRequest, ) - val elapsedMs = (System.nanoTime() - startNs) / 1_000_000 + val elapsedMs = (System.nanoTime() - genStartNanos) / 1_000_000 val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS val delayMs = floor - elapsedMs if (delayMs > 0) Thread.sleep(delayMs) @@ -711,7 +766,7 @@ class KeyMintSecurityLevelInterceptor( ) val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response, attestation) + generatedKeys[keyId] = GeneratedKeyInfo(keyPair, null, record.nspace, response, attestation) if (record.isAttestationKey) attestationKeys.add(keyId) SystemLogger.debug("Restored persisted key: $keyId") @@ -739,6 +794,8 @@ class KeyMintSecurityLevelInterceptor( private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L private const val KEYMINT_TOO_MANY_OPERATIONS = -29 private const val KEYMINT_CANNOT_ATTEST_IDS = -66 + private const val KEYMINT_UNKNOWN_ERROR = -1000 + private const val SECURE_HW_COMMUNICATION_FAILED = -49 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 @@ -900,6 +957,34 @@ private fun KeyMintAttestation.toAuthorizations( if (this.rsaPublicExponent != null) { authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) } + if (this.callerNonce == true) { + authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true))) + } + if (this.minMacLength != null) { + authList.add(createAuth(Tag.MIN_MAC_LENGTH, KeyParameterValue.integer(this.minMacLength))) + } + if (this.rollbackResistance == true) { + authList.add(createAuth(Tag.ROLLBACK_RESISTANCE, KeyParameterValue.boolValue(true))) + } + if (this.earlyBootOnly == true) { + authList.add(createAuth(Tag.EARLY_BOOT_ONLY, KeyParameterValue.boolValue(true))) + } + if (this.allowWhileOnBody == true) { + authList.add(createAuth(Tag.ALLOW_WHILE_ON_BODY, KeyParameterValue.boolValue(true))) + } + if (this.trustedUserPresenceRequired == true) { + authList.add(createAuth(Tag.TRUSTED_USER_PRESENCE_REQUIRED, KeyParameterValue.boolValue(true))) + } + if (this.trustedConfirmationRequired == true) { + authList.add(createAuth(Tag.TRUSTED_CONFIRMATION_REQUIRED, KeyParameterValue.boolValue(true))) + } + if (this.maxUsesPerBoot != null) { + authList.add(createAuth(Tag.MAX_USES_PER_BOOT, KeyParameterValue.integer(this.maxUsesPerBoot))) + } + if (this.maxBootLevel != null) { + authList.add(createAuth(Tag.MAX_BOOT_LEVEL, KeyParameterValue.integer(this.maxBootLevel))) + } + authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true))) authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED))) authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion))) @@ -916,17 +1001,37 @@ private fun KeyMintAttestation.toAuthorizations( if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) { authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) } - authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis()))) - authList.add( - Authorization().apply { - this.keyParameter = - KeyParameter().apply { - this.tag = Tag.USER_ID - this.value = KeyParameterValue.integer(callingUid / 100000) - } + + fun createSwAuth(tag: Int, value: KeyParameterValue): Authorization { + val param = KeyParameter().apply { + this.tag = tag + this.value = value + } + return Authorization().apply { + this.keyParameter = param this.securityLevel = SecurityLevel.SOFTWARE } - ) + } + + authList.add(createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis()))) + + this.activeDateTime?.let { + authList.add(createSwAuth(Tag.ACTIVE_DATETIME, KeyParameterValue.dateTime(it.time))) + } + this.originationExpireDateTime?.let { + authList.add(createSwAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time))) + } + this.usageExpireDateTime?.let { + authList.add(createSwAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time))) + } + this.usageCountLimit?.let { + authList.add(createSwAuth(Tag.USAGE_COUNT_LIMIT, KeyParameterValue.integer(it))) + } + if (this.unlockedDeviceRequired == true) { + authList.add(createSwAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true))) + } + + authList.add(createSwAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000))) return authList.toTypedArray() } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt index 0b8af43..395a934 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt @@ -3,10 +3,14 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.Digest +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyParameterValue import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.PaddingMode +import android.hardware.security.keymint.Tag import android.os.ServiceSpecificException import android.system.keystore2.IKeystoreOperation +import android.system.keystore2.KeyParameters import java.security.KeyPair import java.security.Signature import java.security.SignatureException @@ -22,7 +26,7 @@ private sealed interface CryptoPrimitive { fun update(data: ByteArray?): ByteArray? fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? fun abort() - fun getIv(): ByteArray? = null + fun getBeginParameters(): Array? = null } private object JcaAlgorithmMapper { @@ -125,15 +129,14 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri } private class CipherPrimitive( - keyPair: KeyPair, + cryptoKey: java.security.Key, params: KeyMintAttestation, private val opMode: Int, ) : CryptoPrimitive { private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM private val cipher: Cipher = Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { - val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private - init(opMode, key) + init(opMode, cryptoKey) } override fun updateAad(aadInput: ByteArray?) { @@ -147,14 +150,45 @@ private class CipherPrimitive( override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = if (data != null) cipher.doFinal(data) else cipher.doFinal() - override fun getIv(): ByteArray? = if (isAead) cipher.iv else null + override fun getBeginParameters(): Array? { + val iv = cipher.iv ?: return null + return arrayOf( + KeyParameter().apply { + tag = Tag.NONCE + value = KeyParameterValue.blob(iv) + } + ) + } + + override fun abort() {} +} + +private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive { + private val agreement: javax.crypto.KeyAgreement = + javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) } + + override fun update(data: ByteArray?): ByteArray? = null + + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + if (data == null) + throw ServiceSpecificException( + KeystoreErrorCodes.invalidArgument, + "Peer public key required for key agreement", + ) + val peerKey = + java.security.KeyFactory.getInstance("EC") + .generatePublic(java.security.spec.X509EncodedKeySpec(data)) + agreement.doPhase(peerKey, true) + return agreement.generateSecret() + } override fun abort() {} } class SoftwareOperation( private val txId: Long, - keyPair: KeyPair, + keyPair: KeyPair?, + secretKey: javax.crypto.SecretKey?, params: KeyMintAttestation, private val latencyFloorMs: Long = 0L, ) { @@ -164,8 +198,12 @@ class SoftwareOperation( var onFinishCallback: (() -> Unit)? = null - val iv: ByteArray? - get() = primitive.getIv() + val beginParameters: KeyParameters? + get() { + val params = primitive.getBeginParameters() ?: return null + if (params.isEmpty()) return null + return KeyParameters().apply { keyParameter = params } + } init { val purpose = params.purpose.firstOrNull() @@ -174,10 +212,17 @@ class SoftwareOperation( primitive = when (purpose) { - KeyPurpose.SIGN -> Signer(keyPair, params) - KeyPurpose.VERIFY -> Verifier(keyPair, params) - KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) - KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) + KeyPurpose.SIGN -> Signer(keyPair!!, params) + KeyPurpose.VERIFY -> Verifier(keyPair!!, params) + KeyPurpose.ENCRYPT -> { + val key: java.security.Key = secretKey ?: keyPair!!.public + CipherPrimitive(key, params, Cipher.ENCRYPT_MODE) + } + KeyPurpose.DECRYPT -> { + val key: java.security.Key = secretKey ?: keyPair!!.private + CipherPrimitive(key, params, Cipher.DECRYPT_MODE) + } + KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!) else -> throw ServiceSpecificException( KeystoreErrorCodes.unsupportedPurpose,