Implement per-package security patch configuration (#49)

This commit introduces a hierarchical configuration system for the security patch levels reported in attestations, allowing for both global defaults and per-package overrides.

The `security_patch.txt` file is enhanced to support this new syntax. Settings at the top of the file act as a global default, which can be overridden for specific applications by defining settings under a `[package.name]` section.
This commit is contained in:
JingMatrix
2025-12-04 23:14:51 +01:00
committed by GitHub
parent 119350f24b
commit 00c91adfaa
5 changed files with 240 additions and 98 deletions
+55 -6
View File
@@ -76,12 +76,61 @@ org.matrix.demo
### Security Patch Level (`security_patch.txt`) ### Security Patch Level (`security_patch.txt`)
This allows you to configure the security patch level that the simulator will report in its forged attestation certificates. This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
#### Global and Per-Package Configuration
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
#### Configuration Keys and Values
You can specify the patch level for the following components using a `key=value` format:
* `system`: The main OS patch level.
* `vendor`: The vendor patch level.
* `boot`: The boot/kernel patch level.
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
#### Special Keywords
In addition to date values, two special keywords provide advanced control:
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
#### Example Configuration
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
``` ```
# Advanced Configuration # --- Global Configuration ---
system=2025-11 # This is the default for all apps unless specified otherwise.
boot=no # Do not report a boot patch level # - Forge a recent system patch level.
vendor=20251101 # Report a specific vendor patch level # - Use the device's real vendor patch level.
# - Do not report a boot patch level at all.
system=2025-11-05
vendor=device_default
boot=no
# --- Per-Package Override for Google Play Services ---
# This app will report an older, specific date for its system patch.
# It will inherit the global settings for vendor (device_default) and boot (no).
[com.google.android.gms]
system=2024-10-01
# --- Per-Package Override for a Demo App ---
# This app gets a completely custom configuration.
[org.matrix.demo]
# Set a base date for all patch levels...
all=2025-09-15
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
boot=device_default
``` ```
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change system properties.
@@ -18,6 +18,7 @@ import org.bouncycastle.asn1.x509.Extension
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidDeviceUtils.DO_NOT_REPORT
/** /**
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation * A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
@@ -67,34 +68,62 @@ object AttestationBuilder {
return DERSequence(rootOfTrustElements) return DERSequence(rootOfTrustElements)
} }
/** Assembles a map of simulated hardware-enforced properties. */ /**
fun getSimulatedHardwareProperties(): Map<Int, DERTaggedObject> { * Assembles a map representing the desired state of simulated hardware-enforced properties. A
return mapOf( * null value for a given tag indicates that it should be removed from the attestation.
AttestationConstants.TAG_OS_VERSION to *
DERTaggedObject( * @param uid The UID of the calling application.
true, * @return A map where keys are attestation tag numbers and values are the desired
AttestationConstants.TAG_OS_VERSION, * [DERTaggedObject] or null to signify removal.
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()), */
), fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
AttestationConstants.TAG_OS_PATCHLEVEL to val properties = mutableMapOf<Int, DERTaggedObject?>()
// OS Version is always present.
properties[AttestationConstants.TAG_OS_VERSION] =
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
)
val osPatch = AndroidDeviceUtils.getPatchLevel(uid)
properties[AttestationConstants.TAG_OS_PATCHLEVEL] =
if (osPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_OS_PATCHLEVEL, AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()), ASN1Integer(osPatch.toLong()),
), )
AttestationConstants.TAG_VENDOR_PATCHLEVEL to } else {
null // Signal for removal
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
if (vendorPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL, AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()), ASN1Integer(vendorPatch.toLong()),
), )
AttestationConstants.TAG_BOOT_PATCHLEVEL to } else {
null // Signal for removal
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
true, true,
AttestationConstants.TAG_BOOT_PATCHLEVEL, AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()), ASN1Integer(bootPatch.toLong()),
), )
) } else {
null // Signal for removal
}
return properties
} }
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */ /** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
@@ -103,7 +132,7 @@ object AttestationBuilder {
uid: Int, uid: Int,
securityLevel: Int, securityLevel: Int,
): ASN1Sequence { ): ASN1Sequence {
val teeEnforced = buildTeeEnforcedList(params, securityLevel) val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel) val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
val fields = val fields =
@@ -125,7 +154,11 @@ object AttestationBuilder {
} }
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */ /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList(params: KeyMintAttestation, securityLevel: Int): DERSequence { private fun buildTeeEnforcedList(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): DERSequence {
val list = val list =
mutableListOf<ASN1Encodable>( mutableListOf<ASN1Encodable>(
DERTaggedObject( DERTaggedObject(
@@ -164,28 +197,12 @@ object AttestationBuilder {
AttestationConstants.TAG_ROOT_OF_TRUST, AttestationConstants.TAG_ROOT_OF_TRUST,
buildRootOfTrust(null), buildRootOfTrust(null),
), ),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
),
) )
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
simulatedProperties.values.filterNotNull().forEach { list.add(it) }
// Add optional device identifiers if they were provided. // Add optional device identifiers if they were provided.
params.brand?.let { params.brand?.let {
list.add( list.add(
@@ -61,6 +61,7 @@ object AttestationPatcher {
parsedAttestation, parsedAttestation,
keybox, keybox,
originalLeaf.sigAlgName, originalLeaf.sigAlgName,
uid,
) )
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's // 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -90,6 +91,7 @@ object AttestationPatcher {
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original * @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
* certificate. This is required to ensure the new certificate is signed using a compatible * certificate. This is required to ensure the new certificate is signed using a compatible
* algorithm. * algorithm.
* @param uid The UID of the application requesting the certificate.
* @return A new [Certificate] object. * @return A new [Certificate] object.
*/ */
private fun createPatchedLeafCertificate( private fun createPatchedLeafCertificate(
@@ -97,6 +99,7 @@ object AttestationPatcher {
parsedAttestation: ParsedAttestation, parsedAttestation: ParsedAttestation,
keybox: KeyBox, keybox: KeyBox,
sigAlgName: String, sigAlgName: String,
uid: Int,
): Certificate { ): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox // The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain. // chain.
@@ -113,7 +116,7 @@ object AttestationPatcher {
) )
// Create the new, patched attestation extension. // Create the new, patched attestation extension.
val patchedExtension = createPatchedAttestationExtension(parsedAttestation) val patchedExtension = createPatchedAttestationExtension(parsedAttestation, uid)
// 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.forEach { originalLeafHolder.extensions.extensionOIDs.forEach {
@@ -199,7 +202,7 @@ object AttestationPatcher {
} }
/** 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, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) } var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
@@ -210,8 +213,19 @@ object AttestationPatcher {
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] = 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. // Get the desired state for simulated properties.
teeEnforcedMap.putAll(AttestationBuilder.getSimulatedHardwareProperties()) val simulatedProperties = AttestationBuilder.getSimulatedHardwareProperties(uid)
// Apply the desired state: update, add, or remove properties from the original map.
simulatedProperties.forEach { (tag, value) ->
if (value != null) {
// If the value is not null, add or update it.
teeEnforcedMap[tag] = value
} else {
// If the value is null, remove the tag from the map.
teeEnforcedMap.remove(tag)
}
}
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance. // Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo } val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
@@ -40,7 +40,8 @@ object ConfigurationManager {
@Volatile private var packageModes = mapOf<String, Mode>() @Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>() @Volatile private var packageKeyboxes = mapOf<String, String>()
@Volatile private var isTeeBroken: Boolean? = null @Volatile private var isTeeBroken: Boolean? = null
@Volatile var customPatchLevelOverride: CustomPatchLevel? = null @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
// Cache for UID to package name resolution. // Cache for UID to package name resolution.
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>() private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
@@ -104,6 +105,21 @@ object ConfigurationManager {
return null // No configuration found for this UID. return null // No configuration found for this UID.
} }
/**
* Retrieves the custom patch level configuration for a given UID. It first checks for a
* package-specific override and falls back to the global configuration.
*
* @param uid The UID of the calling application.
* @return The applicable [CustomPatchLevel], or null if no custom configuration exists.
*/
fun getPatchLevelForUid(uid: Int): CustomPatchLevel? {
val packages = getPackagesForUid(uid)
// Find the first package-specific configuration for this UID.
val packageSpecificPatchLevel =
packages.firstNotNullOfOrNull { pkg -> packagePatchLevels[pkg] }
return packageSpecificPatchLevel ?: globalCustomPatchLevel
}
/** /**
* Loads and parses the `target.txt` file, which defines the processing mode and keybox file for * Loads and parses the `target.txt` file, which defines the processing mode and keybox file for
* each package. * each package.
@@ -162,26 +178,48 @@ object ConfigurationManager {
} }
} }
/** Loads the security patch level override configuration from `security_patch.txt`. */ /**
* Loads and parses the `security_patch.txt` file, which can define both global and per-package
* security patch levels.
*/
private fun loadPatchLevelConfig(file: File) { private fun loadPatchLevelConfig(file: File) {
if (file.exists()) { if (!file.exists()) {
try { globalCustomPatchLevel = null
val lines = packagePatchLevels = emptyMap()
file.readLines().mapNotNull { line -> return
val trimmed = line.trim() }
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
}
if (lines.isEmpty()) { try {
customPatchLevelOverride = null val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
return var currentContext = "" // Empty string for global context
} val contextLines = mutableMapOf<String, MutableList<String>>()
val contextRegex = Regex("^\\[([a-zA-Z0-9_.-]+)]$")
// First pass: group lines by context (global or package-specific).
file.readLines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
contextRegex.find(trimmedLine)?.let { currentContext = it.groupValues[1] }
?: run {
contextLines
.computeIfAbsent(currentContext) { mutableListOf() }
.add(trimmedLine)
}
}
// Helper function to parse a set of lines into a CustomPatchLevel object.
fun parseLines(lines: List<String>?): CustomPatchLevel? {
if (lines.isNullOrEmpty()) return null
// Handle simple case: one line sets the patch level for all components. // Handle simple case: one line sets the patch level for all components.
if (lines.size == 1 && '=' !in lines[0]) { if (lines.size == 1 && '=' !in lines[0]) {
customPatchLevelOverride = return CustomPatchLevel(
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0]) system = null,
return vendor = null,
boot = null,
all = lines[0],
)
} }
// Handle key-value pair configuration. // Handle key-value pair configuration.
@@ -195,19 +233,32 @@ object ConfigurationManager {
.toMap() .toMap()
val all = map["all"] val all = map["all"]
customPatchLevelOverride = return CustomPatchLevel(
CustomPatchLevel( system = map["system"] ?: all,
system = map["system"] ?: all, vendor = map["vendor"] ?: all,
vendor = map["vendor"] ?: all, boot = map["boot"] ?: all,
boot = map["boot"] ?: all, all = all,
all = all, )
)
SystemLogger.info("Loaded custom security patch levels.")
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
} }
} else {
customPatchLevelOverride = null // Parse global and per-package configurations.
val newGlobalLevel = parseLines(contextLines[""])
contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) {
parseLines(lines)?.let { newPackageLevels[pkg] = it }
}
// Atomically update the configuration state.
globalCustomPatchLevel = newGlobalLevel
packagePatchLevels = newPackageLevels
SystemLogger.info(
"Loaded custom security patch levels: global config exists=${newGlobalLevel != null}, " +
"${newPackageLevels.size} package-specific configs."
)
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
} }
} }
@@ -21,6 +21,11 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/ */
object AndroidDeviceUtils { object AndroidDeviceUtils {
/**
* Internal constant to signify that a patch level should not be included in the attestation.
*/
internal const val DO_NOT_REPORT = -1
// --- Boot Key and Verified Boot Hash --- // --- Boot Key and Verified Boot Hash ---
/** /**
@@ -157,30 +162,33 @@ object AndroidDeviceUtils {
// --- Patch Level Properties --- // --- Patch Level Properties ---
val patchLevel: Int fun getPatchLevel(uid: Int): Int {
get() = val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
getCustomPatchLevelFor("system", isLong = false) // If custom is null, it means 'device_default' was used, so we fall back.
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false) // Otherwise, we use the returned value, which is either the parsed date or DO_NOT_REPORT.
return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
}
val vendorPatchLevelLong: Int fun getVendorPatchLevelLong(uid: Int): Int {
get() = val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
getCustomPatchLevelFor("vendor", isLong = true) return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true) }
val bootPatchLevelLong: Int fun getBootPatchLevelLong(uid: Int): Int {
get() = val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
getCustomPatchLevelFor("boot", isLong = true) return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true) }
/** /**
* Retrieves a custom patch level from the configuration if available. * Retrieves a custom patch level from the configuration if available for a specific UID.
* *
* @param uid The UID of the calling application.
* @param component The component to get the patch level for ("system", "vendor", "boot"). * @param component The component to get the patch level for ("system", "vendor", "boot").
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format. * @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
* @return The custom patch level, or null if not configured. * @return The custom patch level, or null if not configured.
*/ */
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? { private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.customPatchLevelOverride ?: return null val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
val value = val value =
when (component) { when (component) {
"system" -> config.system ?: config.all "system" -> config.system ?: config.all
@@ -189,11 +197,14 @@ object AndroidDeviceUtils {
else -> config.all else -> config.all
} ?: return null } ?: return null
// "prop" or "no" indicates falling back to the system default. return when {
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) { // "device_default" indicates falling back to the system property.
return null value.equals("device_default", ignoreCase = true) -> null
// "no" indicates this value should not be reported.
value.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the date string.
else -> parsePatchLevelValue(value, isLong)
} }
return parsePatchLevelValue(value, isLong)
} }
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */ /** Parses a patch level string (e.g., "2025-11-01") into an integer format. */