{"id":224730,"date":"2026-09-07T16:11:48","date_gmt":"2026-09-07T16:11:48","guid":{"rendered":"https:\/\/neodata.com.uy\/labs\/?p=224730"},"modified":"2026-09-07T16:11:48","modified_gmt":"2026-09-07T16:11:48","slug":"the-certificate-has-two-lists","status":"publish","type":"post","link":"https:\/\/neodata.com.uy\/labs\/the-certificate-has-two-lists\/","title":{"rendered":"The certificate has two lists."},"content":{"rendered":"\n\n<p class=\"wp-block-paragraph\"><strong>Type:<\/strong> Primer<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Category:<\/strong> Research \u2014 Keys &amp; Secure Hardware<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Series:<\/strong> Key containers on mobile devices, part 2 of\n3<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reading time:<\/strong> ~9 min<\/p>\n\n\n\n\n\n<h2 id=\"android-key-attestation-is-the-only-widely-available-way-to-prove-to-a-remote-party-where-a-key-lives--provided-the-verifier-reads-the-right-half-of-it\" class=\"wp-block-heading\">Android\nkey attestation is the only widely available way to prove to a remote\nparty where a key lives \u2014 provided the verifier reads the right half of\nit.<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The previous article ended on a limitation: every local method of\nasking what protects a key runs inside the application's own process,\nand reports whatever the platform says. That is adequate for choosing\nwhat to attempt and useless as evidence.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Key attestation is the mechanism that produces evidence. It matters\nmost for credential issuance \u2014 an issuer signing a device-bound\ncredential such as an mdoc or an SD-JWT VC with key binding needs to\nknow that the holder's key is where the holder's software claims it is \u2014\nand it is regularly consumed incorrectly.<\/p>\n\n\n\n\n\n<h2 id=\"what-the-certificate-is\" class=\"wp-block-heading\">What the certificate is<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Requesting attestation at key generation causes the keystore to\nreturn a certificate chain rather than a bare key. The leaf is a\nstandard X.509 v3 certificate with a fixed serial number of 1 and a\nfixed subject of <code>CN=Android Keystore Key<\/code> \u2014 neither carries\ninformation. The content sits in an extension with OID\n<strong>1.3.6.1.4.1.11129.2.1.17<\/strong>.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">That extension contains a DER-encoded structure:<\/p>\n\n\n\n\n\n<pre class=\"wp-block-code\"><code>KeyDescription ::= SEQUENCE {\n    attestationVersion         INTEGER,\n    attestationSecurityLevel   SecurityLevel,\n    keyMintVersion             INTEGER,\n    keyMintSecurityLevel       SecurityLevel,\n    attestationChallenge       OCTET_STRING,\n    uniqueId                   OCTET_STRING,\n    softwareEnforced           AuthorizationList,\n    hardwareEnforced           AuthorizationList,\n}<\/code><\/pre>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><code>SecurityLevel<\/code> takes the same three values as the\nprevious article: Software, TrustedEnvironment, StrongBox.\n<code>attestationChallenge<\/code> carries a nonce supplied by the\nverifier, which is what makes the certificate a fresh statement rather\nthan a replayable one.<\/p>\n\n\n\n\n\n<h2 id=\"the-two-lists\" class=\"wp-block-heading\">The two lists<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><code>AuthorizationList<\/code> appears twice, and the difference\nbetween the two occurrences is the whole point of the mechanism.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The <strong>hardwareEnforced<\/strong> list contains authorizations\ncollected or generated by code inside the secure hardware, not\ncontrolled by the platform. The TEE or secure element vouches for them\nand enforces them on every use of the key.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The <strong>softwareEnforced<\/strong> list contains authorizations\nenforced by the Android platform. AOSP's own framing is that these are\ntrusted only as long as the device is running an OS that complies with\nthe Android Platform Security Model \u2014 bootloader locked, verified boot\nin the Verified state.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The same tag can appear in either list. Which list it appears in\ndetermines what a verifier may conclude:<\/p>\n\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><code>purpose<\/code>, <code>algorithm<\/code>, <code>keySize<\/code>,\n<code>ecCurve<\/code>, <code>digest<\/code>, <code>origin<\/code>,\n<code>noAuthRequired<\/code>, <code>userAuthType<\/code>,\n<code>rollbackResistance<\/code> in <strong>hardwareEnforced<\/strong>:\nthe secure hardware itself constrains how the key can be used. A key\nmarked sign-only cannot be used to decrypt; a key requiring user\nauthentication cannot be used without it.<\/li>\n\n\n<li>The same tags in <strong>softwareEnforced<\/strong>: the constraint\nexists only as OS policy. It holds while the OS is intact and evaporates\nwhen it is not.<\/li>\n\n<\/ul>\n\n\n\n\n<p class=\"wp-block-paragraph\">Two tags are always software-enforced and this is expected rather\nthan suspicious. <code>attestationApplicationId<\/code> \u2014 which\napplication requested the key \u2014 and <code>creationDateTime<\/code> have\nno meaning inside secure hardware, which knows nothing about packages or\nwall-clock time. They are still useful, but they are platform\nstatements, not hardware statements.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The verifier's obligation follows: <strong>read the security level,\nand separately confirm that each authorization you are relying on\nappears in the hardware-enforced list.<\/strong> A key can carry\n<code>attestationSecurityLevel = TrustedEnvironment<\/code> while an\nauthorization the issuer cares about sits on the software side. Checking\nonly the level, or checking only that a tag is present somewhere,\nproduces a conclusion the certificate does not support.<\/p>\n\n\n\n\n\n<h2 id=\"the-rest-of-the-structure\" class=\"wp-block-heading\">The rest of the structure<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Root of trust.<\/strong><\/p>\n\n\n\n\n\n<pre class=\"wp-block-code\"><code>RootOfTrust ::= SEQUENCE {\n    verifiedBootKey    OCTET_STRING,\n    deviceLocked       BOOLEAN,\n    verifiedBootState  VerifiedBootState,\n    verifiedBootHash   OCTET_STRING\n}<\/code><\/pre>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><code>VerifiedBootState<\/code> is Verified, SelfSigned, Unverified or\nFailed. For an issuer, <code>verifiedBootState = Verified<\/code> and\n<code>deviceLocked = true<\/code> are the conditions under which the\nsoftware-enforced list means anything at all.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">A parsing note: <code>verifiedBootHash<\/code> is absent in\nattestation schema versions 1 and 2. A parser written against the\ncurrent schema will fail on older devices.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Patch levels.<\/strong> <code>osVersion<\/code>,\n<code>osPatchLevel<\/code> (as YYYYMM), <code>vendorPatchLevel<\/code>,\n<code>bootPatchLevel<\/code>. These let an issuer set a floor on how\nstale a device's software may be.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Identity fields.<\/strong> <code>attestationIdBrand<\/code>,\n<code>Device<\/code>, <code>Product<\/code>, <code>Manufacturer<\/code>,\n<code>Model<\/code>, <code>Serial<\/code>, <code>Imei<\/code>,\n<code>Meid<\/code>, and a second IMEI field from KeyMint 3. These require\nspecific device support, are gated by the platform, and are frequently\nunavailable to third-party applications. Do not design an issuance flow\nthat depends on them without confirming availability on the target\ndevice population.<\/p>\n\n\n\n\n\n<h2 id=\"validating-the-chain\" class=\"wp-block-heading\">Validating the chain<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The verifier builds the chain from leaf to root and confirms the root\nis a published Google attestation root.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">There are currently two. The long-standing root is RSA-signed. A new\nECDSA P-384 root was introduced as part of the transition to remote key\nprovisioning and began signing chains on 1 February 2026. <strong>A\nverifier must trust both<\/strong>, and can distinguish them by the\nroot's signature algorithm: <code>SHA384withECDSA<\/code> for the new\none, <code>SHA256withRSA<\/code> for the legacy one. Whether and when\ndevices chain exclusively to the new root is a transition detail to\nconfirm against Google's current documentation rather than assume.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Revocation status is published as JSON at\n<code>https:\/\/android.googleapis.com\/attestation\/status<\/code>, keyed on\ncertificate serial number. It changes, so a verifier fetches it rather\nthan embedding it.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Google publishes a Kotlin verification library,\n<code>android\/keyattestation<\/code>. Using it is preferable to writing\nan ASN.1 parser for a structure with version-dependent fields and two\nlists that are easy to confuse.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A correction worth stating explicitly<\/strong>, because the\ntwo cases are widely conflated. Google's documentation observes that a\ndevice <em>without<\/em> hardware attestation is not thereby\nuntrustworthy \u2014 the absence of attestation is a neutral signal, not a\nnegative one, because plenty of legitimate devices simply lack the\ncapability. That reasoning does not extend to a <em>revoked<\/em>\ncertificate. A revoked certificate must fail validation of the\nattestation evidence it carries. The two situations are \"we have no\nevidence\" and \"the evidence we have is void,\" and they call for\ndifferent decisions.<\/p>\n\n\n\n\n\n<h2 id=\"remote-key-provisioning\" class=\"wp-block-heading\">Remote key provisioning<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Historically, attestation keys were injected at the factory in\nbatches, with a single key shared across at least 100,000 devices so\nthat the attestation key could not serve as a device identifier. Remote\nkey provisioning replaces this with per-device keys provisioned over the\nair, carrying short-lived certificates.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">The practical consequences for an issuer:<\/p>\n\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Two chain structures coexist. A factory-keyed device presents a\nchain through a vendor intermediate to the legacy root, with a\nlong-lived leaf. An RKP device presents a rotated leaf through a\nprovisioned intermediate to the newer root.<\/li>\n\n\n<li>Certificate validity windows are short. An issuer cannot cache a\nchain indefinitely and treat it as current.<\/li>\n\n\n<li>The extension schema is identical in both cases. Only the roots and\nthe key lifecycle differ.<\/li>\n\n<\/ul>\n\n\n\n\n<p class=\"wp-block-paragraph\">The exact status of RKP across releases \u2014 supported, required to be\nimplemented, or the only mechanism for devices launching on a given\nversion \u2014 is stated differently in different AOSP documents and is worth\nreconciling against current documentation before writing it into a\nspecification.<\/p>\n\n\n\n\n\n<h2 id=\"what-attestation-does-not-prove\" class=\"wp-block-heading\">What attestation does not\nprove<\/h2>\n\n\n\n\n\n<p class=\"wp-block-paragraph\">Five limits, each of which has produced a wrong assumption\nsomewhere:<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It does not prove the requesting application is\ngenuine.<\/strong> <code>attestationApplicationId<\/code> is\nsoftware-enforced. On an intact device it identifies the caller; on a\nmodified one it does not.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It does not prove the secure hardware is sound.<\/strong>\nAttestation is a statement made <em>by<\/em> the TEE about itself.\nPublished work has extracted hardware-protected keys from a major\nvendor's TEE keymaster implementation through IV reuse, together with a\ndowngrade attack that forced patched devices back into vulnerable\nbehaviour. A certificate from a broken TEE is a correctly signed\nstatement of something false.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It does not prove the chain is real on every device.<\/strong>\nDevices whose attestation keyboxes have leaked can be used to forge\nattestations, and tooling to do so circulates. This is a large part of\nwhy revocation checking and RKP exist.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It does not root in Google on every device.<\/strong> Hardware\nattestation is a standard AOSP capability and does not require Google\nMobile Services to <em>produce<\/em> a certificate. But on a device\nwithout GMS certification, the chain roots in the manufacturer's own\nattestation root. A verifier that only trusts Google roots will reject\nan otherwise perfectly hardware-backed key. Supporting such devices\nmeans maintaining additional trust anchors, obtained through each\nmanufacturer's own channels.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It does not prove anything about enrolment.<\/strong> The\ncertificate says where a key is. It says nothing about whether the\nperson presenting it is who the credential will claim they are.<\/p>\n\n\n\n\n\n<h2 id=\"the-verifiers-checklist\" class=\"wp-block-heading\">The verifier's checklist<\/h2>\n\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>Generate the key with a server-supplied challenge; confirm the\nchallenge in the certificate matches.<\/li>\n\n\n<li>Build and validate the chain to a trusted root; support both Google\nroots, plus any manufacturer roots your device population requires.<\/li>\n\n\n<li>Check the revocation status list, freshly fetched.<\/li>\n\n\n<li>Check certificate validity windows.<\/li>\n\n\n<li>Read <code>attestationSecurityLevel<\/code> and\n<code>keyMintSecurityLevel<\/code>.<\/li>\n\n\n<li>Confirm every authorization you rely on is in the\n<strong>hardware-enforced<\/strong> list.<\/li>\n\n\n<li>Confirm <code>verifiedBootState = Verified<\/code> and\n<code>deviceLocked = true<\/code> before granting any weight to\nsoftware-enforced content.<\/li>\n\n\n<li>Apply a patch-level floor appropriate to the credential's\nvalue.<\/li>\n\n\n<li>Record the whole chain and the parsed result. When a decision is\nquestioned years later, the certificate is the only evidence that\nsurvives.<\/li>\n\n<\/ol>\n\n\n\n\n<p class=\"wp-block-paragraph\">Steps 6 and 7 are the ones most often skipped, and skipping them\nturns a proof into a formality.<\/p>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Series: Key containers on mobile devices<\/strong><\/p>\n\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><a href=\"https:\/\/neodata.com.uy\/labs\/what-hardware-backed-means-on-android\/\">Part\n1: What \"hardware-backed\" means on Android.<\/a><\/li>\n\n\n<li><a href=\"https:\/\/neodata.com.uy\/labs\/seven-containers-ranked-by-what-you-can-prove\/\">Part\n3: Seven containers, ranked by what you can prove.<\/a><\/li>\n\n<\/ul>\n\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n\n\n<h2 id=\"references\" class=\"wp-block-heading\">References<\/h2>\n\n\n\n\n<ol class=\"wp-block-list\">\n\n<li>Android Open Source Project. <em>Key and ID attestation.<\/em> <a href=\"https:\/\/source.android.com\/docs\/security\/features\/keystore\/attestation\">https:\/\/source.android.com\/docs\/security\/features\/keystore\/attestation<\/a><\/li>\n\n\n<li>Android Developers. <em>Verify hardware-backed key pairs with key\nattestation.<\/em> <a href=\"https:\/\/developer.android.com\/privacy-and-security\/security-key-attestation\">https:\/\/developer.android.com\/privacy-and-security\/security-key-attestation<\/a><\/li>\n\n\n<li>Google. <em>android\/keyattestation<\/em> verification library. <a href=\"https:\/\/github.com\/android\/keyattestation\">https:\/\/github.com\/android\/keyattestation<\/a><\/li>\n\n\n<li>Android Open Source Project. <em>Remote key provisioning.<\/em> <a href=\"https:\/\/source.android.com\/docs\/core\/ota\/modular-system\/remote-key-provisioning\">https:\/\/source.android.com\/docs\/core\/ota\/modular-system\/remote-key-provisioning<\/a><\/li>\n\n\n<li>Shakevsky A, Ronen E, Wool A (2022). <em>Trust Dies in Darkness:\nShedding Light on Samsung's TrustZone Keymaster Design.<\/em> 31st USENIX\nSecurity Symposium, pp. 251\u2013268. <a href=\"https:\/\/eprint.iacr.org\/2022\/208\">IACR ePrint 2022\/208<\/a>.<\/li>\n\n\n<li>GrapheneOS. <em>Attestation compatibility guide.<\/em> <a href=\"https:\/\/grapheneos.org\/articles\/attestation-compatibility-guide\">https:\/\/grapheneos.org\/articles\/attestation-compatibility-guide<\/a><\/li>\n\n<\/ol>\n\n","protected":false},"excerpt":{"rendered":"<p>Android key attestation records a key&#8217;s security level and separates authorizations enforced by hardware from those enforced by software. A verifier must validate the certificate chain, revocation status, boot state and the specific list containing every relied-upon constraint.<\/p>\n","protected":false},"author":2,"featured_media":224725,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10],"tags":[45,50,51,47,42,49],"nlabs_article_type":[36],"class_list":["post-224730","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-research","tag-android-keystore","tag-asn-1","tag-key-attestation","tag-keymint","tag-remote-key-provisioning","tag-x-509","nlabs_article_type-primer"],"_links":{"self":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/224730","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/comments?post=224730"}],"version-history":[{"count":1,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/224730\/revisions"}],"predecessor-version":[{"id":224772,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/posts\/224730\/revisions\/224772"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/media\/224725"}],"wp:attachment":[{"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/media?parent=224730"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/categories?post=224730"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/tags?post=224730"},{"taxonomy":"nlabs_article_type","embeddable":true,"href":"https:\/\/neodata.com.uy\/labs\/wp-json\/wp\/v2\/nlabs_article_type?post=224730"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}