java: Overhaul detection of attestation properties

- Reuse and extend the attestation we perform for detection of functional TEE.
- Extract properties like boot hash, os version, attest version, keymaster version from the generated certificate.
- Use previous methods as fallback for devices without functional TEE.
- Set ro.boot.vbmeta.digest during initialization with the boot hash value.

Signed-off-by: Dakkshesh <beakthoven@gmail.com>
This commit is contained in:
Dakkshesh
2025-08-19 19:33:27 +05:30
parent 088e9ff1b0
commit d762663192
5 changed files with 213 additions and 74 deletions
@@ -23,20 +23,61 @@ fun getTransactCode(clazz: Class<*>, method: String): Int =
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
.getInt(null)
val bootHash: ByteArray by lazy {
getBootHashFromProp() ?: randomBytes()
// cache attest data to avoid running attestation multiple times
private val cachedAttestData: AttestationData? by lazy {
getAttestData() // from CertHacker
}
val bootKey: ByteArray by lazy {
randomBytes()
}
fun setupBootHash() {
getBootHashFromProp()?.also {
Logger.d("Using boot hash from system property: ${it.toHex()}")
}
?: getBootHashFromAttestation()?.also {
Logger.d("Using boot hash from attestation: ${it.toHex()}")
setBootHashProp(it)
}
?: randomBytes().also {
Logger.d("Generating random boot hash: ${it.toHex()}")
setBootHashProp(it)
}
}
@OptIn(ExperimentalStdlibApi::class)
private fun getBootHashFromProp(): ByteArray? {
fun getBootHashFromProp(): ByteArray? {
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null
Logger.d("System property ro.boot.vbmeta.digest: $digest")
if (digest.isBlank()) {
Logger.d("Property is blank")
return null
}
return if (digest.length == 64) digest.hexToByteArray() else null
}
private fun getBootHashFromAttestation(): ByteArray? {
return try {
cachedAttestData?.verifiedBootHash
} catch (e: Exception) {
Logger.e("Failed to get boot hash from attestation: ${e.message}")
null
}
}
private fun setBootHashProp(bytes: ByteArray) {
val hex = bytes.toHex()
try {
Logger.d("Setting ro.boot.vbmeta.digest to: $hex")
SystemProperties.set("ro.boot.vbmeta.digest", hex)
} catch (e: Exception) {
Logger.e("Exception setting vbmeta digest: ${e.message}")
}
}
private fun randomBytes(): ByteArray = ByteArray(32).also {
ThreadLocalRandom.current().nextBytes(it)
}
@@ -114,9 +155,6 @@ private fun parsePatchLevelValue(value: String, component: String, isLong: Boole
}
}
val osVersion: Int
get() = getOsVersion(Build.VERSION.SDK_INT)
private val osVersionMap = mapOf(
Build.VERSION_CODES.BAKLAVA to 160000,
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
@@ -128,7 +166,8 @@ private val osVersionMap = mapOf(
Build.VERSION_CODES.Q to 100000
)
private fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000
val osVersion: Int
get() = cachedAttestData?.osVersion ?: osVersionMap[Build.VERSION.SDK_INT] ?: 160000
private val attestVersionMap = mapOf(
Build.VERSION_CODES.Q to 4, // Keymaster 4.1
@@ -142,11 +181,10 @@ private val attestVersionMap = mapOf(
)
val attestVersion: Int
get() = attestVersionMap[Build.VERSION.SDK_INT] ?: 400
get() = cachedAttestData?.attestVersion ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400
val keymasterVersion: Int
get() = if (attestVersion == 4) 41 else attestVersion
get() = cachedAttestData?.keymasterVersion ?: if (attestVersion == 4) 41 else attestVersion
fun String.convertPatchLevel(isLong: Boolean): Int = runCatching {
val parts = split("-")
@@ -195,4 +233,5 @@ val moduleHash: ByteArray by lazy {
}
}
fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() }
fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() }
fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
@@ -10,14 +10,28 @@ import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.system.keystore2.KeyDescriptor
import android.util.Pair
import io.github.beakthoven.TrickyStoreOSS.*
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Enumerated
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.KeyUsage
@@ -31,16 +45,21 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.ByteArrayInputStream
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Security
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.*
import java.util.Date
import java.util.LinkedList
import java.util.concurrent.ConcurrentHashMap
import javax.security.auth.x500.X500Principal
import android.os.Build
object CertificateHacker {
@@ -625,7 +644,7 @@ object CertificateHacker {
}
if (verifiedBootHash == null) {
verifiedBootHash = bootHash
verifiedBootHash = getBootHashFromProp()
}
val rootOfTrustElements = arrayOf(
@@ -684,7 +703,9 @@ object CertificateHacker {
private fun createAttestationExtension(params: KeyGenParameters, uid: Int, securityLevel: Int = 1): Extension {
try {
val key = bootKey
val hash = bootHash
val hash = getBootHashFromProp()
Logger.d("Using boothash ${hash?.toHex() ?: 0}")
val rootOfTrustEncodables = arrayOf(
DEROctetString(key),
@@ -834,4 +855,130 @@ object CertificateHacker {
return DEROctetString(DERSequence(applicationIdArray).encoded)
}
}
data class AttestationData(
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
val keymasterVersion: Int?,
val osVersion: Int?,
)
val keygen_alias = "tricky_store_oss_attest"
val teeStatus: Boolean by lazy { isTEEWorking() }
private fun isTEEWorking(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install()
} else {
android.security.keystore.AndroidKeyStoreProvider.install()
}
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
val challenge = ByteArray(16).apply {
SecureRandom().nextBytes(this)
}
val parameterSpec = KeyGenParameterSpec.Builder(
keygen_alias,
KeyProperties.PURPOSE_SIGN
)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.setIsStrongBoxBacked(false)
.build()
keyPairGenerator.initialize(parameterSpec)
keyPairGenerator.generateKeyPair()
Logger.d("TEE check: successful")
// keyStore.deleteEntry(keygen_alias)
true
} catch (e: Exception) {
Logger.w("TEE check failure: ${e.message}")
false
}
}
private fun getAttestCert(): X509Certificate? {
return if (teeStatus) {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val certChain = keyStore.getCertificateChain(keygen_alias)
if (certChain == null || certChain.isEmpty()) {
null
} else {
keyStore.deleteEntry(keygen_alias)
certChain[0] as X509Certificate
}
} else {
null
}
}
fun getAttestData(): AttestationData? {
val leaf: X509Certificate = getAttestCert() ?: return null
val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17")
return try {
val leafHolder = X509CertificateHolder(leaf.encoded)
val ext: Extension = leafHolder.getExtension(ATTESTATION_OID) ?: run {
Logger.i("No attestation extension found on certificate")
return null
}
val keyDescriptionSeq = ASN1Sequence.getInstance(ext.extnValue.octets)
val encodables = keyDescriptionSeq.toArray()
val attestVersion = ASN1Integer.getInstance(encodables[0]).value.intValueExact()
val keymasterVersion = ASN1Integer.getInstance(encodables[2]).value.intValueExact()
var attestVerifiedBootHash: ByteArray? = null
var attestOSVersion: Int? = null
val teeEnforced = ASN1Sequence.getInstance(encodables[7])
teeEnforced.forEach { element ->
val tagged = element as ASN1TaggedObject
when (tagged.tagNo) {
704 -> { // Parse Root of Trust
val rootOfTrustSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
if (rootOfTrustSeq.size() >= 4) {
attestVerifiedBootHash = ASN1OctetString.getInstance(rootOfTrustSeq.getObjectAt(3)).octets
}
}
705 -> { // Parse OS Version
attestOSVersion = ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()).value.intValueExact()
}
}
}
Logger.i("Extracted attestationVersion: $attestVersion")
Logger.i("Extracted keymasterVersion: $keymasterVersion")
Logger.i("Extracted verifiedBootHash: ${attestVerifiedBootHash?.toHex() ?: 0}")
Logger.i("Extracted osVersion: $attestOSVersion")
AttestationData(
verifiedBootHash = attestVerifiedBootHash,
attestVersion = attestVersion,
keymasterVersion = keymasterVersion,
osVersion = attestOSVersion
)
} catch (e: Exception) {
Logger.e("Failed to parse attestation data", e)
null
}
}
@@ -18,6 +18,7 @@ fun main(args: Array<String>) {
Logger.i("Welcome to TrickyStoreOSS!")
try {
setupBootHash()
initializeInterceptors()
maintainService()
} catch (e: Exception) {
@@ -6,18 +6,12 @@
package io.github.beakthoven.TrickyStoreOSS.core.config
import android.content.pm.IPackageManager
import android.os.Build
import android.os.FileObserver
import android.os.ServiceManager
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
import io.github.beakthoven.TrickyStoreOSS.teeStatus
import java.io.File
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
object Config {
private val hackPackages = mutableSetOf<String>()
@@ -74,58 +68,12 @@ object Config {
@Volatile
private var teeBroken: Boolean? = null
private fun isTEEWorking(): Boolean {
val alias = "tee_attest_test_key"
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install();
} else {
android.security.keystore.AndroidKeyStoreProvider.install();
}
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
val challenge = ByteArray(16).apply {
SecureRandom().nextBytes(this)
}
val parameterSpec = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_SIGN
)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.setIsStrongBoxBacked(false)
.build()
keyPairGenerator.initialize(parameterSpec)
keyPairGenerator.generateKeyPair()
keyStore.deleteEntry(alias)
true
} catch (e: Exception) {
Logger.e("TEE check failure: ${e.message}")
false
}
}
private fun storeTEEStatus(root: File) {
val statusFile = File(root, TEE_STATUS_FILE)
val status = isTEEWorking()
teeBroken = !status
teeBroken = !teeStatus
try {
statusFile.writeText("teeBroken=${!status}")
statusFile.writeText("teeBroken=${teeBroken}")
Logger.i("TEE status written to $statusFile: teeBroken=$teeBroken")
} catch (e: Exception) {
Logger.e("Failed to write TEE status: ${e.message}")
}
@@ -9,4 +9,8 @@ public class SystemProperties {
public static String get(String key, String def) {
throw new RuntimeException("");
}
public static void set(String key, String val) {
throw new RuntimeException("");
}
}