Patch certificate chain in generateKey reply

When an application generates a key with an attestation request, the `generateKey` method returns a `KeyMetadata` object which contains the full, unpatched certificate chain.

This leaves a potential detection vector open. A sophisticated application could inspect the returned data in its own process memory and discover the original, hardware-backed certificates before they are used for attestation, thus detecting the hooking framework.

This commit introduces a post-transaction hook for the `generateKey` transaction. After the genuine KeyStore service has executed the request, this hook intercepts the reply parcel. It extracts the certificate chain from the `KeyMetadata`, applies the patching routine, and then reconstructs the reply with the modified (patched) certificate chain.
This commit is contained in:
JingMatrix
2025-11-29 19:58:02 +01:00
parent b2838ac04b
commit 457a58da04
@@ -10,6 +10,7 @@ import android.system.keystore2.*
import java.security.KeyPair
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.core.BinderInterceptor
@@ -77,13 +78,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?,
resultCode: Int,
): TransactionResult {
// We only care about successful 'importKey' transactions to clean cached keys.
if (
code == IMPORT_KEY_TRANSACTION &&
resultCode == 0 &&
reply != null &&
!InterceptorUtils.hasException(reply)
) {
// We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
if (code == IMPORT_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
@@ -91,6 +90,21 @@ class KeyMintSecurityLevelInterceptor(
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
} else if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
val metadata: KeyMetadata =
reply.readTypedObject(KeyMetadata.CREATOR)
?: return TransactionResult.SkipTransaction
val originalChain =
CertificateHelper.getCertificateChain(metadata)
?: return TransactionResult.SkipTransaction
if (originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(metadata)
}
}
return TransactionResult.SkipTransaction
}
@@ -148,6 +162,8 @@ class KeyMintSecurityLevelInterceptor(
writeTypedObject(response.metadata, 0)
}
return TransactionResult.OverrideReply(0, resultParcel)
} else if (parsedParams.attestationChallenge != null) {
return TransactionResult.Continue
}
// If not generating, clear any stale state for this alias and let the call proceed.