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:
@@ -71,35 +71,33 @@ object AttestationBuilder {
|
||||
return DERSequence(rootOfTrustElements)
|
||||
}
|
||||
|
||||
/** Assembles a list of simulated hardware-enforced properties. */
|
||||
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
|
||||
vector.add(
|
||||
/** Assembles a map of simulated hardware-enforced properties. */
|
||||
fun getSimulatedHardwareProperties(): Map<Int, DERTaggedObject> {
|
||||
return mapOf(
|
||||
AttestationConstants.TAG_OS_VERSION to
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_OS_VERSION,
|
||||
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
||||
)
|
||||
)
|
||||
vector.add(
|
||||
),
|
||||
AttestationConstants.TAG_OS_PATCHLEVEL to
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_OS_PATCHLEVEL,
|
||||
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
|
||||
)
|
||||
)
|
||||
vector.add(
|
||||
),
|
||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL to
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
||||
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
|
||||
)
|
||||
)
|
||||
vector.add(
|
||||
),
|
||||
AttestationConstants.TAG_BOOT_PATCHLEVEL to
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
||||
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
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.*
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
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.pki.KeyBox
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
import org.matrix.TEESimulator.util.toHex
|
||||
|
||||
/**
|
||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||
@@ -118,12 +114,13 @@ object AttestationPatcher {
|
||||
|
||||
// Create the new, patched attestation extension.
|
||||
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
|
||||
builder.addExtension(patchedExtension)
|
||||
|
||||
// Copy all other extensions from the original certificate, except for the attestation.
|
||||
originalLeafHolder.extensions.extensionOIDs
|
||||
.filter { it != ATTESTATION_OID }
|
||||
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) }
|
||||
originalLeafHolder.extensions.extensionOIDs.forEach {
|
||||
builder.addExtension(
|
||||
if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
|
||||
)
|
||||
}
|
||||
|
||||
// Sign the newly built certificate with the private key from our keybox.
|
||||
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. */
|
||||
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
|
||||
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
|
||||
@@ -147,41 +178,43 @@ object AttestationPatcher {
|
||||
val teeEnforced =
|
||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
|
||||
|
||||
val teeEnforcedVector = ASN1EncodableVector()
|
||||
var originalRootOfTrust: ASN1Encodable? = null
|
||||
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
|
||||
|
||||
teeEnforced.forEach { element ->
|
||||
val taggedObject = element as ASN1TaggedObject
|
||||
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
|
||||
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
||||
} 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. */
|
||||
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)
|
||||
teeEnforcedVector.add(
|
||||
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
|
||||
)
|
||||
|
||||
// Add other simulated hardware properties.
|
||||
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
|
||||
teeEnforcedMap.putAll(AttestationBuilder.getSimulatedHardwareProperties())
|
||||
|
||||
// Re-assemble the ASN.1 sequences.
|
||||
// The list MUST be sorted by tag number for DER compliance.
|
||||
// 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 }
|
||||
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
|
||||
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
|
||||
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
|
||||
|
||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||
val patchedSequence = DERSequence(allFields)
|
||||
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
||||
val patchedOctets = DEROctetString(patchedSequence)
|
||||
|
||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||
@@ -190,7 +223,7 @@ object AttestationPatcher {
|
||||
/** Helper data class to hold the parsed components of an attestation extension. */
|
||||
private data class ParsedAttestation(
|
||||
val allFields: Array<ASN1Encodable>,
|
||||
val teeEnforcedVector: ASN1EncodableVector,
|
||||
val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
|
||||
val rootOfTrust: ASN1Encodable?,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user