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:
Enginex0
2026-06-25 00:58:13 +01:00
parent e2dc7aa210
commit c2552ba164
2 changed files with 50 additions and 3 deletions
@@ -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.
@@ -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"