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 bfc15cba62
commit b0206c10ec
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 #### 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. * **`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 --- # --- Global Configuration ---
# This is the default for all apps unless specified otherwise. # 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. # - Use the device's real vendor patch level.
# - Do not report a boot patch level at all. # - Do not report a boot patch level at all.
system=2025-11-05 system=YYYY-MM-05
vendor=device_default vendor=device_default
boot=no boot=no
@@ -51,6 +51,9 @@ object DeviceAttestationService {
val attestVersion: Int?, val attestVersion: Int?,
val keymasterVersion: Int?, val keymasterVersion: Int?,
val osVersion: 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. // A unique alias for the key used to perform the TEE functionality check.
@@ -178,6 +181,9 @@ object DeviceAttestationService {
var verifiedBootKey: ByteArray? = null var verifiedBootKey: ByteArray? = null
var verifiedBootHash: ByteArray? = null var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null var osVersion: Int? = null
var osPatchLevel: Int? = null
var vendorPatchLevel: Int? = null
var bootPatchLevel: Int? = null
val softwareEnforced = val softwareEnforced =
ASN1Sequence.getInstance( ASN1Sequence.getInstance(
@@ -219,17 +225,35 @@ object DeviceAttestationService {
.octets .octets
} }
} }
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION) AttestationConstants.TAG_OS_VERSION -> {
osVersion = osVersion =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue .positiveValue
.toInt() .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( 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( return AttestationData(
moduleHash, moduleHash,
@@ -238,6 +262,9 @@ object DeviceAttestationService {
attestVersion, attestVersion,
keymasterVersion, keymasterVersion,
osVersion, osVersion,
osPatchLevel,
vendorPatchLevel,
bootPatchLevel,
) )
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("Failed to parse attestation data from certificate.", e) 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.Build
import android.os.SystemProperties import android.os.SystemProperties
import java.security.MessageDigest import java.security.MessageDigest
import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1Integer
@@ -164,19 +165,56 @@ object AndroidDeviceUtils {
fun getPatchLevel(uid: Int): Int { fun getPatchLevel(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "system", isLong = false) val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
// If custom is null, it means 'device_default' was used, so we fall back. return custom ?: getRealDevicePatchLevelInt("system", 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)
} }
fun getVendorPatchLevelLong(uid: Int): Int { fun getVendorPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true) 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 { fun getBootPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true) 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 else -> config.all
} ?: return null } ?: return null
// First, resolve dynamic keywords and templates into a concrete date string.
val resolvedValue = resolveDateKeywords(value)
return when { return when {
// "device_default" indicates falling back to the system property. // "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. // "no" indicates this value should not be reported.
value.equals("no", ignoreCase = true) -> DO_NOT_REPORT resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the date string. // Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(value, isLong) 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. */ /** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
private fun parsePatchLevelValue(value: String, isLong: Boolean): Int? { private fun parsePatchLevelValue(value: String, isLong: Boolean): Int? {
val normalized = value.replace("-", "") val normalized = value.replace("-", "")