From c2552ba164a456775d8a294bbe7ecaa0925ece0f Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 25 Jun 2026 00:58:13 +0100 Subject: [PATCH] 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 --- .../attestation/DeviceAttestationService.kt | 43 +++++++++++++++++++ .../shim/KeyMintSecurityLevelInterceptor.kt | 10 +++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt index 30292fb..171a285 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt @@ -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. diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index 5008b25..1d79bef 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -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"