feat(interception): add AOSP authorize_create enforcement and wire format fixes

Integrate upstream AOSP compliance checks that failed post-v5.0 testing:

- INCLUDE_UNIQUE_ID: SELinux gen_unique_id + Android permission gate
- Forced operation rejection with PERMISSION_DENIED
- Null purpose guard returning INVALID_ARGUMENT
- Wire format: use createServiceSpecificErrorReply for authorize_create
- USAGE_COUNT_LIMIT with AtomicInteger counters and onFinishCallback
- effectiveParams merging key digest with operation purpose
- AuthorizeCreate rewrite: algorithm-purpose before purpose-list (AOSP HAL order)
- CALLER_NONCE in attestation teeEnforced list

Based on upstream commits e55d16d, 3078ea9, 2bc46be, 07c98bc, 41abe77.
This commit is contained in:
Enginex0
2026-03-19 09:21:52 +01:00
parent 634a1293c1
commit 45477a7898
4 changed files with 127 additions and 52 deletions
@@ -447,6 +447,11 @@ object AttestationBuilder {
) )
} }
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
}
params.activeDateTime?.let { params.activeDateTime?.let {
list.add( list.add(
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
@@ -14,40 +14,38 @@ object AuthorizeCreate {
rawOpParams: Array<KeyParameter>? = null, rawOpParams: Array<KeyParameter>? = null,
): Int? { ): Int? {
if (keyParams == null) return null if (keyParams == null) return null
return checkPurpose(keyParams, opParams) val purpose = opParams.purpose.firstOrNull() ?: return null
?: checkAlgorithmPurpose(keyParams, opParams) // Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
?: checkTemporalValidity(keyParams, opParams) return checkAlgorithmPurpose(keyParams, purpose)
?: checkCallerNonce(keyParams, rawOpParams) ?: checkPurpose(keyParams, purpose)
?: checkTemporalValidity(keyParams, purpose)
?: checkCallerNonce(keyParams, purpose, rawOpParams)
} }
private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val requestedPurpose = opParams.purpose.firstOrNull() ?: return null val algo = keyParams.algorithm
if (requestedPurpose == KeyPurpose.WRAP_KEY) if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
) {
return KeystoreErrorCodes.unsupportedPurpose
}
if (algo == Algorithm.EC && purpose == KeyPurpose.DECRYPT)
return KeystoreErrorCodes.unsupportedPurpose
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
return KeystoreErrorCodes.unsupportedPurpose
return null
}
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
if (purpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose return KeystoreErrorCodes.incompatiblePurpose
if (requestedPurpose !in keyParams.purpose) if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose return KeystoreErrorCodes.incompatiblePurpose
return null return null
} }
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
val purpose = opParams.purpose.firstOrNull() ?: return null
return when (keyParams.algorithm) {
Algorithm.EC -> when (purpose) {
KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose
KeyPurpose.AGREE_KEY -> null
else -> null
}
Algorithm.RSA -> when (purpose) {
KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose
else -> null
}
else -> null
}
}
private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val purpose = opParams.purpose.firstOrNull()
keyParams.activeDateTime?.let { activeDate -> keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
@@ -68,10 +66,11 @@ object AuthorizeCreate {
return null return null
} }
private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array<KeyParameter>?): Int? { private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
if (keyParams.callerNonce == true) return null if (keyParams.callerNonce == true) return null
val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited return KeystoreErrorCodes.callerNonceProhibited
return null return null
} }
} }
@@ -70,7 +70,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> { GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data) if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data)
} }
CREATE_OPERATION_TRANSACTION -> { CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -247,29 +247,38 @@ class KeyMintSecurityLevelInterceptor(
// Android framework calls createOperation with domain=APP+alias; // Android framework calls createOperation with domain=APP+alias;
// keystore2 internally resolves to KEY_ID — but software keys never // keystore2 internally resolves to KEY_ID — but software keys never
// reach keystore2's database, so we must handle both lookup paths. // reach keystore2's database, so we must handle both lookup paths.
val generatedKeyInfo = when (keyDescriptor.domain) { val resolvedEntry: Map.Entry<KeyIdentifier, GeneratedKeyInfo> =
Domain.APP -> { when (keyDescriptor.domain) {
val alias = keyDescriptor.alias ?: run { Domain.APP -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL") val alias = keyDescriptor.alias ?: run {
return TransactionResult.ContinueAndSkipPost SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
val key = KeyIdentifier(callingUid, alias)
generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
} }
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run { Domain.KEY_ID -> {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL") val nspace = keyDescriptor.nspace
val entry = if (nspace == null || nspace == 0L) null
else generatedKeys.entries
.filter { it.key.uid == callingUid }
.find { it.value.nspace == nspace }
entry ?: run {
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
} }
Domain.KEY_ID -> { val generatedKeyInfo = resolvedEntry.value
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run { val resolvedKeyId = resolvedEntry.key
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
trackAndEnforceOpLimit(callingUid, txId)?.let { return it } trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
@@ -284,14 +293,52 @@ class KeyMintSecurityLevelInterceptor(
else -> p.algorithm else -> p.algorithm
}) })
} }
val forced = data.readBoolean()
val requestedPurpose = parsedParams.purpose.firstOrNull()
if (requestedPurpose == null) {
return InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_INVALID_ARGUMENT)
}
if (forced) {
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
}
AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode ->
SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode")
return InterceptorUtils.createErrorReply(errorCode) return InterceptorUtils.createServiceSpecificErrorReply(errorCode)
} }
val keyParams = generatedKeyInfo.keyParams
val effectiveParams = if (keyParams != null) {
keyParams.copy(
purpose = parsedParams.purpose,
digest = parsedParams.digest.ifEmpty { keyParams.digest },
)
} else parsedParams
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency) val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, effectiveParams, opLatency)
if (keyParams?.usageCountLimit != null) {
val limit = keyParams.usageCountLimit
val remaining = usageCounters.getOrPut(resolvedKeyId) {
java.util.concurrent.atomic.AtomicInteger(limit)
}
if (remaining.get() <= 0) {
cleanupKeyData(resolvedKeyId)
usageCounters.remove(resolvedKeyId)
throw android.os.ServiceSpecificException(RESPONSE_KEY_NOT_FOUND)
}
softwareOperation.onFinishCallback = {
if (remaining.decrementAndGet() <= 0) {
cleanupKeyData(resolvedKeyId)
usageCounters.remove(resolvedKeyId)
SystemLogger.info("Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit).")
}
}
}
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
pruneOpsForUid(callingUid, softwareOperation, maxOps) pruneOpsForUid(callingUid, softwareOperation, maxOps)
val operationBinder = SoftwareOperationBinder(softwareOperation) val operationBinder = SoftwareOperationBinder(softwareOperation)
@@ -315,7 +362,7 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult { private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
if (data.dataSize() > MAX_ALIAS_LENGTH) { if (data.dataSize() > MAX_ALIAS_LENGTH) {
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes") SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
@@ -361,6 +408,21 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
} }
// AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires
// SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION
if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) {
val hasSELinux = ConfigurationManager.checkSELinuxPermission(
callingPid, "keystore_key", "gen_unique_id",
)
val hasAndroid = ConfigurationManager.hasPermissionForUid(
callingUid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION",
)
if (!hasSELinux && !hasAndroid) {
SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid")
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
}
}
val isSymmetric = parsedParams.algorithm == Algorithm.AES || val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
parsedParams.algorithm == Algorithm.HMAC || parsedParams.algorithm == Algorithm.HMAC ||
parsedParams.algorithm == Algorithm.TRIPLE_DES parsedParams.algorithm == Algorithm.TRIPLE_DES
@@ -668,7 +730,10 @@ class KeyMintSecurityLevelInterceptor(
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024 private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val KEYMINT_INVALID_INPUT_LENGTH = -21 private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val KEYMINT_INVALID_ARGUMENT = -38
private const val RESPONSE_INVALID_ARGUMENT = 20 private const val RESPONSE_INVALID_ARGUMENT = 20
private const val RESPONSE_PERMISSION_DENIED = 6
private const val RESPONSE_KEY_NOT_FOUND = 7
private const val TEE_LATENCY_FLOOR_MS = 15L private const val TEE_LATENCY_FLOOR_MS = 15L
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
@@ -742,6 +807,7 @@ class KeyMintSecurityLevelInterceptor(
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>() val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet() val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet() val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val usageCounters = ConcurrentHashMap<KeyIdentifier, java.util.concurrent.atomic.AtomicInteger>()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>() private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
@@ -771,6 +837,7 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.debug("Remove cached attestaion key ${keyId}") SystemLogger.debug("Remove cached attestaion key ${keyId}")
} }
importedKeys.remove(keyId) importedKeys.remove(keyId)
usageCounters.remove(keyId)
} }
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
@@ -796,6 +863,7 @@ class KeyMintSecurityLevelInterceptor(
patchedChains.clear() patchedChains.clear()
attestationKeys.clear() attestationKeys.clear()
importedKeys.clear() importedKeys.clear()
usageCounters.clear()
GeneratedKeyPersistence.deleteAll() GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
} }
@@ -162,6 +162,8 @@ class SoftwareOperation(
@Volatile var finalized = false @Volatile var finalized = false
private set private set
var onFinishCallback: (() -> Unit)? = null
val iv: ByteArray? val iv: ByteArray?
get() = primitive.getIv() get() = primitive.getIv()
@@ -231,6 +233,7 @@ class SoftwareOperation(
if (delayMs > 0) Thread.sleep(delayMs) if (delayMs > 0) Thread.sleep(delayMs)
} }
finalized = true finalized = true
onFinishCallback?.invoke()
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result return result
} catch (e: ServiceSpecificException) { } catch (e: ServiceSpecificException) {