fix(dispatch): gate AUTO forge on RSA capability
v282 forged every AUTO attestation that carried a challenge, so a strict app that validates attestation server-side, such as Kraken, rejected the software-forged chain where it accepted a patched real-TEE chain, breaking login. The trigger was algorithm-blind: the AUTO capability probe only mints an EC key, so it could not tell an EC-capable TEE from one that cannot provision RSA attestation keys. Add an isRsaAttestable probe and forge AUTO attestation only for RSA the real TEE cannot provision. EC and RSA-capable devices keep their genuine TEE chain via PATCH, restoring the v280 behavior strict apps depend on while preserving the RSA red fix on incapable devices. Refs #37
This commit is contained in:
@@ -8,6 +8,7 @@ import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.ASN1OctetString
|
||||
@@ -60,6 +61,7 @@ object DeviceAttestationService {
|
||||
|
||||
// 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 RSA_ATTEST_CHECK_KEY_ALIAS = "TEESimulator_RsaAttestCheck"
|
||||
|
||||
/**
|
||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||
@@ -67,6 +69,13 @@ object DeviceAttestationService {
|
||||
*/
|
||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||
|
||||
/**
|
||||
* Lazily determines whether the real TEE can attest an RSA key. A device may mint EC keys yet
|
||||
* lack a provisioned RSA attestation key, so [isTeeFunctional] alone over-reports capability.
|
||||
* AUTO dispatch reads this to forge RSA attestation only where the hardware genuinely cannot.
|
||||
*/
|
||||
val isRsaAttestable: Boolean by lazy { checkRsaAttestability() }
|
||||
|
||||
/**
|
||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||
* is cached. Returns null if the TEE is not functional or parsing fails.
|
||||
@@ -107,6 +116,40 @@ object DeviceAttestationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the real TEE can attest an RSA key by generating one with an attestation
|
||||
* challenge. Mirrors [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 an RSA key with attestation was generated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun checkRsaAttestability(): Boolean {
|
||||
SystemLogger.info("Performing RSA attestation capability check...")
|
||||
return try {
|
||||
val keyPairGenerator =
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
|
||||
|
||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||
|
||||
val spec =
|
||||
KeyGenParameterSpec.Builder(RSA_ATTEST_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
||||
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
||||
.setAttestationChallenge(challenge)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(spec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
SystemLogger.info("RSA attestation capability check successful.")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.warning("RSA attestation capability check failed.", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
||||
* deleted after retrieval to clean up.
|
||||
|
||||
+7
-3
@@ -28,6 +28,7 @@ import java.util.concurrent.locks.LockSupport
|
||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
@@ -713,8 +714,9 @@ class KeyMintSecurityLevelInterceptor(
|
||||
// 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
|
||||
// carrying the requested IDs and rooted under the keybox will satisfy the caller.
|
||||
// AUTO attestation forges from the keybox instead of trusting the EC-only TEE
|
||||
// probe, which cannot tell whether the device can attest RSA / device-ID / StrongBox.
|
||||
// AUTO forges RSA attestation only when the real TEE cannot provision an RSA
|
||||
// attestation key (isRsaAttestable). EC and RSA-capable devices keep their genuine
|
||||
// TEE chain via PATCH, which strict callers accept where a forgery is rejected.
|
||||
val forceGenerate =
|
||||
oversized ||
|
||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||
@@ -722,7 +724,9 @@ class KeyMintSecurityLevelInterceptor(
|
||||
attestationKey != null ||
|
||||
hasDeviceIdAttestation ||
|
||||
(ConfigurationManager.isAutoMode(callingUid) &&
|
||||
parsedParams.attestationChallenge != null)
|
||||
parsedParams.attestationChallenge != null &&
|
||||
parsedParams.algorithm == Algorithm.RSA &&
|
||||
!DeviceAttestationService.isRsaAttestable)
|
||||
|
||||
SystemLogger.trace {
|
||||
"[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest"
|
||||
|
||||
Reference in New Issue
Block a user