Handle swapped attestation lists on certain Android 11 devices (#108)

Observed an abnormal Keymaster attestation structure on certain Android 11 devices where the `softwareEnforced` and `teeEnforced` authorization lists were swapped in order. This is a deviation from the documented specification and the behavior seen on most devices.

This non-compliance caused parsing failures, as the code expected the `teeEnforced` list to be at a fixed index (7). On the affected devices, this index contained the `softwareEnforced` list, which critically lacks the `TAG_ROOT_OF_TRUST` needed for successful validation and patching.

This commit introduces a defensive normalization step to handle this device-specific anomaly gracefully:

1.  Before parsing, the code now inspects the ASN.1 sequence at the expected `softwareEnforced` index (6).
2.  It checks for the presence of the `TAG_ROOT_OF_TRUST`, which can only exist in the TEE-enforced list.
3.  If the tag is found, the code concludes the lists are swapped and corrects the `allFields` array in-place by swapping the elements at indices 6 and 7.

By normalizing the data structure at the beginning, the rest of the parsing and patching logic can proceed without modification, ensuring correct operation on both compliant and non-compliant devices.
This commit is contained in:
JingMatrix
2026-01-30 21:10:41 +01:00
parent 68af5ac680
commit 8649b8b928
@@ -216,11 +216,37 @@ object AttestationPatcher {
}
}
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
if (seq !is ASN1Sequence) return false
return seq.any { element ->
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
}
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
val allFields = sequence.toArray()
// Check if the fields are in the wrong order and swap them if necessary.
val softwareEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
val teeEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
// The signature of a swapped order: the RoT is in the software list's position.
if (
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
) {
// Swap the elements in the array to restore the standard order.
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
teeEnforcedCandidate
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
softwareEnforcedCandidate
}
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence