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 3acf73210d
commit 9f03b84364
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 {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
@@ -14,40 +14,38 @@ object AuthorizeCreate {
rawOpParams: Array<KeyParameter>? = null,
): Int? {
if (keyParams == null) return null
return checkPurpose(keyParams, opParams)
?: checkAlgorithmPurpose(keyParams, opParams)
?: checkTemporalValidity(keyParams, opParams)
?: checkCallerNonce(keyParams, rawOpParams)
val purpose = opParams.purpose.firstOrNull() ?: return null
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
return checkAlgorithmPurpose(keyParams, purpose)
?: checkPurpose(keyParams, purpose)
?: checkTemporalValidity(keyParams, purpose)
?: checkCallerNonce(keyParams, purpose, rawOpParams)
}
private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val requestedPurpose = opParams.purpose.firstOrNull() ?: return null
if (requestedPurpose == KeyPurpose.WRAP_KEY)
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val algo = keyParams.algorithm
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
if (requestedPurpose !in keyParams.purpose)
if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose
return null
}
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): 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? {
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
val now = System.currentTimeMillis()
val purpose = opParams.purpose.firstOrNull()
keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
@@ -68,10 +66,11 @@ object AuthorizeCreate {
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
val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true
if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
return KeystoreErrorCodes.callerNonceProhibited
return null
}
}
@@ -70,7 +70,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data)
}
CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -247,29 +247,38 @@ class KeyMintSecurityLevelInterceptor(
// Android framework calls createOperation with domain=APP+alias;
// keystore2 internally resolves to KEY_ID — but software keys never
// reach keystore2's database, so we must handle both lookup paths.
val generatedKeyInfo = when (keyDescriptor.domain) {
Domain.APP -> {
val alias = keyDescriptor.alias ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
val resolvedEntry: Map.Entry<KeyIdentifier, GeneratedKeyInfo> =
when (keyDescriptor.domain) {
Domain.APP -> {
val alias = keyDescriptor.alias ?: run {
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 {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
Domain.KEY_ID -> {
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
}
}
Domain.KEY_ID -> {
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: 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
}
}
val generatedKeyInfo = resolvedEntry.value
val resolvedKeyId = resolvedEntry.key
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
@@ -284,14 +293,52 @@ class KeyMintSecurityLevelInterceptor(
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 ->
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 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
pruneOpsForUid(callingUid, softwareOperation, maxOps)
val operationBinder = SoftwareOperationBinder(softwareOperation)
@@ -315,7 +362,7 @@ class KeyMintSecurityLevelInterceptor(
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) {
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return TransactionResult.ContinueAndSkipPost
@@ -361,6 +408,21 @@ class KeyMintSecurityLevelInterceptor(
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 ||
parsedParams.algorithm == Algorithm.HMAC ||
parsedParams.algorithm == Algorithm.TRIPLE_DES
@@ -668,7 +730,10 @@ class KeyMintSecurityLevelInterceptor(
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024
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_PERMISSION_DENIED = 6
private const val RESPONSE_KEY_NOT_FOUND = 7
private const val TEE_LATENCY_FLOOR_MS = 15L
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
@@ -742,6 +807,7 @@ class KeyMintSecurityLevelInterceptor(
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val attestationKeys: 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>()
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
@@ -771,6 +837,7 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.debug("Remove cached attestaion key ${keyId}")
}
importedKeys.remove(keyId)
usageCounters.remove(keyId)
}
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
@@ -796,6 +863,7 @@ class KeyMintSecurityLevelInterceptor(
patchedChains.clear()
attestationKeys.clear()
importedKeys.clear()
usageCounters.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
}
@@ -162,6 +162,8 @@ class SoftwareOperation(
@Volatile var finalized = false
private set
var onFinishCallback: (() -> Unit)? = null
val iv: ByteArray?
get() = primitive.getIv()
@@ -231,6 +233,7 @@ class SoftwareOperation(
if (delayMs > 0) Thread.sleep(delayMs)
}
finalized = true
onFinishCallback?.invoke()
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
} catch (e: ServiceSpecificException) {