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 } clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
.getInt(null) .getInt(null)
val bootHash: ByteArray by lazy { // cache attest data to avoid running attestation multiple times
getBootHashFromProp() ?: randomBytes() private val cachedAttestData: AttestationData? by lazy {
getAttestData() // from CertHacker
} }
val bootKey: ByteArray by lazy { val bootKey: ByteArray by lazy {
randomBytes() 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) @OptIn(ExperimentalStdlibApi::class)
private fun getBootHashFromProp(): ByteArray? { fun getBootHashFromProp(): ByteArray? {
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null 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 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 { private fun randomBytes(): ByteArray = ByteArray(32).also {
ThreadLocalRandom.current().nextBytes(it) 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( private val osVersionMap = mapOf(
Build.VERSION_CODES.BAKLAVA to 160000, Build.VERSION_CODES.BAKLAVA to 160000,
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000, Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
@@ -128,7 +166,8 @@ private val osVersionMap = mapOf(
Build.VERSION_CODES.Q to 100000 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( private val attestVersionMap = mapOf(
Build.VERSION_CODES.Q to 4, // Keymaster 4.1 Build.VERSION_CODES.Q to 4, // Keymaster 4.1
@@ -142,11 +181,10 @@ private val attestVersionMap = mapOf(
) )
val attestVersion: Int val attestVersion: Int
get() = attestVersionMap[Build.VERSION.SDK_INT] ?: 400 get() = cachedAttestData?.attestVersion ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400
val keymasterVersion: Int 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 { fun String.convertPatchLevel(isLong: Boolean): Int = runCatching {
val parts = split("-") 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.EcCurve
import android.hardware.security.keymint.KeyParameter import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag import android.hardware.security.keymint.Tag
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import android.util.Pair import android.util.Pair
import io.github.beakthoven.TrickyStoreOSS.*
import io.github.beakthoven.TrickyStoreOSS.core.config.Config import io.github.beakthoven.TrickyStoreOSS.core.config.Config
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor 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.x500.X500Name
import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.KeyUsage import org.bouncycastle.asn1.x509.KeyUsage
@@ -31,16 +45,21 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.math.BigInteger import java.math.BigInteger
import java.nio.charset.StandardCharsets 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.Certificate
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec
import java.util.* import java.util.Date
import java.util.LinkedList
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import android.os.Build
object CertificateHacker { object CertificateHacker {
@@ -625,7 +644,7 @@ object CertificateHacker {
} }
if (verifiedBootHash == null) { if (verifiedBootHash == null) {
verifiedBootHash = bootHash verifiedBootHash = getBootHashFromProp()
} }
val rootOfTrustElements = arrayOf( val rootOfTrustElements = arrayOf(
@@ -684,7 +703,9 @@ object CertificateHacker {
private fun createAttestationExtension(params: KeyGenParameters, uid: Int, securityLevel: Int = 1): Extension { private fun createAttestationExtension(params: KeyGenParameters, uid: Int, securityLevel: Int = 1): Extension {
try { try {
val key = bootKey val key = bootKey
val hash = bootHash val hash = getBootHashFromProp()
Logger.d("Using boothash ${hash?.toHex() ?: 0}")
val rootOfTrustEncodables = arrayOf( val rootOfTrustEncodables = arrayOf(
DEROctetString(key), DEROctetString(key),
@@ -834,4 +855,130 @@ object CertificateHacker {
return DEROctetString(DERSequence(applicationIdArray).encoded) 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!") Logger.i("Welcome to TrickyStoreOSS!")
try { try {
setupBootHash()
initializeInterceptors() initializeInterceptors()
maintainService() maintainService()
} catch (e: Exception) { } catch (e: Exception) {
@@ -6,18 +6,12 @@
package io.github.beakthoven.TrickyStoreOSS.core.config package io.github.beakthoven.TrickyStoreOSS.core.config
import android.content.pm.IPackageManager import android.content.pm.IPackageManager
import android.os.Build
import android.os.FileObserver import android.os.FileObserver
import android.os.ServiceManager 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.CertificateHacker
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
import io.github.beakthoven.TrickyStoreOSS.teeStatus
import java.io.File import java.io.File
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.security.spec.ECGenParameterSpec
object Config { object Config {
private val hackPackages = mutableSetOf<String>() private val hackPackages = mutableSetOf<String>()
@@ -74,58 +68,12 @@ object Config {
@Volatile @Volatile
private var teeBroken: Boolean? = null 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) { private fun storeTEEStatus(root: File) {
val statusFile = File(root, TEE_STATUS_FILE) val statusFile = File(root, TEE_STATUS_FILE)
val status = isTEEWorking() teeBroken = !teeStatus
teeBroken = !status
try { try {
statusFile.writeText("teeBroken=${!status}") statusFile.writeText("teeBroken=${teeBroken}")
Logger.i("TEE status written to $statusFile: teeBroken=$teeBroken")
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("Failed to write TEE status: ${e.message}") Logger.e("Failed to write TEE status: ${e.message}")
} }
@@ -9,4 +9,8 @@ public class SystemProperties {
public static String get(String key, String def) { public static String get(String key, String def) {
throw new RuntimeException(""); throw new RuntimeException("");
} }
public static void set(String key, String val) {
throw new RuntimeException("");
}
} }