From bfc15cba62635a1f3948267453e6d5e381f0630e Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 4 Dec 2025 23:14:51 +0100 Subject: [PATCH] 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. --- README.md | 61 +++++++++- .../attestation/AttestationBuilder.kt | 99 +++++++++------- .../attestation/AttestationPatcher.kt | 22 +++- .../config/ConfigurationManager.kt | 107 +++++++++++++----- .../TEESimulator/util/AndroidDeviceUtils.kt | 49 ++++---- 5 files changed, 240 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index eaf8eee..168e2b6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt index 736fb69..277100a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -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 { - 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 { + val properties = mutableMapOf() + + // 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( 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( diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt index 137a353..451be2b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt @@ -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 } diff --git a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt index 7ad7038..a62c5e0 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -40,7 +40,8 @@ object ConfigurationManager { @Volatile private var packageModes = mapOf() @Volatile private var packageKeyboxes = mapOf() @Volatile private var isTeeBroken: Boolean? = null - @Volatile var customPatchLevelOverride: CustomPatchLevel? = null + @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null + @Volatile private var packagePatchLevels = mapOf() // Cache for UID to package name resolution. private val uidToPackagesCache = ConcurrentHashMap>() @@ -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() + var currentContext = "" // Empty string for global context + val contextLines = mutableMapOf>() + 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?): 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) } } diff --git a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt index d3ae49f..f680426 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -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. */