fix(attestation): per-security-level RSA/EC probe
c2552ba gated AUTO forge on isRsaAttestable, but the probe minted its
key without setIsStrongBoxBacked, so it measured only the TEE. A device
whose TEE provisions an RSA attestation key while its StrongBox cannot
(OnePlus PJZ110, Android 16) had StrongBox RSA requests PATCHed against
the real keystore, which has no StrongBox attestation key and returns
-74 (ATTESTATION_KEYS_NOT_PROVISIONED).
Probe each (algorithm, security-level) pair independently and have
dispatch consult the verdict matching the request's security level, for
both RSA and EC. StrongBox-incapable requests forge; capable ones keep
the genuine TEE chain via PATCH.
Refs #37
This commit is contained in:
@@ -10,6 +10,7 @@ import java.security.SecureRandom
|
|||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.security.spec.ECGenParameterSpec
|
import java.security.spec.ECGenParameterSpec
|
||||||
import java.security.spec.RSAKeyGenParameterSpec
|
import java.security.spec.RSAKeyGenParameterSpec
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||||
@@ -63,7 +64,6 @@ object DeviceAttestationService {
|
|||||||
|
|
||||||
// A unique alias for the key used to perform the TEE functionality check.
|
// A unique alias for the key used to perform the TEE functionality check.
|
||||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||||
private const val RSA_ATTEST_CHECK_KEY_ALIAS = "TEESimulator_RsaAttestCheck"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||||
@@ -71,31 +71,57 @@ object DeviceAttestationService {
|
|||||||
*/
|
*/
|
||||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||||
|
|
||||||
@Volatile private var rsaAttestableVerdict: Boolean? = null
|
// Per (algorithm, security-level) attestation-capability verdicts, keyed by probe-key alias.
|
||||||
private val rsaProbeInFlight = AtomicBoolean(false)
|
// A device may attest one algorithm or security level yet lack a provisioned attestation key
|
||||||
|
// for another (e.g. a TEE that attests RSA over a StrongBox that cannot), so each pair is
|
||||||
|
// probed and cached on its own.
|
||||||
|
private data class ProbeSpec(
|
||||||
|
val algorithm: String,
|
||||||
|
val strongBox: Boolean,
|
||||||
|
val keyAlias: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val rsaTeeProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, false, "TEESimulator_RsaAttestCheck")
|
||||||
|
private val rsaStrongBoxProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_RSA, true, "TEESimulator_RsaAttestCheckSb")
|
||||||
|
private val ecTeeProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, false, "TEESimulator_EcAttestCheck")
|
||||||
|
private val ecStrongBoxProbe =
|
||||||
|
ProbeSpec(KeyProperties.KEY_ALGORITHM_EC, true, "TEESimulator_EcAttestCheckSb")
|
||||||
|
|
||||||
|
private val attestableVerdicts = ConcurrentHashMap<String, Boolean>()
|
||||||
|
private val attestProbesInFlight = ConcurrentHashMap<String, AtomicBoolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the real TEE can attest an RSA key. A device may mint EC keys yet lack a provisioned
|
* Whether the real hardware can attest an RSA key at the requested security level. AUTO dispatch
|
||||||
* RSA attestation key, so [isTeeFunctional] alone over-reports capability. AUTO dispatch reads
|
* reads this to forge RSA attestation only where the hardware genuinely cannot serve it.
|
||||||
* this to forge RSA attestation only where the hardware genuinely cannot.
|
|
||||||
*
|
*
|
||||||
* Only a definitive hardware verdict is cached: a successful probe, or a confirmed
|
* Only a definitive verdict is cached: a successful probe, or a permanent keystore failure. A
|
||||||
* attestation-keys-unavailable failure. A transient or unrecognized failure reports attestable
|
* transient or unrecognized failure leaves the verdict unset and reports attestable, so dispatch
|
||||||
* so dispatch PATCHes the genuine chain, then re-probes on the next read. A one-off keystore
|
* PATCHes the genuine chain and re-probes next read — a one-off keystore hiccup can never freeze
|
||||||
* hiccup can never freeze the device into forging an attestation it could serve.
|
* the device into forging an attestation it could serve.
|
||||||
*/
|
*/
|
||||||
val isRsaAttestable: Boolean
|
fun isRsaAttestable(strongBox: Boolean): Boolean =
|
||||||
get() {
|
isHardwareAttestable(if (strongBox) rsaStrongBoxProbe else rsaTeeProbe)
|
||||||
rsaAttestableVerdict?.let { return it }
|
|
||||||
if (rsaProbeInFlight.compareAndSet(false, true)) {
|
/** Whether the real hardware can attest an EC key at the requested security level. */
|
||||||
try {
|
fun isEcAttestable(strongBox: Boolean): Boolean =
|
||||||
probeRsaAttestability()?.let { rsaAttestableVerdict = it }
|
isHardwareAttestable(if (strongBox) ecStrongBoxProbe else ecTeeProbe)
|
||||||
} finally {
|
|
||||||
rsaProbeInFlight.set(false)
|
private fun isHardwareAttestable(probe: ProbeSpec): Boolean {
|
||||||
}
|
attestableVerdicts[probe.keyAlias]?.let { return it }
|
||||||
|
val probeInFlight =
|
||||||
|
attestProbesInFlight.computeIfAbsent(probe.keyAlias) { AtomicBoolean(false) }
|
||||||
|
if (probeInFlight.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
probeAttestability(probe)?.let { attestableVerdicts[probe.keyAlias] = it }
|
||||||
|
} finally {
|
||||||
|
probeInFlight.set(false)
|
||||||
}
|
}
|
||||||
return rsaAttestableVerdict ?: true
|
|
||||||
}
|
}
|
||||||
|
return attestableVerdicts[probe.keyAlias] ?: true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||||
@@ -138,59 +164,66 @@ object DeviceAttestationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probes whether the real TEE can attest an RSA key by generating one with an attestation
|
* Probes whether the real hardware can attest a key matching [probe] by generating one with an
|
||||||
* challenge. Mirrors [checkTeeFunctionality]; the request runs as the module UID, so it is
|
* attestation challenge at the probe's algorithm and security level. Mirrors
|
||||||
* skipped by interception and reaches genuine hardware rather than the forge path.
|
* [checkTeeFunctionality]; the request runs as the module UID, so it is skipped by interception
|
||||||
|
* and reaches genuine hardware rather than the forge path.
|
||||||
*
|
*
|
||||||
* @return `true` if attestation succeeded, `false` only on a confirmed attestation-keys-
|
* @return `true` if attestation succeeded, `false` only on a confirmed attestation-keys-
|
||||||
* unavailable failure, or `null` on a transient or unrecognized failure where the caller
|
* unavailable failure, or `null` on a transient or unrecognized failure where the caller
|
||||||
* fails open and re-probes.
|
* fails open and re-probes.
|
||||||
*/
|
*/
|
||||||
private fun probeRsaAttestability(): Boolean? {
|
private fun probeAttestability(probe: ProbeSpec): Boolean? {
|
||||||
SystemLogger.info("Performing RSA attestation capability check...")
|
val label = "${probe.algorithm} attestation (strongBox=${probe.strongBox})"
|
||||||
|
SystemLogger.info("Performing $label capability check...")
|
||||||
return try {
|
return try {
|
||||||
val keyPairGenerator =
|
val keyPairGenerator =
|
||||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
|
KeyPairGenerator.getInstance(probe.algorithm, "AndroidKeyStore")
|
||||||
|
|
||||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||||
|
|
||||||
val spec =
|
val builder =
|
||||||
KeyGenParameterSpec.Builder(RSA_ATTEST_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
KeyGenParameterSpec.Builder(probe.keyAlias, KeyProperties.PURPOSE_SIGN)
|
||||||
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
|
|
||||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||||
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
|
||||||
.setAttestationChallenge(challenge)
|
.setAttestationChallenge(challenge)
|
||||||
.build()
|
.setIsStrongBoxBacked(probe.strongBox)
|
||||||
|
if (probe.algorithm == KeyProperties.KEY_ALGORITHM_RSA) {
|
||||||
|
builder
|
||||||
|
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
|
||||||
|
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
||||||
|
} else {
|
||||||
|
builder.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||||
|
}
|
||||||
|
|
||||||
keyPairGenerator.initialize(spec)
|
keyPairGenerator.initialize(builder.build())
|
||||||
keyPairGenerator.generateKeyPair()
|
keyPairGenerator.generateKeyPair()
|
||||||
|
|
||||||
SystemLogger.info("RSA attestation capability check successful.")
|
SystemLogger.info("$label capability check successful.")
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (isRsaAttestationUnavailable(e)) {
|
if (isAttestationUnavailable(e)) {
|
||||||
SystemLogger.info("RSA attestation unsupported by hardware; AUTO will forge RSA attestation.")
|
SystemLogger.info("$label unsupported by hardware; AUTO will forge attestation.")
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"RSA attestation capability check failed transiently; treating as capable.",
|
"$label capability check failed transiently; treating as capable.",
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
deleteRsaProbeKey()
|
deleteProbeKey(probe.keyAlias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether [error] definitively means the hardware cannot attest an RSA key: a permanent
|
* Whether [error] definitively means the hardware cannot attest the probed key: a permanent
|
||||||
* [KeyStoreException] from the keystore. Transient failures and non-keystore errors return
|
* [KeyStoreException] from the keystore. Transient failures and non-keystore errors return
|
||||||
* `false`, so the caller fails open and re-probes rather than caching a guess. The probe runs a
|
* `false`, so the caller fails open and re-probes rather than caching a guess. The probe runs a
|
||||||
* fixed, valid spec as root, so its only permanent keystore failure mode is missing RSA
|
* fixed, valid spec as root, so its only permanent keystore failure mode is missing attestation
|
||||||
* attestation support; [KeyStoreException.isTransientFailure] draws the transient/permanent line.
|
* support; [KeyStoreException.isTransientFailure] draws the transient/permanent line.
|
||||||
*/
|
*/
|
||||||
private fun isRsaAttestationUnavailable(error: Throwable): Boolean {
|
private fun isAttestationUnavailable(error: Throwable): Boolean {
|
||||||
var cause: Throwable? = error
|
var cause: Throwable? = error
|
||||||
while (cause != null) {
|
while (cause != null) {
|
||||||
val keyStoreError = cause as? KeyStoreException
|
val keyStoreError = cause as? KeyStoreException
|
||||||
@@ -200,12 +233,11 @@ object DeviceAttestationService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteRsaProbeKey() {
|
private fun deleteProbeKey(keyAlias: String) {
|
||||||
try {
|
try {
|
||||||
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }.deleteEntry(keyAlias)
|
||||||
.deleteEntry(RSA_ATTEST_CHECK_KEY_ALIAS)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.warning("Failed to delete RSA attestation probe key.", e)
|
SystemLogger.warning("Failed to delete attestation probe key.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -709,9 +709,12 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
// Device-ID attestation must be forged, not patched: the real TEE returns
|
// Device-ID attestation must be forged, not patched: the real TEE returns
|
||||||
// CANNOT_ATTEST_IDS, so there is no real chain to patch — only a synthetic one
|
// CANNOT_ATTEST_IDS, so there is no real chain to patch — only a synthetic one
|
||||||
// carrying the requested IDs and rooted under the keybox will satisfy the caller.
|
// carrying the requested IDs and rooted under the keybox will satisfy the caller.
|
||||||
// AUTO forges RSA attestation only when the real TEE cannot provision an RSA
|
// AUTO forges asymmetric attestation only when the real hardware cannot provision an
|
||||||
// attestation key (isRsaAttestable). EC and RSA-capable devices keep their genuine
|
// attestation key for that algorithm at the requested security level: a device may
|
||||||
// TEE chain via PATCH, which strict callers accept where a forgery is rejected.
|
// attest RSA in the TEE yet not in StrongBox, so the probe must match the request.
|
||||||
|
// Devices that can attest keep their genuine chain via PATCH, which strict callers
|
||||||
|
// accept where a forgery is rejected.
|
||||||
|
val strongBox = securityLevel == SecurityLevel.STRONGBOX
|
||||||
val forceGenerate =
|
val forceGenerate =
|
||||||
oversized ||
|
oversized ||
|
||||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||||
@@ -720,8 +723,12 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
hasDeviceIdAttestation ||
|
hasDeviceIdAttestation ||
|
||||||
(ConfigurationManager.isAutoMode(callingUid) &&
|
(ConfigurationManager.isAutoMode(callingUid) &&
|
||||||
parsedParams.attestationChallenge != null &&
|
parsedParams.attestationChallenge != null &&
|
||||||
parsedParams.algorithm == Algorithm.RSA &&
|
when (parsedParams.algorithm) {
|
||||||
!DeviceAttestationService.isRsaAttestable)
|
Algorithm.RSA ->
|
||||||
|
!DeviceAttestationService.isRsaAttestable(strongBox)
|
||||||
|
Algorithm.EC -> !DeviceAttestationService.isEcAttestable(strongBox)
|
||||||
|
else -> false
|
||||||
|
})
|
||||||
|
|
||||||
SystemLogger.trace {
|
SystemLogger.trace {
|
||||||
"[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest"
|
"[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest"
|
||||||
|
|||||||
Reference in New Issue
Block a user