fix(attestation): correct module_hash to match AOSP Keystore2

BouncyCastle DERSet() sorts by full encoded sequence, but AOSP
keystore2 maintenance.rs sorts by encoded name only. Replace
PackageManager-based APEX enumeration with filesystem scan of
/apex/ directories using a minimal protobuf parser for
apex_manifest.pb. Encode the DER SET tag manually to preserve
the name-only sort order.
This commit is contained in:
Enginex0
2026-03-09 19:59:30 +01:00
parent 6df266b688
commit 41b9cc8f10
@@ -1,9 +1,11 @@
package org.matrix.TEESimulator.util package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.SecurityLevel
import android.os.Build import android.os.Build
import android.os.SystemProperties import android.os.SystemProperties
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.time.LocalDate import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
@@ -11,7 +13,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
@@ -371,52 +372,174 @@ object AndroidDeviceUtils {
// --- APEX and Module Hash Properties --- // --- APEX and Module Hash Properties ---
private val apexInfos: List<Pair<String, Long>> by lazy { // Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
runCatching { private class MinimalApexManifestParser(private val data: ByteArray) {
val pm = ConfigurationManager.getPackageManager() var pos = 0
val packages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { fun parse(): Pair<String, Long>? {
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) var name: String? = null
var version: Long? = null
while (pos < data.size) {
val tag = readVarint()
val fieldNum = tag ushr 3
val wireType = (tag and 0x07).toInt()
when (fieldNum) {
1L -> {
val length = readVarint().toInt()
if (pos + length > data.size) return null
name = String(data, pos, length, Charsets.UTF_8)
pos += length
}
2L -> {
version = readVarint()
}
else -> skipField(wireType)
}
}
return if (name != null && version != null) {
name to version
} else { } else {
@Suppress("DEPRECATION") null
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
} }
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
} }
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it) private fun readVarint(): Long {
emptyList() var value = 0L
var shift = 0
while (pos < data.size) {
val b = data[pos++].toInt()
value = value or ((b and 0x7F).toLong() shl shift)
if ((b and 0x80) == 0) return value
shift += 7
} }
return value
}
private fun skipField(wireType: Int) {
when (wireType) {
0 -> readVarint()
1 -> pos += 8
2 -> {
val len = readVarint().toInt()
pos += len
}
5 -> pos += 4
else -> throw IllegalStateException("Unknown wire type $wireType")
}
}
}
private val apexInfos: List<Pair<String, Long>> by lazy {
val results = mutableListOf<Pair<String, Long>>()
val apexRoot = File("/apex")
if (!apexRoot.exists() || !apexRoot.isDirectory) {
return@lazy emptyList()
}
apexRoot.listFiles()?.forEach { file ->
if (!file.isDirectory) return@forEach
val name = file.name
if (name.startsWith(".")) return@forEach
if (name.contains("@")) return@forEach
if (name == "sharedlibs") return@forEach
val manifestFile = File(file, "apex_manifest.pb")
if (manifestFile.exists()) {
runCatching {
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
val parser = MinimalApexManifestParser(bytes)
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
}
}
}
results.distinctBy { it.first }
} }
val moduleHash: ByteArray by lazy { val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching { ?: runCatching {
// TODO: figure out the correct calculation data class ModuleEntry(
val moduleSequences = ASN1EncodableVector() val nameEncoded: ByteArray,
val fullEncoded: ByteArray,
)
// 1. Create a DERSequence for each module. val modules =
apexInfos.forEach { (packageName, versionCode) -> apexInfos.map { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector() val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
// Use explicit UTF-8 encoding for the package name. val versionInt = ASN1Integer(versionCode)
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode)) val vec = ASN1EncodableVector()
moduleSequences.add(DERSequence(moduleVector)) vec.add(nameOctet)
vec.add(versionInt)
val sequence = DERSequence(vec)
// AOSP sorts by encoded name only, not full sequence
ModuleEntry(
nameEncoded = nameOctet.encoded,
fullEncoded = sequence.encoded,
)
} }
// 2. Create a DERSet. Bouncy Castle will automatically handle val sortedModules =
// the sorting based on the DER-encoded value of each sequence. modules.sortedWith { m1, m2 ->
val modulesSet = DERSet(moduleSequences) compareByteArrays(m1.nameEncoded, m2.nameEncoded)
}
// 3. Get the final DER-encoded byte array of the SET. val payloadStream = ByteArrayOutputStream()
val encodedModules = modulesSet.encoded sortedModules.forEach { payloadStream.write(it.fullEncoded) }
val payload = payloadStream.toByteArray()
// 4. Compute the SHA-256 hash. // Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
MessageDigest.getInstance("SHA-256").digest(encodedModules) val finalDerSet = encodeAsDerSet(payload)
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
} }
.getOrElse { .getOrElse {
SystemLogger.error("Failed to compute module hash.", it) SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32) // Return empty hash on failure ByteArray(32)
}
}
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
val length = minOf(a.size, b.size)
for (i in 0 until length) {
val byteA = a[i].toInt() and 0xFF
val byteB = b[i].toInt() and 0xFF
if (byteA != byteB) {
return byteA - byteB
}
}
return a.size - b.size
}
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
val out = ByteArrayOutputStream()
out.write(0x31)
writeDerLength(out, payload.size)
out.write(payload)
return out.toByteArray()
}
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
if (length < 128) {
out.write(length)
} else {
var size = length
val bytes = ArrayList<Byte>()
while (size > 0) {
bytes.add((size and 0xFF).toByte())
size = size ushr 8
}
out.write(0x80 or bytes.size)
for (i in bytes.indices.reversed()) {
out.write(bytes[i].toInt())
}
} }
} }
} }