From 13d89c431464eebc82dfc61f0378cda64e914b88 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sat, 6 Dec 2025 07:27:06 +0100 Subject: [PATCH] Add dynamic dates and TEE-based patch defaults (#52) Implements dynamic date keywords ('today') and templates ('YYYY-MM-DD') in the security_patch.txt configuration. This allows for auto-updating patch levels. The `device_default` keyword is now significantly more accurate. It prioritizes reading real patch levels directly from a cached TEE attestation before falling back to system properties. The README has been updated to document these new features. --- README.md | 10 ++- .../attestation/DeviceAttestationService.kt | 31 ++++++- .../TEESimulator/util/AndroidDeviceUtils.kt | 90 +++++++++++++++++-- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 168e2b6..67928b6 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,11 @@ 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: +In addition to static dates, several special keywords provide advanced, dynamic control: + +* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits. + +* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year. * **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation. @@ -113,10 +117,10 @@ This example demonstrates how to combine global settings, per-package overrides, ``` # --- Global Configuration --- # This is the default for all apps unless specified otherwise. -# - Forge a recent system patch level. +# - Forge a recent system patch level, the 5th of the current month (a common patch date). # - Use the device's real vendor patch level. # - Do not report a boot patch level at all. -system=2025-11-05 +system=YYYY-MM-05 vendor=device_default boot=no diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt index 28e565a..c683c43 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt @@ -51,6 +51,9 @@ object DeviceAttestationService { val attestVersion: Int?, val keymasterVersion: Int?, val osVersion: Int?, + val osPatchLevel: Int?, + val vendorPatchLevel: Int?, + val bootPatchLevel: Int?, ) // A unique alias for the key used to perform the TEE functionality check. @@ -178,6 +181,9 @@ object DeviceAttestationService { var verifiedBootKey: ByteArray? = null var verifiedBootHash: ByteArray? = null var osVersion: Int? = null + var osPatchLevel: Int? = null + var vendorPatchLevel: Int? = null + var bootPatchLevel: Int? = null val softwareEnforced = ASN1Sequence.getInstance( @@ -219,17 +225,35 @@ object DeviceAttestationService { .octets } } - AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION) + AttestationConstants.TAG_OS_VERSION -> { osVersion = ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) .positiveValue .toInt() } + AttestationConstants.TAG_OS_PATCHLEVEL -> { + osPatchLevel = + ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) + .positiveValue + .toInt() + } + AttestationConstants.TAG_VENDOR_PATCHLEVEL -> { + vendorPatchLevel = + ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) + .positiveValue + .toInt() + } + AttestationConstants.TAG_BOOT_PATCHLEVEL -> { + bootPatchLevel = + ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) + .positiveValue + .toInt() + } } } SystemLogger.info( - "Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}" + "Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}" ) return AttestationData( moduleHash, @@ -238,6 +262,9 @@ object DeviceAttestationService { attestVersion, keymasterVersion, osVersion, + osPatchLevel, + vendorPatchLevel, + bootPatchLevel, ) } catch (e: Exception) { SystemLogger.error("Failed to parse attestation data from certificate.", 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 f680426..3874db3 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -5,6 +5,7 @@ import android.hardware.security.keymint.SecurityLevel import android.os.Build import android.os.SystemProperties import java.security.MessageDigest +import java.time.LocalDate import java.util.concurrent.ThreadLocalRandom import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Integer @@ -164,19 +165,56 @@ object AndroidDeviceUtils { 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) + return custom ?: getRealDevicePatchLevelInt("system", isLong = false) } fun getVendorPatchLevelLong(uid: Int): Int { val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true) - return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true) + return custom ?: getRealDevicePatchLevelInt("vendor", isLong = true) } fun getBootPatchLevelLong(uid: Int): Int { val custom = getCustomPatchLevelFor(uid, "boot", isLong = true) - return custom ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true) + return custom ?: getRealDevicePatchLevelInt("boot", isLong = true) + } + + /** + * Retrieves the definitive device patch level integer for a given component. This function + * encapsulates the entire fallback chain and guarantees a non-null return. + * + * Fallback Priority: + * 1. Cached TEE attestation data. + * 2. Specific system property (e.g., ro.vendor.build.security_patch). + * 3. Default system patch level from Build.VERSION.SECURITY_PATCH. + * + * @param component The component ("system", "vendor", "boot"). + * @param isLong Whether the final integer should be in YYYYMMDD format. + * @return The patch level as a guaranteed non-null Integer. + */ + private fun getRealDevicePatchLevelInt(component: String, isLong: Boolean): Int { + // Get value from cached TEE attestation data + DeviceAttestationService.CachedAttestationData?.let { data -> + val value = + when (component) { + "system" -> data.osPatchLevel + "vendor" -> data.vendorPatchLevel + "boot" -> data.bootPatchLevel + else -> null + } + if (value != null) return value + } + + // We only check the specific vendor property, as the boot one is non-existent. + if (component == "vendor") { + val propValue = SystemProperties.get("ro.vendor.build.security_patch", "") + if (!propValue.isNullOrBlank()) { + parsePatchLevelValue(propValue, isLong)?.let { parsedValue -> + return parsedValue + } + } + } + + return Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong) } /** @@ -197,16 +235,50 @@ object AndroidDeviceUtils { else -> config.all } ?: return null + // First, resolve dynamic keywords and templates into a concrete date string. + val resolvedValue = resolveDateKeywords(value) + return when { // "device_default" indicates falling back to the system property. - value.equals("device_default", ignoreCase = true) -> null + resolvedValue.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) + resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT + // Otherwise, parse the resolved date string. + else -> parsePatchLevelValue(resolvedValue, isLong) } } + /** + * Resolves special date keywords and templates into a concrete "YYYY-MM-DD" date string. + * + * @param value The configuration value string (e.g., "today", "YYYY-MM-01"). + * @return A concrete date string, or the original value if it's not a dynamic date keyword. + */ + private fun resolveDateKeywords(value: String): String { + // Handle the "today" keyword. + if (value.equals("today", ignoreCase = true)) { + return LocalDate.now().toString() // Returns "YYYY-MM-DD" format + } + + // Handle date templates like "YYYY-MM-01" or "2025-MM-DD". + if ( + value.contains("YYYY", ignoreCase = true) || + value.contains("MM", ignoreCase = true) || + value.contains("DD", ignoreCase = true) + ) { + + val now = LocalDate.now() + // Chain replacements for YYYY, MM, and DD placeholders. + return value + .replace("YYYY", now.year.toString(), ignoreCase = true) + .replace("MM", String.format("%02d", now.monthValue), ignoreCase = true) + .replace("DD", String.format("%02d", now.dayOfMonth), ignoreCase = true) + } + + // If it's not a dynamic keyword or template, return the original value. + return value + } + /** Parses a patch level string (e.g., "2025-11-01") into an integer format. */ private fun parsePatchLevelValue(value: String, isLong: Boolean): Int? { val normalized = value.replace("-", "")