What hardware attestation underpins
We rely on Android hardware key attestation for three security decisions. All of them trace back to one certificate — the Google attestation root — so when that certificate changes, all three are affected.
Genuine hardware
Verify keys were generated inside real secure hardware, not software.
TEE / StrongBox
Confirm the key is protected by the Trusted Execution Environment or StrongBox.
Device integrity
Assess overall device state to drive security and risk decisions.
The change
RSA out, ECDSA P-384 in
Google changed the root certificate used to verify Android key attestation, as part of the transition to Remote Key Provisioning (RKP).
Old root RSA | Old root RSA |
New root ECDSA P-384 | New root ECDSA P-384 |
Old | New | |
|---|---|---|
Signature algorithm | RSA | ECDSA P-384 |
Cryptography type | Large-number cryptography | Elliptic Curve Cryptography |
Provisioning model | Factory attestation model | Remote Key Provisioning (RKP) |
See it yourself: which root is a device on?
You don't need special tooling to observe the change. Any attestation-viewer app that dumps the chain — or a few lines against the Keystore — shows it. The single tell is the root certificate's signature algorithm: SHA384withECDSA means the new root; SHA256withRSA means the legacy one.
The whole check is one function: generate an EC key with an attestation challenge, pull the resulting chain, and log each certificate's signature algorithm.
KotlinAttestDump.kt · client / debug
// Generate an EC key inside AndroidKeyStore WITH attestation,
// then dump the chain so you can read the root's algorithm.
fun dumpAttestationChain(challenge: ByteArray) {
val kpg = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"attest_demo_key",
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge) // server nonce => triggers attestation
// .setIsStrongBoxBacked(true) // force StrongBox if present
.build()
kpg.initialize(spec)
kpg.generateKeyPair() // key + attestation chain created here
// Pull the chain and inspect each certificate.
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val chain = ks.getCertificateChain("attest_demo_key")
chain.forEachIndexed { i, cert ->
cert as X509Certificate
val role = when (i) {
0 -> "LEAF"; chain.lastIndex -> "ROOT"; else -> "INTERMEDIATE"
}
Log.i("Attest", "[$i] $role")
Log.i("Attest", " sigAlg = ${cert.sigAlgName}") // SHA384withECDSA => new root
Log.i("Attest", " expires = ${cert.notAfter}") // short => RKP
}
}Run it on a recent phone and the root line gives it away — here's what the app and the logs show:
9:41▪ ▪ ▪ 100%
Key Attestation
Attestation result
● StrongBox
security levelSTRONG_BOX
provisioningRemote (RKP)
Root certificate
key algorithmEC / secp384r1
signatureSHA384withECDSA
leaf notAfter+61 days
adb logcat -s Attest
[0] LEAF sigAlg = SHA256withECDSA
expires = +61 days # RKP
[3] ROOT
sigAlg = SHA384withECDSA # NEW
# pre-2021 device instead shows:
sigAlg = SHA256withRSA # legacyUnchanged
The attestation format stays the same
Almost everything stayed the same. The attestation data format is unchanged, and the X.509 chain still has the same four tiers:
LEAFLeaf certificate — carries the Android Attestation Extension, OID 1.3.6.1.4.1.11129.2.1.17
INTERMEDIATEIntermediate certificate(s) — vendor / RKP
ROOTGoogle root certificate — the only thing that changed
And the attestation extension still provides exactly the same information you parse today:
Security level Software / TEE / StrongBox
Verified boot state Bootloader & boot integrity
OS patch level Security patch date
KeyMint info Module hashes on newer versions
Only the root certificate changed.
The reason
The shift to Remote Key Provisioning
The rotation is the visible edge of a much larger shift in how attestation keys are provisioned.
Old model — factory keys
One permanent attestation key per device
Burned into hardware during manufacturing
Could not be rotated if compromised
New model — RKP
Every app gets its own temporary attestation key
Generated inside TEE / StrongBox
Certified remotely by Google
Rotated every few months
Better securityEasier key rotationLimits impact of a compromised key
RKP chain (new)Factory chain (legacy)ECDSA P-384 rootnew Google rootRSA rootlegacy Google rootIntermediate CARKP-provisionedIntermediate CAfactory chainAttestation leafper-app · rotatedAttestation leafnever rotatedIdentical attestation extensionschema unchanged: security level · boot state · MODULE_HASH
Fig 3. Both chains keep the same structure and the same leaf extension — they diverge only at the root and the key's lifecycle.
The clock
The rotation has already happened
The transition ran on published dates — and two of them are already behind us, which is why this is a fix-now item rather than something to schedule for later.
February 2026
New ECDSA P-384 root introduced. RKP devices begin receiving certificates rooted in it, alongside the existing RSA root.
March 31, 2026
Deadline Google gave developers to have the new root in their trust stores.
April 10, 2026 — cutover
RKP devices switch to the new root exclusively. Any verifier still holding only the old root now rejects RKP-enabled devices — which is most modern phones.
Now
Both roots are live in the wild. Factory-keyed devices keep chaining to the RSA root indefinitely; RKP devices chain to the ECDSA root. Trust both.
Server impact
What your backend must do
The server-side work is small, and all of it is configuration rather than new logic. Servers must:
Trust both Google root certificates (legacy RSA and new ECDSA P-384).
Support ECDSA P-384 signature verification at the anchor.
Keep parsing the same attestation extension — no change here.
Google publishes both trusted roots at a single endpoint — load them from there instead of pinning one in source:
endpointsgoogle attestation trust data
# Both roots (legacy RSA + new ECDSA P-384):
https://android.googleapis.com/attestation/root
# Revoked serials (still check these):
https://android.googleapis.com/attestation/statusStandard PKIX path validation handles both algorithms once both roots are in the trust set:
KotlinAttestationVerifier.kt · server
// Load Google's published roots: legacy RSA + new ECDSA P-384.
// Refresh from the endpoint above — don't hardcode a single root.
fun googleTrustAnchors(rootPems: List<ByteArray>): Set<TrustAnchor> {
val cf = CertificateFactory.getInstance("X.509")
return rootPems.map { pem ->
val root = cf.generateCertificate(pem.inputStream()) as X509Certificate
TrustAnchor(root, null)
}.toSet()
}
fun validate(chainPems: List<ByteArray>, anchors: Set<TrustAnchor>) {
val cf = CertificateFactory.getInstance("X.509")
val chain = chainPems.map { cf.generateCertificate(it.inputStream()) as X509Certificate }
val params = PKIXParameters(anchors).apply { isRevocationEnabled = true }
// PKIX walks the chain to a KNOWN root — handles RSA + ECDSA P-384 alike.
CertPathValidator.getInstance("PKIX").validate(cf.generateCertPath(chain), params)
// Throws CertPathValidatorException if it does not chain to a trusted root.
}Prefer the official library
Google ships a Kotlin verifier (github.com/android/keyattestation) whose trust anchors are generated from the published roots and which checks the short RKP certificate expiry correctly. Using it removes the whole class of "forgot to add the new root" bugs.
Failure modes
Where verification breaks
Every failure mode here is silent — nothing breaks at build time; it breaks at runtime for a growing share of real devices.
Hardcoded root
Server trusts only the old RSA root.
Result RKP devices fail attestation.
RSA-only verification
Verifier only supports RSA signatures.
Result Cannot verify ECDSA P-384 certificates.
Fixed chain length
Verifier expects a specific chain length.
Resul Validation fails if the RKP chain differs.
Critical distinction
A validation failure is not a tamper signal
A certificate-validation failure does not automatically mean the device was tampered with. These are two different states and must be handled separately.
Validation failed ≠ Device tampered
Validation can fail for entirely benign reasons:
Outdated trust store
Missing new Google root
Network / CRL issues
Unsupported ECDSA verification
For RASP & risk engines
Don't let a stale-store or transient failure trip a kill switch or an UNSAFE verdict on its own. Treat "I couldn't validate" and "this is definitely fake" as separate signals, and make the fail-open vs fail-closed choice a deliberate, logged decision.
The fix
What to do
Five concrete changes close the gap:
Add the new root. Support the new ECDSA P-384 Google root.
Keep the old root. Continue trusting the legacy RSA root during (and after) the transition.
Support ECDSA. Ensure the backend can verify ECDSA P-384 signatures.
Don't hardcode. Avoid pinning a single root certificate in source.
Separate the signals. Don't use validation failure alone as a device-risk signal.
Key takeaway
Only the root certificate changed — not the attestation format. Support both Google roots, verify ECDSA P-384, and never treat certificate-validation failures alone as evidence of device tampering.