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
This commit is contained in:
Enginex0
2026-03-19 09:40:48 +01:00
parent 45477a7898
commit cb81116701
4 changed files with 205 additions and 53 deletions
@@ -364,6 +364,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first,
null,
keyDescriptor.nspace,
response,
parsedParameters,
@@ -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(),
@@ -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()
}
@@ -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<KeyParameter>? = 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<KeyParameter>? {
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,