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 f76a4dd977
commit bfc15cba62
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`)
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
system=2025-11
boot=no # Do not report a boot patch level
vendor=20251101 # Report a specific vendor patch level
# --- Global Configuration ---
# This is the default for all apps unless specified otherwise.
# - Forge a recent system 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.logging.SystemLogger
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
@@ -67,34 +68,62 @@ object AttestationBuilder {
return DERSequence(rootOfTrustElements)
}
/** 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()),
),
AttestationConstants.TAG_OS_PATCHLEVEL to
/**
* Assembles a map representing the desired state of simulated hardware-enforced properties. A
* null value for a given tag indicates that it should be removed from the attestation.
*
* @param uid The UID of the calling application.
* @return A map where keys are attestation tag numbers and values are the desired
* [DERTaggedObject] or null to signify removal.
*/
fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
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(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
),
AttestationConstants.TAG_VENDOR_PATCHLEVEL to
ASN1Integer(osPatch.toLong()),
)
} else {
null // Signal for removal
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
if (vendorPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevelLong.toLong()),
),
AttestationConstants.TAG_BOOT_PATCHLEVEL to
ASN1Integer(vendorPatch.toLong()),
)
} else {
null // Signal for removal
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
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. */
@@ -103,7 +132,7 @@ object AttestationBuilder {
uid: Int,
securityLevel: Int,
): ASN1Sequence {
val teeEnforced = buildTeeEnforcedList(params, securityLevel)
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
val fields =
@@ -125,7 +154,11 @@ object AttestationBuilder {
}
/** 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 =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
@@ -164,28 +197,12 @@ object AttestationBuilder {
AttestationConstants.TAG_ROOT_OF_TRUST,
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.
params.brand?.let {
list.add(
@@ -61,6 +61,7 @@ object AttestationPatcher {
parsedAttestation,
keybox,
originalLeaf.sigAlgName,
uid,
)
// 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
* certificate. This is required to ensure the new certificate is signed using a compatible
* algorithm.
* @param uid The UID of the application requesting the certificate.
* @return A new [Certificate] object.
*/
private fun createPatchedLeafCertificate(
@@ -97,6 +99,7 @@ object AttestationPatcher {
parsedAttestation: ParsedAttestation,
keybox: KeyBox,
sigAlgName: String,
uid: Int,
): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain.
@@ -113,7 +116,7 @@ object AttestationPatcher {
)
// 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.
originalLeafHolder.extensions.extensionOIDs.forEach {
@@ -199,7 +202,7 @@ object AttestationPatcher {
}
/** 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
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
@@ -210,8 +213,19 @@ object AttestationPatcher {
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
// Add other simulated hardware properties.
teeEnforcedMap.putAll(AttestationBuilder.getSimulatedHardwareProperties())
// Get the desired state for simulated properties.
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.
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
@@ -40,7 +40,8 @@ object ConfigurationManager {
@Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>()
@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.
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
@@ -104,6 +105,21 @@ object ConfigurationManager {
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
* 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) {
if (file.exists()) {
try {
val lines =
file.readLines().mapNotNull { line ->
val trimmed = line.trim()
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
}
if (!file.exists()) {
globalCustomPatchLevel = null
packagePatchLevels = emptyMap()
return
}
if (lines.isEmpty()) {
customPatchLevelOverride = null
return
}
try {
val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
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.
if (lines.size == 1 && '=' !in lines[0]) {
customPatchLevelOverride =
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0])
return
return CustomPatchLevel(
system = null,
vendor = null,
boot = null,
all = lines[0],
)
}
// Handle key-value pair configuration.
@@ -195,19 +233,32 @@ object ConfigurationManager {
.toMap()
val all = map["all"]
customPatchLevelOverride =
CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
SystemLogger.info("Loaded custom security patch levels.")
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
return CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
}
} 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 {
/**
* 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 ---
/**
@@ -157,30 +162,33 @@ object AndroidDeviceUtils {
// --- Patch Level Properties ---
val patchLevel: Int
get() =
getCustomPatchLevelFor("system", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
fun getPatchLevel(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
// If custom is null, it means 'device_default' was used, so we fall back.
// 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
get() =
getCustomPatchLevelFor("vendor", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
fun getVendorPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
}
val bootPatchLevelLong: Int
get() =
getCustomPatchLevelFor("boot", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
fun getBootPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
return custom ?: 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 isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
* @return The custom patch level, or null if not configured.
*/
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.customPatchLevelOverride ?: return null
private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
val value =
when (component) {
"system" -> config.system ?: config.all
@@ -189,11 +197,14 @@ object AndroidDeviceUtils {
else -> config.all
} ?: return null
// "prop" or "no" indicates falling back to the system default.
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) {
return null
return when {
// "device_default" indicates falling back to the system property.
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. */