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.
This commit is contained in:
JingMatrix
2025-12-06 07:27:06 +01:00
committed by GitHub
parent 00c91adfaa
commit 13d89c4314
3 changed files with 117 additions and 14 deletions
+7 -3
View File
@@ -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
@@ -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)
@@ -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("-", "")