fix(attestation): harden RSA capability probe

The RSA capability probe cached any first-call failure for the process
lifetime via by-lazy plus a catch-all false, so a transient keystore
hiccup could freeze the device into forging an attestation the real TEE
could serve, silently re-creating the issue #37 regression with no
self-heal.

Memoize only a definitive verdict: a successful probe, or a permanent
KeyStoreException per the framework's own isTransientFailure(). Transient
and non-keystore failures fail open, reporting the device attestable so
dispatch PATCHes the genuine chain, and re-probe on the next read. An
AtomicBoolean guard keeps at most one probe in flight with no lock held
across the keygen. Delete the probe key best-effort in finally.

Refs #37
This commit is contained in:
Enginex0
2026-06-25 02:19:51 +01:00
parent c2552ba164
commit 28f48f2e01
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.attestation package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.security.KeyStoreException
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
@@ -9,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.atomic.AtomicBoolean
import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1OctetString import org.bouncycastle.asn1.ASN1OctetString
@@ -69,12 +71,31 @@ object DeviceAttestationService {
*/ */
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() } val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
@Volatile private var rsaAttestableVerdict: Boolean? = null
private val rsaProbeInFlight = AtomicBoolean(false)
/** /**
* Lazily determines whether the real TEE can attest an RSA key. A device may mint EC keys yet * Whether the real TEE can attest an RSA key. A device may mint EC keys yet lack a provisioned
* lack a provisioned RSA attestation key, so [isTeeFunctional] alone over-reports capability. * RSA attestation key, so [isTeeFunctional] alone over-reports capability. AUTO dispatch reads
* AUTO dispatch reads this to forge RSA attestation only where the hardware genuinely cannot. * this to forge RSA attestation only where the hardware genuinely cannot.
*
* Only a definitive hardware verdict is cached: a successful probe, or a confirmed
* attestation-keys-unavailable failure. A transient or unrecognized failure reports attestable
* so dispatch PATCHes the genuine chain, then re-probes on the next read. A one-off keystore
* hiccup can never freeze the device into forging an attestation it could serve.
*/ */
val isRsaAttestable: Boolean by lazy { checkRsaAttestability() } val isRsaAttestable: Boolean
get() {
rsaAttestableVerdict?.let { return it }
if (rsaProbeInFlight.compareAndSet(false, true)) {
try {
probeRsaAttestability()?.let { rsaAttestableVerdict = it }
} finally {
rsaProbeInFlight.set(false)
}
}
return rsaAttestableVerdict ?: 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
@@ -117,13 +138,15 @@ object DeviceAttestationService {
} }
/** /**
* Checks whether the real TEE can attest an RSA key by generating one with an attestation * Probes 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 * 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. * 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. * @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
* fails open and re-probes.
*/ */
private fun checkRsaAttestability(): Boolean { private fun probeRsaAttestability(): Boolean? {
SystemLogger.info("Performing RSA attestation capability check...") SystemLogger.info("Performing RSA attestation capability check...")
return try { return try {
val keyPairGenerator = val keyPairGenerator =
@@ -145,8 +168,44 @@ object DeviceAttestationService {
SystemLogger.info("RSA attestation capability check successful.") SystemLogger.info("RSA attestation capability check successful.")
true true
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.warning("RSA attestation capability check failed.", e) if (isRsaAttestationUnavailable(e)) {
false SystemLogger.info("RSA attestation unsupported by hardware; AUTO will forge RSA attestation.")
false
} else {
SystemLogger.warning(
"RSA attestation capability check failed transiently; treating as capable.",
e,
)
null
}
} finally {
deleteRsaProbeKey()
}
}
/**
* Whether [error] definitively means the hardware cannot attest an RSA key: a permanent
* [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
* fixed, valid spec as root, so its only permanent keystore failure mode is missing RSA
* attestation support; [KeyStoreException.isTransientFailure] draws the transient/permanent line.
*/
private fun isRsaAttestationUnavailable(error: Throwable): Boolean {
var cause: Throwable? = error
while (cause != null) {
val keyStoreError = cause as? KeyStoreException
if (keyStoreError != null) return !keyStoreError.isTransientFailure
cause = cause.cause
}
return false
}
private fun deleteRsaProbeKey() {
try {
KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
.deleteEntry(RSA_ATTEST_CHECK_KEY_ALIAS)
} catch (e: Exception) {
SystemLogger.warning("Failed to delete RSA attestation probe key.", e)
} }
} }