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:
@@ -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))
|
||||
|
||||
+28
-29
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+75
-7
@@ -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,19 +247,26 @@ 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) {
|
||||
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
|
||||
}
|
||||
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
|
||||
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
|
||||
}
|
||||
}
|
||||
Domain.KEY_ID -> {
|
||||
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
|
||||
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
|
||||
@@ -270,6 +277,8 @@ class KeyMintSecurityLevelInterceptor(
|
||||
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.")
|
||||
}
|
||||
|
||||
+3
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user