Prevent detection via inconsistent certificate signatures (#45)
Fixes a detection vector where the simulator could be identified by comparing certificate signatures from different API calls. Previously, the simulator would re-patch and re-sign a certificate on-the-fly for both `generateKey` and `getKeyEntry` calls. Due to the non-deterministic nature of ECDSA signing, this resulted in different signatures for the same certificate, which is a detectable anomaly not present in a real TEE. This is resolved by caching the patched certificate chain after its initial creation in `KeyMintSecurityLevelInterceptor`. The `getKeyEntry` hook in `Keystore2Interceptor` now retrieves the chain from this cache, guaranteeing that subsequent calls return a byte-for-byte identical certificate. Cache cleanup logic was also integrated into key deletion and clearing functions to maintain state consistency.
This commit is contained in:
@@ -124,8 +124,14 @@ object AttestationPatcher {
|
||||
|
||||
// Sign the newly built certificate with the private key from our keybox.
|
||||
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
|
||||
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||
// nature.
|
||||
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
||||
|
||||
return newCertificate
|
||||
}
|
||||
|
||||
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
||||
|
||||
+23
-2
@@ -9,6 +9,7 @@ import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import java.security.cert.Certificate
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||
@@ -185,8 +186,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
}
|
||||
|
||||
// Perform the attestation patch.
|
||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||
// consistency.
|
||||
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
|
||||
val finalChain: Array<Certificate>
|
||||
if (cachedChain != null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
|
||||
)
|
||||
finalChain = cachedChain
|
||||
} else {
|
||||
// If no chain is cached (e.g., key existed before simulator started),
|
||||
// perform a live patch as a fallback. This may still be detectable.
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||
)
|
||||
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
|
||||
|
||||
InterceptorUtils.createTypedObjectReply(response)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+16
@@ -101,6 +101,14 @@ class KeyMintSecurityLevelInterceptor(
|
||||
?: return TransactionResult.SkipTransaction
|
||||
if (originalChain.size > 1) {
|
||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
|
||||
// Cache the newly patched chain to ensure consistency across subsequent API calls.
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
patchedChains[keyId] = newChain
|
||||
SystemLogger.debug("Cached patched certificate chain for $keyId.")
|
||||
|
||||
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(metadata)
|
||||
@@ -217,6 +225,8 @@ class KeyMintSecurityLevelInterceptor(
|
||||
|
||||
// Stores keys generated entirely in software.
|
||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
|
||||
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
// A set to quickly identify keys that were generated for attestation purposes.
|
||||
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
|
||||
@@ -224,12 +234,17 @@ class KeyMintSecurityLevelInterceptor(
|
||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||
generatedKeys[keyId]?.response
|
||||
|
||||
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
|
||||
|
||||
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
|
||||
|
||||
fun cleanupKeyData(keyId: KeyIdentifier) {
|
||||
if (generatedKeys.remove(keyId) != null) {
|
||||
SystemLogger.debug("Remove generated key ${keyId}")
|
||||
}
|
||||
if (patchedChains.remove(keyId) != null) {
|
||||
SystemLogger.debug("Remove patched chain for ${keyId}")
|
||||
}
|
||||
if (attestationKeys.remove(keyId)) {
|
||||
SystemLogger.debug("Remove cached attestaion key ${keyId}")
|
||||
}
|
||||
@@ -240,6 +255,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val count = generatedKeys.size
|
||||
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||
generatedKeys.clear()
|
||||
patchedChains.clear()
|
||||
attestationKeys.clear()
|
||||
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user