Skip to content

Instantly share code, notes, and snippets.

@Lua12138
Last active December 24, 2025 03:09
Show Gist options
  • Select an option

  • Save Lua12138/81312aeb1d2360fa7ef4b756ec7d9d18 to your computer and use it in GitHub Desktop.

Select an option

Save Lua12138/81312aeb1d2360fa7ef4b756ec7d9d18 to your computer and use it in GitHub Desktop.
object KeyStoreChallenge {
private val TAG = "KeyAttestation"
/**
* How to verify see to
* https://developer.android.com/privacy-and-security/security-key-attestation?hl=zh-cn#key_attestation_ext_schema
*/
fun <T> use(alias: String, challenge: ByteArray, block: (Boolean, List<PEM>) -> T): T {
if (!this.challenge(alias, true, challenge)) {
if (!this.challenge(alias, false, challenge)) {
return block(false, listOf())
}
}
val (okay, certs) = this.load(alias)
return block(okay, certs)
}
fun load(alias: String): Pair<Boolean, List<PEM>> {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val certChain = keyStore.getCertificateChain(alias)
if (certChain == null || certChain.isEmpty()) {
Log.e(TAG, "cannot found certificate chain within $alias")
return Pair(false, listOf())
}
val certs = mutableListOf<ByteArray>()
certChain.forEach { cert ->
certs.add(cert.encoded)
}
return Pair(certs.isNotEmpty(), certs)
}
fun challenge(
alias: String,
devicePropertiesAttestationIncluded: Boolean,
challenge: ByteArray
): Boolean {
if (challenge.isNotEmpty() && challenge.size < 16) {
throw RuntimeException("challenge must be gt 16 bytes or be empty(disable challenge)")
}
val now = Date()
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#parameterspec-names
val spec = ECGenParameterSpec("secp256r1")
val builder = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).setAlgorithmParameterSpec(spec)
.setDigests("SHA-256", "SHA-384", "SHA-512")
.setKeyValidityStart(now)
if (Build.VERSION.SDK_INT > 17 && challenge.isNotEmpty()) {
builder.setAttestationChallenge(challenge)
}
if (Build.VERSION.SDK_INT >= 31) {
// need READ_PHONE_STATE permission
builder.setDevicePropertiesAttestationIncluded(devicePropertiesAttestationIncluded)
}
val generator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
generator.initialize(builder.build())
val keyPair = try {
// https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setDevicePropertiesAttestationIncluded(boolean)
// KeyGenerator.generateKey() will throw ProviderException if device properties attestation fails or is not supported.
generator.generateKeyPair()
} catch (_: ProviderException) {
return false
}
Log.d(TAG, "generate public key success: ${keyPair.public.encoded.toPemString()}")
// It should be null if the operator from tee
Log.d(TAG, "generate private key success: ${keyPair.private?.encoded.toPemString()}")
return true
}
}
// 依赖项目: https://github.com/android/keyattestation
fun tee_cert_chains(@RequestBody body: String): String {
var description: KeyDescription? = null
val certs = mutableListOf<X509Certificate>()
val x509 = CertificateFactory.getInstance("X.509")
body.byteInputStream().use { input ->
for (certificate in x509.generateCertificates(input)) {
if (certificate is X509Certificate) {
if (certificate.hasAttestationExtension()) {
description = certificate.keyDescription()
}
certs.add(certificate)
}
}
}
val verifier = Verifier(
{ GoogleTrustAnchors() },
{ setOf<String>() },
{ Instant.now() }
)
val result = verifier.verify(certs)
val j = JSONObject()
val applicationId = description?.hardwareEnforced?.attestationApplicationId
?: description?.softwareEnforced?.attestationApplicationId
val pkg = JSONArray()
val sig = JSONArray()
applicationId?.packages?.forEachIndexed { index, info ->
JSONObject().put("应用包名", info.name)
.put("应用版本", info.version)
.let(pkg::put)
}
applicationId?.signatures?.map { sig.put(it.toByteArray().toHexString()) }
j.put("签名应用", pkg)
j.put("签名信息", sig)
j.put("系统版本", description?.hardwareEnforced?.osVersion)
j.put("系统补丁版本", description?.hardwareEnforced?.osPatchLevel)
j.put("实际签署时间", description?.softwareEnforced?.creationDateTime)
j.put("申请签署时间", description?.softwareEnforced?.activeDateTime)
j.put("证书验证成功", false)
when (result) {
is VerificationResult.Success -> {
j.put("证书验证成功", true)
j.put("公钥", result.publicKey)
j.put("安全等级", result.securityLevel)
j.put("解锁状态", result.verifiedBootState)
with(result.attestedDeviceIds) {
val imei = JSONArray()
imeis.forEach { imei.put(it) }
JSONObject().put("brand", brand)
.put("device", device)
.put("product", product)
.put("serial number", serialNumber)
.put("imei", imei)
.put("meid", meid)
.put("manufacturer", manufacturer)
.put("model", model)
}.let { j.put("设备标识", it) }
j.put("设备信息", result.deviceInformation)
j.put("挑战字符(hex)", result.challenge.toByteArray().toHexString())
}
is VerificationResult.PathValidationFailure -> {
j.put("错误信息", "证书链无效,不能验证来自Google CA签发")
}
else -> {
j.put("错误信息", result.toString())
}
}
return j.toString()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment