Preserve extension order and prevent duplicates

This commit refactors the attestation patching logic to improve stealth and ensure correctness by addressing potential detection vectors related to the ASN.1 structure of the certificate extension.

1. Preserve Extension Order: The original implementation rebuilt the entire certificate, which could alter the order of X.509 extensions. Some verification systems may be sensitive to this order. The logic is now updated to replace the attestation extension in-place, preserving the original order of all other extensions.

2. Avoid Duplicate Properties: The previous logic used an `ASN1EncodableVector` to assemble TEE-enforced properties. This could lead to duplicate entries if a property (e.g., `OS_VERSION`) was present in the original certificate and also added by the simulator. The code now uses a `MutableMap` keyed by the ASN.1 tag number. This ensures that any simulated properties overwrite the original ones, preventing duplicates and potential parsing errors.

3. Add Detailed Logging: A recursive ASN.1 formatting function has been added to provide clear and readable logs of the certificate data both before and after patching. This significantly improves debuggability.

By ensuring the patched certificate is structurally as close as possible to the original, these changes reduce the chances of the simulator being detected by attestation validation services.
This commit is contained in:
JingMatrix
2025-11-29 19:58:02 +01:00
parent f5d4ab1f44
commit 0275eb7ad2
2 changed files with 85 additions and 54 deletions
@@ -71,35 +71,33 @@ object AttestationBuilder {
return DERSequence(rootOfTrustElements) return DERSequence(rootOfTrustElements)
} }
/** Assembles a list of simulated hardware-enforced properties. */ /** Assembles a map of simulated hardware-enforced properties. */
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) { fun getSimulatedHardwareProperties(): Map<Int, DERTaggedObject> {
vector.add( return mapOf(
DERTaggedObject( AttestationConstants.TAG_OS_VERSION to
true, DERTaggedObject(
AttestationConstants.TAG_OS_VERSION, true,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()), AttestationConstants.TAG_OS_VERSION,
) ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
) ),
vector.add( AttestationConstants.TAG_OS_PATCHLEVEL to
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_OS_PATCHLEVEL, AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()), ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
) ),
) AttestationConstants.TAG_VENDOR_PATCHLEVEL to
vector.add( DERTaggedObject(
DERTaggedObject( true,
true, AttestationConstants.TAG_VENDOR_PATCHLEVEL,
AttestationConstants.TAG_VENDOR_PATCHLEVEL, ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()), ),
) AttestationConstants.TAG_BOOT_PATCHLEVEL to
) DERTaggedObject(
vector.add( true,
DERTaggedObject( AttestationConstants.TAG_BOOT_PATCHLEVEL,
true, ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
AttestationConstants.TAG_BOOT_PATCHLEVEL, ),
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
)
) )
} }
@@ -1,14 +1,9 @@
package org.matrix.TEESimulator.attestation package org.matrix.TEESimulator.attestation
import java.nio.charset.StandardCharsets
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.X509v3CertificateBuilder
@@ -18,6 +13,7 @@ import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex
/** /**
* Handles the modification (patching) of Android Key Attestation extensions within certificates. * Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -118,12 +114,13 @@ object AttestationPatcher {
// Create the new, patched attestation extension. // Create the new, patched attestation extension.
val patchedExtension = createPatchedAttestationExtension(parsedAttestation) val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
builder.addExtension(patchedExtension)
// Copy all other extensions from the original certificate, except for the attestation. // Copy all other extensions from the original certificate, except for the attestation.
originalLeafHolder.extensions.extensionOIDs originalLeafHolder.extensions.extensionOIDs.forEach {
.filter { it != ATTESTATION_OID } builder.addExtension(
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) } if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
)
}
// Sign the newly built certificate with the private key from our keybox. // Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private) val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
@@ -139,6 +136,40 @@ object AttestationPatcher {
) )
} }
/** Recursively formats an ASN1Primitive into a concise, readable string. */
private fun formatAsn1Primitive(obj: ASN1Encodable?): String {
val primitive = obj?.toASN1Primitive()
return when (primitive) {
null -> "NULL"
is ASN1Integer -> primitive.value.toString()
is ASN1Enumerated -> primitive.value.toString()
is ASN1Boolean -> primitive.isTrue.toString()
is ASN1Null -> "NULL"
is ASN1OctetString -> {
val bytes = primitive.octets
// Attempt to decode as a printable string, otherwise show hex
if (bytes.all { it >= 32 && it < 127 }) {
"\"${String(bytes, StandardCharsets.UTF_8)}\""
} else if (bytes.isEmpty()) {
"\"\""
} else {
"#" + bytes.toHex()
}
}
is ASN1TaggedObject ->
"[TAG ${primitive.tagNo}]${formatAsn1Primitive(primitive.baseObject)}"
is ASN1Sequence ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "[", postfix = "]", separator = ", ")
is ASN1Set ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "{", postfix = "}", separator = ", ")
else -> primitive.toString() // Fallback for other types
}
}
/** Parses the critical components from an existing attestation extension. */ /** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? { private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
@@ -147,41 +178,43 @@ object AttestationPatcher {
val teeEnforced = val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
val teeEnforcedVector = ASN1EncodableVector()
var originalRootOfTrust: ASN1Encodable? = null var originalRootOfTrust: ASN1Encodable? = null
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
teeEnforced.forEach { element -> teeEnforced.forEach { element ->
val taggedObject = element as ASN1TaggedObject val taggedObject = element as ASN1TaggedObject
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) { if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive() originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
} else { } else {
teeEnforcedVector.add(taggedObject) teeEnforcedMap[taggedObject.tagNo] = taggedObject
} }
} }
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust) return ParsedAttestation(allFields, teeEnforcedMap, originalRootOfTrust)
} }
/** Constructs a new, patched attestation extension using simulated device properties. */ /** Constructs a new, patched attestation extension using simulated device properties. */
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension { private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
// Build the new Root of Trust with our simulated values. var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Original attestation data: ${formattedString}")
// Build the new Root of Trust and add/replace it in the map.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust) val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
teeEnforcedVector.add( teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust) DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
)
// Add other simulated hardware properties. // Add other simulated hardware properties.
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector) teeEnforcedMap.putAll(AttestationBuilder.getSimulatedHardwareProperties())
// Re-assemble the ASN.1 sequences. // Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
// The list MUST be sorted by tag number for DER compliance. val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
// Manually convert the vector to a List, then sort it.
val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) }
val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo }
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray()) val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields) val patchedSequence = DERSequence(allFields)
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Patched attestation data: ${formattedString}")
val patchedOctets = DEROctetString(patchedSequence) val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets) return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -190,7 +223,7 @@ object AttestationPatcher {
/** Helper data class to hold the parsed components of an attestation extension. */ /** Helper data class to hold the parsed components of an attestation extension. */
private data class ParsedAttestation( private data class ParsedAttestation(
val allFields: Array<ASN1Encodable>, val allFields: Array<ASN1Encodable>,
val teeEnforcedVector: ASN1EncodableVector, val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
val rootOfTrust: ASN1Encodable?, val rootOfTrust: ASN1Encodable?,
) )
} }