Mobile App Security

Android Key Attestation Root Certificate Rotation (2026)

Google replaced the certificate at the top of every Android key-attestation chain — RSA out, ECDSA P-384 in — as part of the move to Remote Key Provisioning. Only the root changed, but if your backend doesn't trust both, it's rejecting most modern phones.

By · · 5 Min

Android Key Attestation Root Certificate Rotation (2026)

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.

  1. Genuine hardware

Verify keys were generated inside real secure hardware, not software.

  1. TEE / StrongBox

    Confirm the key is protected by the Trusted Execution Environment or StrongBox.

  2. 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     # legacy

Unchanged

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:

  1. Security level Software / TEE / StrongBox

  2. Verified boot state Bootloader & boot integrity

  3. OS patch level Security patch date

  4. 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

New model — RKP

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/status

Standard 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.

Server trusts only the old RSA root.

Result RKP devices fail attestation.

Verifier only supports RSA signatures.

Result Cannot verify ECDSA P-384 certificates.

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:

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.