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:
@@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user