<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Quarkslab's blog - keystore</title><link href="http://blog.quarkslab.com/" rel="alternate"></link><link href="http://blog.quarkslab.com/feeds/keystore.rss.xml" rel="self"></link><id>http://blog.quarkslab.com/</id><updated>2026-08-11T00:00:00+02:00</updated><entry><title>Bypassing Android Hardware Attestation from the Analyst's Chair</title><link href="http://blog.quarkslab.com/bypassing-android-hardware-attestation.html" rel="alternate"></link><published>2026-08-11T00:00:00+02:00</published><updated>2026-08-11T00:00:00+02:00</updated><author><name>Eric Le Guevel</name></author><id>tag:blog.quarkslab.com,2026-08-11:/bypassing-android-hardware-attestation.html</id><summary type="html">&lt;p&gt;Hardware key attestation lets an Android app prove to its backend that a key lives in secure hardware on a locked, verified device. It is also the wall that stops a security analyst working on a rooted phone. This article opens the mechanism from the analyst's chair, from the certificate chain and the attestation extension down to the root of trust, then shows a simple bypass that never touches the secure hardware. We relay the attestation to a clean device and splice a genuine chain back into the target app with a Frida hook. A companion repository ships the validation backend, the demo apps and the instrumentation, so the whole setup can be run and inspected rather than taken on faith.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;link href="resources/2026-08-11_bypassing-android-hardware-attestation/styles.css" rel="stylesheet"/&gt;&lt;/p&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;More and more Android apps want to know one thing before they let us in: is this device trustworthy? Banking apps, payment wallets, identity SDKs and a growing crowd of others now ask the operating system to prove that the phone is a genuine device running an untampered boot chain. When the answer is no, the app degrades, refuses a feature, or simply shuts the door.&lt;/p&gt;
&lt;p&gt;For a security analyst, this is a familiar wall. Our mission phone is usually rooted. That is not an accident, it is the job. Root is what lets us hook, trace, dump and read the app while it runs. The moment the target app leans on hardware attestation, that same rooted state becomes the reason we get locked out. The app asks the hardware for a signed statement about the device, the hardware honestly reports a broken root of trust, and the backend rejects us. Nothing is misbehaving. The system is working exactly as designed, and that is precisely the problem for us.&lt;/p&gt;
&lt;p&gt;This article has two goals, in order.&lt;/p&gt;
&lt;p&gt;First, we explain Android hardware attestation end to end. What problem it solves, where the trust actually comes from, and what the device really signs and sends. We open the certificate chain, walk through the attestation extension, and look at the fields that a backend inspects to make its decision.&lt;/p&gt;
&lt;p&gt;Second, we present a simple bypass built on instrumentation. We stay away from the hard problem. We do not attack the secure hardware, we do not extract keys, and we do not need a leaked keybox. Instead we sit where an analyst already sits, inside the running process, and we redirect the attestation request. A clean, unmodified phone answers it with its own healthy hardware, and its signed statement is relayed back to unblock the analysis on the rooted device. The trick is not breaking the crypto. The trick is deciding who gets asked.&lt;/p&gt;
&lt;p&gt;The whole setup is reproducible. A companion repository ships the validation backend, the demo apps, and the Frida instrumentation used for the relay.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/quarkslab/android-hardware-attestation-demo"&gt;https://github.com/quarkslab/android-hardware-attestation-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A note on scope. Attestation on Android has moved a lot over the years, and it is still moving. We target modern devices, Android 13 and later, and we call out per-version behavior wherever it diverges. StrongBox appears where it changes the picture, but it is not our main axis.&lt;/p&gt;
&lt;p&gt;The milestones, at a glance &lt;sup id="fnref:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:rkp"&gt;&lt;a class="footnote-ref" href="#fn:rkp"&gt;9&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;th&gt;Secure component&lt;/th&gt;
&lt;th&gt;Milestone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;Keymaster 2&lt;/td&gt;
&lt;td&gt;Key attestation introduced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;Keymaster 3&lt;/td&gt;
&lt;td&gt;ID attestation added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;KeyMint, renamed from Keymaster&lt;/td&gt;
&lt;td&gt;RKP lands in AOSP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;KeyMint&lt;/td&gt;
&lt;td&gt;RKP becomes an updatable module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;KeyMint&lt;/td&gt;
&lt;td&gt;RKP only, factory keys phased out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RKP here is Remote Key Provisioning, the shift from attestation keys injected at the factory to short-lived keys issued per device by a Google backend. We come back to it in detail in the provisioning section.&lt;/p&gt;
&lt;p&gt;With the frame set, we start where the app starts, by asking the hardware to vouch for the phone.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Two panels contrasting the nominal case and the analyst's wall. The same attestation request runs the same mechanism on a healthy phone and on a rooted phone. Only the RootOfTrust the hardware honestly reports differs, Verified against Unverified, so the backend grants access in one case and rejects it in the other. Both verdicts are the expected behavior." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig1-attestation-nominal-vs-wall.svg" width="100%"/&gt;
&lt;img alt="Two panels contrasting the nominal case and the analyst's wall. The same attestation request runs the same mechanism on a healthy phone and on a rooted phone. Only the RootOfTrust the hardware honestly reports differs, Verified against Unverified, so the backend grants access in one case and rejects it in the other. Both verdicts are the expected behavior." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig1-attestation-nominal-vs-wall-dark.svg" width="100%"/&gt;&lt;/p&gt;
&lt;h2 id="android-hardware-attestation-mechanism"&gt;Android hardware attestation mechanism&lt;/h2&gt;
&lt;p&gt;Our starting point is simple. We run an application on a rooted device, and at some point the analysis stops. A request fails, a feature refuses to load, a login never completes. Somewhere in the app there is a check that decides our device is not trustworthy, and it is right. Before we can defeat that check we need to understand what it actually measures. This part answers that question. By the end of it the reader should know what hardware attestation proves, how the proof is built and carried, and where its guarantees stop.&lt;/p&gt;
&lt;p&gt;We keep the analyst's eye on it: we want to see what the service sees when it looks at our device, so that later we can decide what to change and what it costs us.&lt;/p&gt;
&lt;h3 id="what-does-attestation-actually-prove"&gt;What does attestation actually prove?&lt;/h3&gt;
&lt;p&gt;Android Keystore lets an app create and use cryptographic keys without ever touching the raw key material. On a device with hardware backing, the key lives inside secure hardware and the platform only ever sends it commands. The problem is trust. Before attestation, an app or a remote server had no reliable way to know whether a given Keystore key was really hardware-backed or just a software key pretending to be one. The Keystore daemon loaded whatever Keymaster HAL was present, the vendor hardware abstraction layer that talks to the secure component, and believed what the HAL claimed about hardware backing &lt;sup id="fnref2:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Key attestation was introduced to close that gap. It was added in Android 7.0 with Keymaster 2, and ID attestation followed in Android 8.0 with Keymaster 3. Its goal is to let a remote party reliably determine three things about a key pair:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The private key really lives in hardware-backed storage.&lt;/li&gt;
&lt;li&gt;The key has known properties, such as its algorithm, size, and purpose.&lt;/li&gt;
&lt;li&gt;Known constraints govern how the key can be used.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keymaster and KeyMint.&lt;/strong&gt; Keymaster is the historical name of the secure component that guards Keystore keys. It was renamed KeyMint in Android 12. We use KeyMint for current behavior and Keymaster only when talking about older devices.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The output of attestation is an X.509 certificate that describes the attested key and the state of the device at key generation time, signed by a key the device did not choose and cannot forge. The rest of this part is about the content of that certificate and the chain that carries it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="End to end attestation flow. The application calls generateKey with a challenge, KeyMint inside the TEE or StrongBox generates the key and signs the attestation, and the certificate chain goes back to the app and on to the backend, which validates it up to the Google Hardware Attestation Root. The private attestation key stays inside the secure hardware and never crosses its boundary." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig2-attestation-end-to-end.svg" width="100%"/&gt;
&lt;img alt="End to end attestation flow. The application calls generateKey with a challenge, KeyMint inside the TEE or StrongBox generates the key and signs the attestation, and the certificate chain goes back to the app and on to the backend, which validates it up to the Google Hardware Attestation Root. The private attestation key stays inside the secure hardware and never crosses its boundary." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig2-attestation-end-to-end-dark.svg" width="100%"/&gt;&lt;/p&gt;
&lt;h3 id="where-do-the-keys-live-tee-and-strongbox"&gt;Where do the keys live: TEE and StrongBox&lt;/h3&gt;
&lt;p&gt;Attestation only means something if we know where the attested key is stored and how well that location resists attack. The certificate states this explicitly through a &lt;code&gt;SecurityLevel&lt;/code&gt; value &lt;sup id="fnref3:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;SecurityLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ENUMERATED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;Software&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;TrustedEnvironment&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nf"&gt;StrongBox&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ASN.1 and DER.&lt;/strong&gt; The structures in this article are written in ASN.1, a notation for describing typed data such as sequences, enumerations and octet strings. DER is the byte encoding of that data as it actually sits inside the certificate. When we say a field is DER-encoded, we mean it is packed into bytes that a parser walks one field at a time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The three levels describe how resilient the key and its attestation are to attack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Software&lt;/code&gt;. Secure only as long as the Android system itself is intact, meaning the bootloader is locked and Verified Boot reports a verified state. There is no hardware guarantee here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TrustedEnvironment&lt;/code&gt;. Enforced by the device Trusted Execution Environment. Secure as long as the TEE is not compromised. The isolation requirements are set out in the Compatibility Definition Document, section 9.11 &lt;sup id="fnref:cdd"&gt;&lt;a class="footnote-ref" href="#fn:cdd"&gt;3&lt;/a&gt;&lt;/sup&gt;. A TEE is highly resistant to remote compromise and moderately resistant to a direct hardware attack &lt;sup id="fnref12:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StrongBox&lt;/code&gt;. Enforced by a dedicated secure element, similar to a hardware security module, with its own CPU and storage &lt;sup id="fnref:keystore-hsm"&gt;&lt;a class="footnote-ref" href="#fn:keystore-hsm"&gt;4&lt;/a&gt;&lt;/sup&gt;. Requirements are in CDD section 9.11.2 &lt;sup id="fnref2:cdd"&gt;&lt;a class="footnote-ref" href="#fn:cdd"&gt;3&lt;/a&gt;&lt;/sup&gt;. StrongBox is highly resistant both to remote compromise and to direct hardware attack such as physical tampering and side channel analysis &lt;sup id="fnref13:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TEE and StrongBox.&lt;/strong&gt; A TEE is a secure world that runs beside Android on the same application processor, isolated by hardware (for example ARM TrustZone). A StrongBox is a physically separate chip. The practical difference for us is the cost of an attack. A TEE shares silicon with the main OS, a StrongBox does not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A check that only requires &lt;code&gt;Software&lt;/code&gt; level is weak and often defeated without touching hardware at all. A check that requires &lt;code&gt;TrustedEnvironment&lt;/code&gt; or &lt;code&gt;StrongBox&lt;/code&gt; is the interesting case, and the one this article addresses.&lt;/p&gt;
&lt;h3 id="the-attestation-certificate-chain"&gt;The attestation certificate chain&lt;/h3&gt;
&lt;p&gt;When an app generates a key with an attestation challenge, KeyMint returns not a single certificate but a chain. The app reads it with &lt;code&gt;KeyStore.getCertificateChain()&lt;/code&gt; and, in a correct design, forwards it to a server it trusts rather than validating it locally &lt;sup id="fnref:verify-attestation"&gt;&lt;a class="footnote-ref" href="#fn:verify-attestation"&gt;5&lt;/a&gt;&lt;/sup&gt;. The reason is direct. If the device OS is compromised, an on-device check can be made to trust anything.&lt;/p&gt;
&lt;p&gt;The chain is ordered. Entry 0 is the leaf, the attestation certificate itself. It certifies the attested public key and carries the attestation extension. Each following certificate signs the previous one, up to a root. On devices that shipped with Google Play and launched on Android 7.0 or later, that root is the Google Hardware Attestation Root, and the set of valid roots is published as a JSON array so a backend can pin them &lt;sup id="fnref2:verify-attestation"&gt;&lt;a class="footnote-ref" href="#fn:verify-attestation"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The leaf is a standard X.509 v3 certificate, but several of its fields are fixed and checked exactly by the Compatibility Test Suite (CTS) &lt;sup id="fnref4:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serialNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INTEGER 1, identical on every attestation certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CN = "Android Keystore Key", identical on every certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;validity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Derived from the key &lt;code&gt;ACTIVE_DATETIME&lt;/code&gt; and &lt;code&gt;USAGE_EXPIRE_DATETIME&lt;/code&gt; tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extensions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contains the attestation extension, OID &lt;code&gt;1.3.6.1.4.1.11129.2.1.17&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The fixed serial number and subject are a common trap. They are not identifiers. Every attestation leaf on every device carries the same two values. The identity and the state of the device are not in these fields. They are inside the attestation extension.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The attestation certificate chain as a stack. The Google Hardware Attestation Root sits at the top, then one or more intermediate certificates, then the leaf certificate at the bottom with its attestation extension highlighted, OID 1.3.6.1.4.1.11129.2.1.17. Each certificate signs the one below it, and entry 0 is the leaf." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig3-attestation-certificate-chain.svg" width="70%"/&gt;
&lt;img alt="The attestation certificate chain as a stack. The Google Hardware Attestation Root sits at the top, then one or more intermediate certificates, then the leaf certificate at the bottom with its attestation extension highlighted, OID 1.3.6.1.4.1.11129.2.1.17. Each certificate signs the one below it, and entry 0 is the leaf." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig3-attestation-certificate-chain-dark.svg" width="70%"/&gt;&lt;/p&gt;
&lt;h3 id="inside-the-attestation-extension-keydescription"&gt;Inside the attestation extension: KeyDescription&lt;/h3&gt;
&lt;p&gt;The attestation extension has OID &lt;code&gt;1.3.6.1.4.1.11129.2.1.17&lt;/code&gt;, an object identifier whose &lt;code&gt;1.3.6.1.4.1.11129&lt;/code&gt; arc is reserved to Google. Its content is a DER-encoded &lt;code&gt;KeyDescription&lt;/code&gt; structure that holds everything the verifier cares about. Here is the current schema, version 500, as shipped with KeyMint 5 &lt;sup id="fnref5:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;KeyDescription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SEQUENCE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;attestationVersion&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="nv"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;attestationSecurityLevel&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nv"&gt;SecurityLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;keyMintVersion&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nv"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;keyMintSecurityLevel&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nv"&gt;SecurityLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;attestationChallenge&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nv"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;uniqueId&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="nv"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;softwareEnforced&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nv"&gt;AuthorizationList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;hardwareEnforced&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nv"&gt;AuthorizationList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two fields deserve attention right away.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;attestationChallenge&lt;/code&gt; is the nonce. It carries back the exact challenge the caller passed at key generation time. A backend generates a fresh random challenge, sends it to the client, and later checks that the same bytes appear here. This is what makes the attestation fresh and binds it to one verification exchange, not a replayed capture.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;attestationVersion&lt;/code&gt; tells the parser which schema to expect, and by extension which KeyMint or Keymaster generation produced the certificate &lt;sup id="fnref6:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;KeyMint or Keymaster version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Keymaster 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Keymaster 3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Keymaster 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Keymaster 4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;KeyMint 1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;KeyMint 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;KeyMint 3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;KeyMint 4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;KeyMint 5.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The two &lt;code&gt;AuthorizationList&lt;/code&gt; fields are the heart of the description, and the split between them is the whole point of hardware attestation &lt;sup id="fnref7:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;softwareEnforced&lt;/code&gt; holds properties enforced by the Android platform code. They can be trusted only as long as the OS complies with the Android Platform Security Model, that is bootloader locked and Verified Boot in a verified state.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hardwareEnforced&lt;/code&gt; holds properties enforced by the TEE or StrongBox. They are collected inside the secure hardware and are not controlled by the platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything a backend really relies on should be read from &lt;code&gt;hardwareEnforced&lt;/code&gt;, because a rooted platform can shape &lt;code&gt;softwareEnforced&lt;/code&gt; freely. When we later look for what to attack, we look at what the backend trusts, and a well written backend trusts the hardware-enforced side.&lt;/p&gt;
&lt;h3 id="the-root-of-trust"&gt;The root of trust&lt;/h3&gt;
&lt;p&gt;The single most important entry in &lt;code&gt;hardwareEnforced&lt;/code&gt; is the root of trust, carried under tag &lt;code&gt;[704]&lt;/code&gt; &lt;sup id="fnref8:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;RootOfTrust&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SEQUENCE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;verifiedBootKey&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;deviceLocked&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nx"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;verifiedBootState&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;VerifiedBootState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;verifiedBootHash&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="nx"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;VerifiedBootState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ENUMERATED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Verified&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;SelfSigned&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Unverified&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Failed&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These four fields are populated by the secure hardware from measurements taken during Verified Boot, before Android runs. They describe the boot chain, not the running system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;verifiedBootKey&lt;/code&gt; is the hash of the key used to verify the boot image.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deviceLocked&lt;/code&gt; is true when the bootloader is locked.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verifiedBootHash&lt;/code&gt; is a digest of the verified boot state.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verifiedBootState&lt;/code&gt; is the summary the app is really after.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The four &lt;code&gt;VerifiedBootState&lt;/code&gt; values map to the four Verified Boot device states that the bootloader can set &lt;sup id="fnref:verified-boot"&gt;&lt;a class="footnote-ref" href="#fn:verified-boot"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:boot-flow"&gt;&lt;a class="footnote-ref" href="#fn:boot-flow"&gt;7&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VerifiedBootState&lt;/th&gt;
&lt;th&gt;Verified Boot color&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Verified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Green&lt;/td&gt;
&lt;td&gt;Device locked, full chain of trust from a hardware root, stock OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SelfSigned&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yellow&lt;/td&gt;
&lt;td&gt;Device locked, but boot verified against a user-supplied root of trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unverified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Orange&lt;/td&gt;
&lt;td&gt;Device unlocked, no boot verification enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Red&lt;/td&gt;
&lt;td&gt;Verification failed, no valid OS found&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is the field that blocks us on the mission device. A rooted phone almost always reports &lt;code&gt;Unverified&lt;/code&gt; because unlocking the bootloader is what root usually requires. A backend that demands &lt;code&gt;Verified&lt;/code&gt; and reads it from the hardware-enforced root of trust will reject that device, and no amount of software patching on the same device changes the four bytes, because they come from below Android.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verified Boot.&lt;/strong&gt; Verified Boot checks each stage of boot against a key before running it, from the bootloader up to the system image. The result is frozen into the root of trust that KeyMint later signs. This is why the state cannot be edited after the fact from userspace. It was measured before userspace existed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="how-does-the-server-verify-all-this"&gt;How does the server verify all this?&lt;/h3&gt;
&lt;p&gt;A correct verification is a chain of independent checks, done off device &lt;sup id="fnref3:verify-attestation"&gt;&lt;a class="footnote-ref" href="#fn:verify-attestation"&gt;5&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the certificate chain and verify each signature up to the root.&lt;/li&gt;
&lt;li&gt;Confirm the root is a trusted Google Hardware Attestation Root from the published set.&lt;/li&gt;
&lt;li&gt;Check the certificate validity dates.&lt;/li&gt;
&lt;li&gt;Extract the attestation extension and confirm &lt;code&gt;attestationChallenge&lt;/code&gt; equals the nonce the server issued.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;securityLevel&lt;/code&gt;, the key properties and the root of trust from the extension, from the &lt;code&gt;hardwareEnforced&lt;/code&gt; list, and apply the policy the service wants.&lt;/li&gt;
&lt;li&gt;Check that no certificate in the chain has been revoked.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The last step relies on a revocation list that Google publishes at a single URL, &lt;code&gt;https://android.googleapis.com/attestation/status&lt;/code&gt;, as JSON &lt;sup id="fnref:trust-but-verify"&gt;&lt;a class="footnote-ref" href="#fn:trust-but-verify"&gt;8&lt;/a&gt;&lt;/sup&gt;. Only keys with a non-valid status appear, so it is not a full list of issued keys. An entry carries a status, &lt;code&gt;REVOKED&lt;/code&gt; for a permanent removal or &lt;code&gt;SUSPENDED&lt;/code&gt; for a temporary one, and a reason such as &lt;code&gt;KEY_COMPROMISE&lt;/code&gt;, &lt;code&gt;CA_COMPROMISE&lt;/code&gt;, &lt;code&gt;SUPERSEDED&lt;/code&gt; or &lt;code&gt;SOFTWARE_FLAW&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If any step fails, the attestation is not trusted. Note that every one of these checks runs on the server. The device only produces the evidence.&lt;/p&gt;
&lt;h3 id="where-do-attestation-keys-come-from-factory-keys-and-rkp"&gt;Where do attestation keys come from: factory keys and RKP&lt;/h3&gt;
&lt;p&gt;The whole chain rests on the private key that signs the leaf. Two provisioning models exist, and the difference matters for the limits we discuss next.&lt;/p&gt;
&lt;p&gt;The older model is factory provisioning, also called batch keys. An attestation key and its certificate are injected at manufacturing time and shared across a batch of devices. In that model the leaf &lt;code&gt;issuer&lt;/code&gt; matches the subject of the batch attestation key. One private key covers many devices, which is efficient but fragile. If that key leaks, every device in the batch can be impersonated, and the only response is to revoke it through the status list &lt;sup id="fnref2:trust-but-verify"&gt;&lt;a class="footnote-ref" href="#fn:trust-but-verify"&gt;8&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The newer model is Remote Key Provisioning, RKP, part of AOSP since Android 12 &lt;sup id="fnref2:rkp"&gt;&lt;a class="footnote-ref" href="#fn:rkp"&gt;9&lt;/a&gt;&lt;/sup&gt;. Here the factory does not program long-lived signing keys onto the device. Instead the device proves the health of its own key generation to a Google backend, which issues short-lived certificates per device. Two properties follow directly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Revocation can target a single device rather than a whole batch.&lt;/li&gt;
&lt;li&gt;Certificates are short-lived, so the validity period must be checked, which shortens the window a compromise stays useful.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Android 14 introduced RKP as an updatable module so the service can be improved without a full OS update &lt;sup id="fnref3:rkp"&gt;&lt;a class="footnote-ref" href="#fn:rkp"&gt;9&lt;/a&gt;&lt;/sup&gt;. For devices that launch on Android 16, the system supports only RKP, and factory keys are phased out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Factory keys and RKP.&lt;/strong&gt; Factory keys are like one master badge shared by a thousand employees. RKP is like issuing each employee a personal badge that expires quickly. If one badge leaks, you cancel one badge, not the master.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="what-attestation-does-not-prove"&gt;What attestation does not prove&lt;/h3&gt;
&lt;p&gt;Attestation is strong, but its guarantees are narrow and precise. Reading them precisely is what makes a bypass possible.&lt;/p&gt;
&lt;p&gt;First, every hardware guarantee is conditional on the secure hardware not being compromised. The &lt;code&gt;SecurityLevel&lt;/code&gt; definitions say so in as many words. &lt;code&gt;TrustedEnvironment&lt;/code&gt; is secure as long as the TEE is not compromised, &lt;code&gt;StrongBox&lt;/code&gt; as long as StrongBox is not compromised. Attestation does not prove the TEE is intact, it assumes it. TEE and firmware key extraction is exactly the class of failure the revocation list exists to contain &lt;sup id="fnref3:trust-but-verify"&gt;&lt;a class="footnote-ref" href="#fn:trust-but-verify"&gt;8&lt;/a&gt;&lt;/sup&gt;. It is not hypothetical. Researchers have pulled hardware-backed ECDSA keys out of Qualcomm's TrustZone through a microarchitectural side channel &lt;sup id="fnref:ncc-heist"&gt;&lt;a class="footnote-ref" href="#fn:ncc-heist"&gt;18&lt;/a&gt;&lt;/sup&gt;, and broken the key protection in Samsung's TrustZone Keymaster design &lt;sup id="fnref:samsung-keymaster"&gt;&lt;a class="footnote-ref" href="#fn:samsung-keymaster"&gt;19&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Second, a leaked attestation private key breaks the model for as long as it takes to detect and revoke it. Batch keys make this worse because one leak covers many devices. This is a known and documented failure mode, and it is the reason RKP and revocation exist at all &lt;sup id="fnref4:trust-but-verify"&gt;&lt;a class="footnote-ref" href="#fn:trust-but-verify"&gt;8&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref4:rkp"&gt;&lt;a class="footnote-ref" href="#fn:rkp"&gt;9&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Third, and this is the point we build on, attestation binds a key to a healthy device, but it does not bind that key to this device or to the process that is asking. The certificate proves that some device with a locked bootloader and a verified boot state generated a key and signed a challenge. It says nothing about where that device is, or whether the app talking to the backend is running on the same hardware that produced the evidence. Nothing in the &lt;code&gt;KeyDescription&lt;/code&gt; ties the attestation to a network location, a session, or a running process context.&lt;/p&gt;
&lt;p&gt;That gap is the whole opportunity for the analyst. If our mission device cannot produce a &lt;code&gt;Verified&lt;/code&gt; attestation, but a healthy device can, and the backend cannot tell which physical device the evidence came from, then the evidence can be generated in one place and presented from another. This is the idea behind the relay technique that the security community calls Remote Key Attestation, documented from the defensive side by Guardsquare &lt;sup id="fnref:guardsquare-rka"&gt;&lt;a class="footnote-ref" href="#fn:guardsquare-rka"&gt;1&lt;/a&gt;&lt;/sup&gt;. The next section takes the analyst view of the same idea and builds a reproducible setup around it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The gap the relay exploits. A healthy legitimate device produces a valid attestation with RootOfTrust Verified. A dashed arrow relays that attestation to the rooted mission device, which presents it to the backend. The backend validates the chain but cannot tell which physical device produced the evidence, so it grants access. Attestation binds a key to a healthy device, not to this device, this session, or the process asking." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig4-attestation-relay-gap.svg" width="100%"/&gt;
&lt;img alt="The gap the relay exploits. A healthy legitimate device produces a valid attestation with RootOfTrust Verified. A dashed arrow relays that attestation to the rooted mission device, which presents it to the backend. The backend validates the chain but cannot tell which physical device produced the evidence, so it grants access. Attestation binds a key to a healthy device, not to this device, this session, or the process asking." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig4-attestation-relay-gap-dark.svg" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;That is the mechanism end to end. Attestation is a signed statement, produced in secure hardware, describing a key and the boot state of the device that made it, verified off device against a Google root and a revocation list. Its strength is real and its scope is exact. In the next part we stay inside that scope and use instrumentation to move a valid statement from a legitimate device to the one under analysis.&lt;/p&gt;
&lt;h2 id="bypass-through-instrumentation_1"&gt;Bypass through instrumentation&lt;/h2&gt;
&lt;p&gt;In the previous section we followed a hardware attestation from key generation to
backend verdict. Now we are on a rooted mission phone, bootloader unlocked, and the app
we want to analyze uses hardware attestation as a gate. Before any interesting feature runs, the app asks the Android Keystore for an
attested key, sends the certificate chain to its backend, and waits for a green
light. On our device that light stays red.&lt;/p&gt;
&lt;p&gt;The reason is exactly what makes attestation useful. As we saw, the &lt;code&gt;RootOfTrust&lt;/code&gt;
structure lives in the &lt;code&gt;hardwareEnforced&lt;/code&gt; authorization list, produced by the secure
environment and not by the platform. Our unlocked bootloader is written there in
plain sight: &lt;code&gt;deviceLocked&lt;/code&gt; is false and &lt;code&gt;verifiedBootState&lt;/code&gt; is not &lt;code&gt;Verified&lt;/code&gt;. The
chain is otherwise perfectly genuine. It chains to a Google root, the signatures are
valid, the challenge matches. It just tells the truth about our device, and the truth
gets us rejected.&lt;/p&gt;
&lt;p&gt;The demo client shows exactly this. The backend answers HTTP 400: &lt;code&gt;deviceLocked&lt;/code&gt; false,
&lt;code&gt;verifiedBootState&lt;/code&gt; &lt;code&gt;Unverified&lt;/code&gt;, StrongBox-backed chain and all.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The demo client app on the rooted phone. The backend response is HTTP 400 with valid false and the error Bootloader is unlocked (deviceLocked is false). The attestation report shows attestation_security_level StrongBox, verified_boot_state Unverified and device_locked false." class="align-center" src="resources/2026-08-11_bypassing-android-hardware-attestation/screenshot-demo-rejected.png" width="340"/&gt;&lt;/p&gt;
&lt;p&gt;So we need the backend to receive a chain that reports a healthy device, without
giving up the root access we need to do our job. Let us look at our options.&lt;/p&gt;
&lt;h3 id="what-can-the-analyst-do"&gt;What can the analyst do?&lt;/h3&gt;
&lt;p&gt;Three paths lead out of this, with very different costs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Move to a clean device and neuter the app.&lt;/strong&gt; We flash a locked, stock phone,
 install the target, and patch out the attestation call or its callers with
 instrumentation. This works, but it throws away the reason we rooted a phone in the
 first place. We lose the very foothold that lets us observe the app. It also means
 fighting whatever anti-tampering the app ships, on a device we cannot fully control.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Attack the hardware or the key material.&lt;/strong&gt; This is the ground Guardsquare mapped:
 leaked keyboxes, tools like TrickyStore that inject an attestation key at the
 Keystore layer, and TEE key extraction &lt;sup id="fnref3:guardsquare-rka"&gt;&lt;a class="footnote-ref" href="#fn:guardsquare-rka"&gt;1&lt;/a&gt;&lt;/sup&gt;. These techniques are powerful and, when
 they work, they defeat attestation at its root. They also depend on a leaked or
 extracted key that a vendor can revoke, and most of them are out of reach without a
 specific vulnerability or a purchased keybox.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Relay the attestation to a clean device.&lt;/strong&gt; We keep our rooted phone and its root
 access. A second phone, clean and unmodified, produces a genuine attestation
 bound to the backend's challenge, and we splice that chain into the target's flow
 with a hook. We attack neither the crypto nor the hardware, only the location where
 the attestation runs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This third path is the subject of this section. It is the cheapest of the three, it
is fully reproducible with commodity phones, and it stays in the instrumentation
world we are comfortable in.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Keeps root&lt;/th&gt;
&lt;th&gt;Needs a hardware or key exploit&lt;/th&gt;
&lt;th&gt;Main cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean device, patch the app&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;loses the analysis foothold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack hardware or key material&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;needs a keybox or a TEE bug, revocable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relay to a clean device&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;needs one clean phone, one-shot gate only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img alt="The three ways past the wall side by side, each shown against the same three layers. Option one, cleaning the device and patching the app, attacks the app logic. Option two, attacking the hardware or key material, attacks the secure hardware. Option three, relaying to a clean device, leaves both untouched and attacks only the location where attestation runs. This article takes the third." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig5-three-bypass-options.svg" width="100%"/&gt;
&lt;img alt="The three ways past the wall side by side, each shown against the same three layers. Option one, cleaning the device and patching the app, attacks the app logic. Option two, attacking the hardware or key material, attacks the secure hardware. Option three, relaying to a clean device, leaves both untouched and attacks only the location where attestation runs. This article takes the third." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig5-three-bypass-options-dark.svg" width="100%"/&gt;&lt;/p&gt;
&lt;h3 id="the-idea-in-one-breath"&gt;The idea in one breath&lt;/h3&gt;
&lt;p&gt;A genuinely clean phone passing attestation is not a bypass. It is attestation doing
its job. The bypass begins when we take that clean phone's attestation and use it to
answer for our rooted phone.&lt;/p&gt;
&lt;p&gt;The plan is a relay. The target app, running on our rooted phone, is about to ask its
local Keystore for an attested key. We intercept that request. We forward the
backend's challenge to a clean phone, which runs its own attestation and hands us back
a real certificate chain, signed by real secure hardware, and carrying our challenge.
We return that chain to the app as if the local Keystore had produced it. The app
forwards it to the backend. The backend sees a genuine chain, a matching challenge,
and a locked, verified device, and it validates.&lt;/p&gt;
&lt;p&gt;Nothing is forged. Every certificate is authentic. We simply changed the phone that
produced them.&lt;/p&gt;
&lt;h3 id="the-moving-parts"&gt;The moving parts&lt;/h3&gt;
&lt;p&gt;To make the demo self-contained, the repository ships four pieces. Three of them
stand in for a real engagement, the fourth is the instrumentation itself.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/quarkslab/android-hardware-attestation-demo"&gt;https://github.com/quarkslab/android-hardware-attestation-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The backend&lt;/strong&gt; (&lt;code&gt;backend/attestation_backend.py&lt;/code&gt;), a Python validator. It issues
 challenges on &lt;code&gt;POST /nonce&lt;/code&gt; and verifies chains on &lt;code&gt;POST /verify&lt;/code&gt;. It plays the role
 of the target app's server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The demo client&lt;/strong&gt; (&lt;code&gt;apps/QuarkslabAttestationDemo&lt;/code&gt;), an Android app that stands in
 for the target. It runs the honest attestation flow so we have a clean seam to hook.
 In a real engagement this is the app under analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The clean server&lt;/strong&gt; (&lt;code&gt;apps/QuarkslabAttestationServer&lt;/code&gt;), an Android app that runs on
 an unmodified phone. Given a challenge, it produces an attestation chain and nothing
 else. It is our oracle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The instrumentation&lt;/strong&gt; (&lt;code&gt;instrumentation/&lt;/code&gt;), a Frida agent plus a Python
 controller. This is the analyst's tool and the heart of the bypass.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We build the technique in three steps. First we find the seam in the client. Then we
stand up the clean oracle. Then we wire the two together with the hook.&lt;/p&gt;
&lt;h3 id="where-is-the-seam"&gt;Where is the seam?&lt;/h3&gt;
&lt;p&gt;Before hooking anything, we need to know what we are cutting. Let us read the honest
flow in the demo client. &lt;code&gt;MainActivity&lt;/code&gt; runs three calls in order.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;attest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Report&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;backend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BackendClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nonce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestNonce&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// The backend encodes the nonce as unpadded base64url. The challenge is the raw value.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;challenge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;URL_SAFE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NO_WRAP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeystoreAttestation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;generateAttestedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chainBase64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;buildReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"LOCAL FAILURE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;javaClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;simpleName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Step one asks the backend for a nonce. Step two turns that nonce into an attested key.
Step three sends the resulting chain back for a verdict. The interesting work happens
in step two, inside &lt;code&gt;KeystoreAttestation&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;generateAttestedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AttestedKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;spec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyGenParameterSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;KEY_ALIAS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PURPOSE_SIGN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PURPOSE_VERIFY&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAlgorithmParameterSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECGenParameterSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secp256r1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDigests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DIGEST_SHA256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttestationChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIsStrongBoxBacked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strongBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;KeyPairGenerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;KEY_ALGORITHM_EC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"AndroidKeyStore"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... export chain with keyStore.getCertificateChain(KEY_ALIAS)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is the whole hardware interaction. The call to &lt;code&gt;setAttestationChallenge&lt;/code&gt;
&lt;sup id="fnref:set-challenge"&gt;&lt;a class="footnote-ref" href="#fn:set-challenge"&gt;10&lt;/a&gt;&lt;/sup&gt; is what turns an ordinary key generation into an attestation request. The
bytes we pass here are copied verbatim into the &lt;code&gt;attestationChallenge&lt;/code&gt; field of the
&lt;code&gt;KeyDescription&lt;/code&gt;, the challenge provided at key generation time. That is the
freshness binding the backend later checks against the nonce it issued.&lt;/p&gt;
&lt;p&gt;On a clean phone this method returns a chain that reports a locked device. On our
rooted phone it returns a chain that reports an unlocked one. Same code, same key,
different verdict. The method is the seam. Everything device-specific is behind it,
and its signature is small.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;generateAttestedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AttestedKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In goes a challenge, out comes a chain wrapped in an &lt;code&gt;AttestedKey&lt;/code&gt;. If we can answer
this call with a chain from another device, the app never knows the difference. This
is where we cut.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note for real targets.&lt;/strong&gt; In the demo the seam is a single tidy method. A real app
rarely offers one. The generic choke points are lower down, at
&lt;code&gt;KeyGenParameterSpec.Builder.setAttestationChallenge&lt;/code&gt; on the way in and at
&lt;code&gt;java.security.KeyStore.getCertificateChain&lt;/code&gt; on the way out. Hooking those two
reaches any app that uses the standard Keystore attestation path, at the cost of a
little more plumbing to correlate the challenge with the chain.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="the-clean-oracle"&gt;The clean oracle&lt;/h3&gt;
&lt;p&gt;The second phone runs the server app on a stock, locked system. Its job is narrow.
Given a nonce, produce an attestation chain bound to it, and return it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;buildKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strongBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;spec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyGenParameterSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PURPOSE_SIGN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAlgorithmParameterSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ECGenParameterSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secp256r1"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDigests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DIGEST_SHA256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttestationChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIsStrongBoxBacked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strongBox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;KeyPairGenerator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;KEY_ALGORITHM_EC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KEYSTORE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is the same key generation as the client, on purpose. The difference is the
device it runs on. Here &lt;code&gt;setAttestationChallenge&lt;/code&gt; produces a chain whose &lt;code&gt;RootOfTrust&lt;/code&gt;
reports &lt;code&gt;deviceLocked&lt;/code&gt; true and &lt;code&gt;verifiedBootState&lt;/code&gt; &lt;code&gt;Verified&lt;/code&gt;, because that is the
real state of this phone. The server exposes it over HTTP.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;attest&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"nonce"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;base64url&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"nonce"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;base64url&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"chain"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;der_base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"chain_length"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One detail makes the relay trivial. The server decodes the nonce, uses the raw bytes
as the challenge, and echoes the same nonce string back next to the chain. Its
response body is byte for byte the request body the backend expects on &lt;code&gt;POST /verify&lt;/code&gt;.
We can forward it as is, no reshaping.&lt;/p&gt;
&lt;p&gt;On the clean phone the oracle is a single screen. We start it, it listens on the local
network, and it is ready to answer &lt;code&gt;/attest&lt;/code&gt; for any nonce. Nothing else happens here.
This device only lends its healthy hardware.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The clean server app running on an unmodified phone. It is listening on http://192.168.1.203:8080 and exposes GET /status, POST /attest with a nonce, and GET /attest with a nonce query parameter." class="align-center" src="resources/2026-08-11_bypassing-android-hardware-attestation/screenshot-server-oracle.png" width="340"/&gt;&lt;/p&gt;
&lt;h3 id="the-hook"&gt;The hook&lt;/h3&gt;
&lt;p&gt;Now we connect the two phones. The Frida agent runs on the rooted phone and replaces
the seam we found. It is short enough to read in full.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'frida-java-bridge'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PACKAGE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'com.quarkslab.attestation.demo'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// android.util.Base64 flags: NO_PADDING(1) | NO_WRAP(2) | URL_SAFE(8).&lt;/span&gt;
&lt;span class="c1"&gt;// This reproduces the unpadded base64url the backend issues from POST /nonce.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BASE64_URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;KeystoreAttestation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PACKAGE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;.KeystoreAttestation`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AttestedKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PACKAGE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;.AttestedKey`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Base64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'android.util.Base64'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ArrayList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'java.util.ArrayList'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;KeystoreAttestation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generateAttestedKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;implementation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// challenge is the raw nonce bytes. Re-encode to the backend base64url form&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// so the clean device is queried with the exact nonce string it expects.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BASE64_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'attestation_request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;nonce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'response'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;??&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remoteChain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ArrayList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;remoteChain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// AttestedKey(List&amp;lt;String&amp;gt; chainBase64, String requestedLevel, String? fallbackReason)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AttestedKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remoteChain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'StrongBox (relayed)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'ready'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PACKAGE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;.KeystoreAttestation.generateAttestedKey`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let us read it the way Frida runs it. &lt;code&gt;Java.perform&lt;/code&gt; gives us a callback that runs on a
thread attached to the Android runtime, which is required before touching any Java
class &lt;sup id="fnref:frida-js"&gt;&lt;a class="footnote-ref" href="#fn:frida-js"&gt;13&lt;/a&gt;&lt;/sup&gt;. &lt;code&gt;Java.use&lt;/code&gt; resolves the classes we need, including the app's own
&lt;code&gt;KeystoreAttestation&lt;/code&gt; and &lt;code&gt;AttestedKey&lt;/code&gt;. We then assign a new function to
&lt;code&gt;generateAttestedKey.implementation&lt;/code&gt;, which redefines the method for every future
call.&lt;/p&gt;
&lt;p&gt;Inside the replacement we do four things.&lt;/p&gt;
&lt;p&gt;We take the &lt;code&gt;challenge&lt;/code&gt; bytes handed to us and re-encode them with
&lt;code&gt;Base64.encodeToString&lt;/code&gt; using the flags &lt;code&gt;NO_PADDING | NO_WRAP | URL_SAFE&lt;/code&gt;, that is
&lt;code&gt;1 | 2 | 8&lt;/code&gt; &lt;sup id="fnref:base64"&gt;&lt;a class="footnote-ref" href="#fn:base64"&gt;11&lt;/a&gt;&lt;/sup&gt;. This rebuilds the exact nonce string the backend issued, because the
backend produces its nonce with unpadded base64url and the client decoded that same
string on the way in. The round trip is lossless.&lt;/p&gt;
&lt;p&gt;We hand that nonce to our controller with &lt;code&gt;send&lt;/code&gt;, then block on &lt;code&gt;recv('response',...)&lt;/code&gt;
followed by &lt;code&gt;op.wait()&lt;/code&gt;. &lt;code&gt;send&lt;/code&gt; pushes a message out to the controller, &lt;code&gt;recv&lt;/code&gt;
registers a one-shot callback for the reply, and &lt;code&gt;wait&lt;/code&gt; blocks the hooked thread until
that reply arrives &lt;sup id="fnref:frida-messages"&gt;&lt;a class="footnote-ref" href="#fn:frida-messages"&gt;12&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref2:frida-js"&gt;&lt;a class="footnote-ref" href="#fn:frida-js"&gt;13&lt;/a&gt;&lt;/sup&gt;. This turns an asynchronous message exchange into a
synchronous call, which is what we need. The app's thread must not return until we have
a chain to give it.&lt;/p&gt;
&lt;p&gt;We wrap the relayed certificates in a &lt;code&gt;java.util.ArrayList&lt;/code&gt; and build an &lt;code&gt;AttestedKey&lt;/code&gt;
with &lt;code&gt;$new&lt;/code&gt;. The &lt;code&gt;'StrongBox (relayed)'&lt;/code&gt; label is cosmetic, only the chain matters to
the backend.&lt;/p&gt;
&lt;p&gt;The important line is the one that is not there. We never call the original
&lt;code&gt;generateAttestedKey&lt;/code&gt;. The local Keystore is never touched, so the rooted phone never
produces its failing chain. The app receives our relayed &lt;code&gt;AttestedKey&lt;/code&gt; and cannot tell
it apart from a local one, because the type and shape are identical.&lt;/p&gt;
&lt;h3 id="the-controller"&gt;The controller&lt;/h3&gt;
&lt;p&gt;The Python side attaches the agent and plays courier between the two phones.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"[!] agent error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stack'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"send"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"attestation_request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"[*] Intercepted nonce &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s2"&gt;"chain"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chain"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"[!] Server request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Always answer so the hooked thread never blocks, even on failure.&lt;/span&gt;
    &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When the agent sends an &lt;code&gt;attestation_request&lt;/code&gt;, the controller reads the nonce, posts it
to the clean device's &lt;code&gt;/attest&lt;/code&gt; endpoint, and posts the returned chain back to the
agent with &lt;code&gt;script.post&lt;/code&gt; &lt;sup id="fnref2:frida-messages"&gt;&lt;a class="footnote-ref" href="#fn:frida-messages"&gt;12&lt;/a&gt;&lt;/sup&gt;. That reply is what unblocks the &lt;code&gt;op.wait()&lt;/code&gt; on the phone.
The controller always answers, even on failure, so a network error degrades to an empty
chain and a clean rejection instead of a frozen app thread.&lt;/p&gt;
&lt;p&gt;Attaching is a few lines with the Frida Python bindings.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frida&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_usb_device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEMO_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# or device.spawn + attach&lt;/span&gt;
&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AGENT_SCRIPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;make_message_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="putting-it-together"&gt;Putting it together&lt;/h3&gt;
&lt;p&gt;With the backend running, the clean server app open on the second phone, and the
controller attached to the target, we tap the attestation button. Here is the full
round trip.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;generateAttestedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;challenge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hooked&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;/&lt;/span&gt;&lt;span class="n"&gt;attest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"chain"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hardware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AttestedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img alt="Sequence diagram of the relay across three swimlanes: the backend, the rooted mission phone holding the client app and the Frida agent plus controller, and the clean phone holding the server oracle. The client gets a nonce from the backend, then the hooked generateAttestedKey call is intercepted by the agent, which asks the clean phone for a real attestation and returns the relayed chain to the app. The app sends it to the backend, which validates it. The hooked call is the pivot and the local Keystore is never called." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig6-relay-sequence.svg" width="100%"/&gt;
&lt;img alt="Sequence diagram of the relay across three swimlanes: the backend, the rooted mission phone holding the client app and the Frida agent plus controller, and the clean phone holding the server oracle. The client gets a nonce from the backend, then the hooked generateAttestedKey call is intercepted by the agent, which asks the clean phone for a real attestation and returns the relayed chain to the app. The app sends it to the backend, which validates it. The hooked call is the pivot and the local Keystore is never called." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig6-relay-sequence-dark.svg" width="100%"/&gt;&lt;/p&gt;
&lt;p&gt;Back on the client, the same button now tells a different story. Same app, same code,
but the verdict flipped to &lt;code&gt;ATTESTATION VALID&lt;/code&gt;. The report reads &lt;code&gt;verified_boot_state
Verified&lt;/code&gt; and &lt;code&gt;device_locked true&lt;/code&gt;, and the requested level is tagged &lt;code&gt;StrongBox
(relayed)&lt;/code&gt;. These values now come from the clean phone, not from ours.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The demo client app on the rooted phone after the relay. The backend response is HTTP 200 with valid true and hardware_backed true. The attestation report now shows verified_boot_state Verified, device_locked true, and the local key generation is tagged requested_level StrongBox (relayed)." class="align-center" src="resources/2026-08-11_bypassing-android-hardware-attestation/screenshot-demo-valid.png" width="340"/&gt;&lt;/p&gt;
&lt;p&gt;The chain that reaches the backend was produced by real secure hardware on a locked,
verified phone. It carries our backend's challenge. So it passes every check the
validator makes, and it is worth walking through them from the backend's own code to
see why none of them fires.&lt;/p&gt;
&lt;p&gt;The chain builds a valid path to a Google root and every signature checks out. The
challenge binding holds, because the &lt;code&gt;attestationChallenge&lt;/code&gt; in the chain is the nonce
we relayed unchanged.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bind the attestation to the challenge we issued.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"challenge_b64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rejected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The device policy is where our rooted phone would have died, and where the relay saves
us. The backend reads the &lt;code&gt;RootOfTrust&lt;/code&gt; only from the &lt;code&gt;hardwareEnforced&lt;/code&gt; list, exactly
as it should, since the software list can be written by a compromised platform.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"verified_boot_state"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"RootOfTrust missing from the tee_enforced authorization list"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"device_locked"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Bootloader is unlocked (deviceLocked is false)"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"Verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Verified Boot state is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, expected Verified"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;tee_enforced&lt;/code&gt; in that error string is the Keymaster-era name for the
hardware-enforced list. KeyMint renamed it &lt;code&gt;hardwareEnforced&lt;/code&gt; from attestation version
300 on, and the current AOSP schema uses that name throughout. Same field, two names.&lt;/p&gt;
&lt;p&gt;On our own chain, &lt;code&gt;device_locked&lt;/code&gt; would be false and this returns an error. On the
relayed chain it is true and the state is &lt;code&gt;Verified&lt;/code&gt;, because those values come from
the clean phone's secure hardware. The hardware-backed check passes for the same
reason, the attestation security level is &lt;code&gt;StrongBox&lt;/code&gt; or &lt;code&gt;TrustedEnvironment&lt;/code&gt;, not
&lt;code&gt;Software&lt;/code&gt;. The verdict is &lt;code&gt;valid&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="why-the-backend-cannot-tell"&gt;Why the backend cannot tell&lt;/h3&gt;
&lt;p&gt;What can the backend actually see? This is what makes the relay work, and what a
mitigation has to change.&lt;/p&gt;
&lt;p&gt;The certificate chain proves that a key lives in genuine secure hardware on a locked,
verified device, and that this hardware saw our challenge. It does not prove which
phone is talking to the backend right now. There is no field in the &lt;code&gt;KeyDescription&lt;/code&gt;
that binds the attestation to the transport, the session, or the device presenting it.
The nonce is the only freshness signal, and we forwarded it faithfully. From the
backend's chair, a relayed chain and a local chain are the same bytes.&lt;/p&gt;
&lt;p&gt;This is the same shape as the Remote Key Attestation that Guardsquare documented &lt;sup id="fnref2:guardsquare-rka"&gt;&lt;a class="footnote-ref" href="#fn:guardsquare-rka"&gt;1&lt;/a&gt;&lt;/sup&gt;,
a chain produced on one device and presented from another. Unlike the variants they describe, ours hides
no keybox and patches no boot chain. It forwards a real attestation from a real clean
device, bound to the real challenge, nothing more.&lt;/p&gt;
&lt;h3 id="where-the-simple-relay-stops"&gt;Where the simple relay stops&lt;/h3&gt;
&lt;p&gt;Our relay leans on two things the backend fails to do, and naming them points straight
at the mitigations.&lt;/p&gt;
&lt;p&gt;The first is decisive. The backend never checks which app the chain was issued to. Our
chain is produced on the clean phone by a different app than the target, and the
attestation says so. The app identity is written into the certificate under
&lt;code&gt;attestationApplicationId&lt;/code&gt;. Our backend simply never reads it. The moment it does, and
compares that field to the app it expects, the chain we built is rejected. That single
check is where our relay stops, and it is the cheapest of all.&lt;/p&gt;
&lt;p&gt;The second is deeper. The relay assumes attestation is a one-shot gate. The app attests
once, at launch or during login, the backend validates the chain and the nonce, grants
access, and moves on. That assumption holds surprisingly often, and when it does the
relay is enough. It breaks the moment the backend asks the client to prove, later in the
same session, that it holds the attested private key. The attested key lives on the clean
phone, not on our rooted one. We relayed a chain, not a key. If the backend sends a fresh
value to sign with that key and checks the signature, our test device cannot answer on
its own. We would have to relay signing operations too, keeping the clean phone in the
loop for every signed message. The one-shot courier becomes a live proxy, heavier and far
more fragile, but not stopped.&lt;/p&gt;
&lt;p&gt;Both are seams a defender can press on. We take them up in the next section, the decisive
and cheap one first.&lt;/p&gt;
&lt;h2 id="mitigation_1"&gt;Mitigation&lt;/h2&gt;
&lt;p&gt;We now switch chairs and look at the relay from the backend side. The question is
simple. What check would have stopped our own bypass? We take the mitigations cheapest
first, and we keep each one honest. For every proposal we say what it forces the
attacker to do, and what it does not solve.&lt;/p&gt;
&lt;p&gt;One caveat up front. The first mitigation has been tested against our own relay. Its code,
however, does not ship with this article. The companion backend leaves it out on purpose,
so the relay has a gate to walk through. We ran the check separately and confirmed it
rejects the relayed chain. The second is a design proposal. We reason about it and point at
the standards it rests on, but we did not build it and we did not test it. We flag this
again in each section and in the synthesis table, so the reader can tell a tested control
from a blueprint.&lt;/p&gt;
&lt;p&gt;It is worth being clear about what the relay exploits. It is not a weakness in the
attestation mechanism. The chain we relayed is genuine, the signatures are valid, the
hardware told the truth. What let us through is a backend that validated the chain without
checking who it was issued to. The relay worked because of two gaps.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The chain carries the identity of the app that requested the attestation, but the
   baseline backend never checks it against the app it expects to be talking to.&lt;/li&gt;
&lt;li&gt;It certifies a key the client is never asked to use, so a chain relayed from another
   device is never challenged.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first gap is the real one, and closing it is the real mitigation. The second only
raises the analyst's cost. We take them in that order.&lt;/p&gt;
&lt;h3 id="bind-the-attestation-to-your-app"&gt;Bind the attestation to your app&lt;/h3&gt;
&lt;p&gt;The attestation extension already carries the identity of the app that generated the
key. The &lt;code&gt;attestationApplicationId&lt;/code&gt; field, tag &lt;code&gt;[709]&lt;/code&gt; in the &lt;code&gt;AuthorizationList&lt;/code&gt;,
holds the package name, the SHA-256 digests of the app's signing certificates, and the
version code &lt;sup id="fnref9:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:willden-attestation"&gt;&lt;a class="footnote-ref" href="#fn:willden-attestation"&gt;14&lt;/a&gt;&lt;/sup&gt;. Our backend never reads it. Google's own issuance guidance is explicit that the package name and signature come from the holder and must be obtained and checked independently &lt;sup id="fnref:digital-credentials"&gt;&lt;a class="footnote-ref" href="#fn:digital-credentials"&gt;20&lt;/a&gt;&lt;/sup&gt;. That omission is what lets the relay
pass, because the chain came from a different app.&lt;/p&gt;
&lt;p&gt;Its value is a DER-encoded structure, nested inside the extension &lt;sup id="fnref:android-key-attestation"&gt;&lt;a class="footnote-ref" href="#fn:android-key-attestation"&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref2:willden-attestation"&gt;&lt;a class="footnote-ref" href="#fn:willden-attestation"&gt;14&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;AttestationApplicationId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SEQUENCE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;packageInfos&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AttestationPackageInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;signatureDigests&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;AttestationPackageInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SEQUENCE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;packageName&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;OCTET_STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It is tempting to picture the app identity as a
field the client sends next to the chain. That is not where it lives. It sits under tag &lt;code&gt;[709]&lt;/code&gt;,
inside the same attestation extension as the challenge and the root of trust, and
the backend parses it out of the certificate. &lt;code&gt;packageName&lt;/code&gt; is the app package
name, &lt;code&gt;signatureDigests&lt;/code&gt; are the SHA-256 digests of the app's signing
certificates. The platform fills both from the app that called &lt;code&gt;generateKey&lt;/code&gt;, so
on a device with a healthy boot state the caller cannot choose them.&lt;/p&gt;
&lt;p&gt;There is a catch, and it is the reason this check is placed first but not alone.
&lt;code&gt;attestationApplicationId&lt;/code&gt; lives in the &lt;code&gt;softwareEnforced&lt;/code&gt; list, populated by the
Android platform, not by the secure hardware. A software-enforced value can be
trusted only as long as the device that produced it runs an operating system that
complies with the Android Platform Security Model, that is, its bootloader is locked
and its &lt;code&gt;verifiedBootState&lt;/code&gt; is &lt;code&gt;Verified&lt;/code&gt; &lt;sup id="fnref10:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;. On a directly rooted phone, with no
relay, a compromised platform could write anything here.&lt;/p&gt;
&lt;p&gt;That caveat is exactly why the check works against the relay. Our backend already
rejects any chain whose hardware-enforced &lt;code&gt;RootOfTrust&lt;/code&gt; does not report a locked,
verified device.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"device_locked"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Bootloader is unlocked (deviceLocked is false)"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"Verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Verified Boot state is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, expected Verified"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once that check passes, the chain was generated on a device whose OS satisfies the
security model. On such a device the &lt;code&gt;softwareEnforced&lt;/code&gt; values are trustworthy. So
the two checks compose. The hardware-enforced boot state vouches for the platform, and
the now-trustworthy platform vouches for the app identity.&lt;/p&gt;
&lt;p&gt;Concretely, the backend adds one comparison after parsing the extension.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative. Not yet implemented in attestation_backend.py.&lt;/span&gt;
&lt;span class="n"&gt;EXPECTED_PACKAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.quarkslab.attestation.demo"&lt;/span&gt;
&lt;span class="n"&gt;EXPECTED_SIGNER_SHA256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;hellip;"&lt;/span&gt;  &lt;span class="c1"&gt;# SHA-256 of our release signing certificate&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"application_package"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Attestation was generated by a different application"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_SIGNER_SHA256&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"application_signatures"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Attestation application signature does not match"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In our relay the chain is produced by the clean oracle, which runs
&lt;code&gt;com.quarkslab.attestation.server&lt;/code&gt;. Its honest platform writes that package and its own
signing digest into the field. The comparison fails and the chain is rejected. The
analyst cannot repackage around it either. A clean verified-boot OS will report the
true identity, and the signing certificate digest cannot be matched without the
target's private signing key. We added this check in a separate test and it rejected the
relayed chain. This single comparison kills the cross-app relay we built, even though the
shipped backend leaves it out on purpose.&lt;/p&gt;
&lt;p&gt;It does not, on its own, prove device integrity. Treat it as a binding to your app, not
as an integrity signal, and always pair it with the hardware-enforced boot state check
above.&lt;/p&gt;
&lt;h3 id="bind-the-attestation-to-a-live-key"&gt;Bind the attestation to a live key&lt;/h3&gt;
&lt;p&gt;The app check closes the relay we demonstrated. Could a determined analyst still get past
it? Only by producing the attestation from a clean instance of the very same app, so that
the package and the signing digest match. That is harder than it sounds. It means driving
the real target app, on a clean and verified device, to attest a challenge the analyst
chooses and to hand back the raw chain. A clean verified device is exactly the kind you
cannot instrument. There is no easy way to inject the nonce or to export the chain.
Rooting that device to instrument it would break its verified boot state and fail the
attestation. Short of a keybox or a TEE bug, which is the hardware path we set aside, the
same-app relay stays out of reach.&lt;/p&gt;
&lt;p&gt;Suppose the analyst gets there anyway. The deeper problem is that the backend inspects a
certificate and never asks the client to use the key it certifies. The fix is proof of
possession. After a valid attestation, the backend requires the client to sign a fresh
server challenge with the attested private key, inside the same session, and it verifies
the signature &lt;sup id="fnref:line-attestation"&gt;&lt;a class="footnote-ref" href="#fn:line-attestation"&gt;16&lt;/a&gt;&lt;/sup&gt;. This is the assertion step that mobile attestation
designs place after attestation, the same attestation-then-assertion pattern used by
hardware-backed authenticators &lt;sup id="fnref:fido-hka"&gt;&lt;a class="footnote-ref" href="#fn:fido-hka"&gt;17&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This does not close the relay. The attested key lives on the clean device, so the analyst
answers the proof by relaying every signing operation to it. The one-shot courier becomes
a live proxy that must stay online for the whole session. That is an operational cost,
latency on every signature and a device that has to stay reachable, not a barrier. It
slows the analyst down, it does not lock them out.&lt;/p&gt;
&lt;p&gt;We describe this mitigation, we do not ship it. The companion backend implements only the
app binding above. Proof of possession is a design proposal here, reasoned but not
implemented and not tested against our relay.&lt;/p&gt;
&lt;h3 id="synthesis"&gt;Synthesis&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;th&gt;Forces the attacker to&lt;/th&gt;
&lt;th&gt;Residual risk&lt;/th&gt;
&lt;th&gt;Defender cost&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Check &lt;code&gt;attestationApplicationId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;attest from the same app on a clean device, which excludes instrumentation&lt;/td&gt;
&lt;td&gt;none for the relay we built, must be paired with the boot state check&lt;/td&gt;
&lt;td&gt;one comparison, backend only&lt;/td&gt;
&lt;td&gt;tested against our relay, not in the shipped code&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref11:aosp-attestation"&gt;&lt;a class="footnote-ref" href="#fn:aosp-attestation"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref3:willden-attestation"&gt;&lt;a class="footnote-ref" href="#fn:willden-attestation"&gt;14&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof of possession of the attested key&lt;/td&gt;
&lt;td&gt;run a live signing proxy to the clean device for the whole session&lt;/td&gt;
&lt;td&gt;relay still possible while the proxy stays online&lt;/td&gt;
&lt;td&gt;one challenge-response, backend and app&lt;/td&gt;
&lt;td&gt;design, not implemented here&lt;/td&gt;
&lt;td&gt;&lt;sup id="fnref2:line-attestation"&gt;&lt;a class="footnote-ref" href="#fn:line-attestation"&gt;16&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref2:fido-hka"&gt;&lt;a class="footnote-ref" href="#fn:fido-hka"&gt;17&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img alt="Two backend checks as two nested layers around the backend, weakest layer on the outside. The outer layer is a dashed line, proof of possession, a cost and not a barrier. The inner layer, closest to the backend, is a solid wall, the attestationApplicationId check, the barrier that stops the relay. A relayed chain from a different app crosses the outer layer and is stopped at the inner solid wall, rejected. A relayed chain from the same app is only slowed at the outer layer, where it turns into a live signing proxy, then passes the inner wall because the app id matches and reaches the backend. A legend states that the solid barrier stops the relay while the dashed cost lets it continue as a live proxy." class="align-center fig-light" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig7-mitigation-barrier.svg" width="70%"/&gt;
&lt;img alt="Two backend checks as two nested layers around the backend, weakest layer on the outside. The outer layer is a dashed line, proof of possession, a cost and not a barrier. The inner layer, closest to the backend, is a solid wall, the attestationApplicationId check, the barrier that stops the relay. A relayed chain from a different app crosses the outer layer and is stopped at the inner solid wall, rejected. A relayed chain from the same app is only slowed at the outer layer, where it turns into a live signing proxy, then passes the inner wall because the app id matches and reaches the backend. A legend states that the solid barrier stops the relay while the dashed cost lets it continue as a live proxy." class="align-center fig-dark" src="resources/2026-08-11_bypassing-android-hardware-attestation/fig7-mitigation-barrier-dark.svg" width="70%"/&gt;&lt;/p&gt;
&lt;p&gt;The checks
above are prevention, and they are cheap. The relay we built does not exploit a weakness
in attestation. It exploits a backend that never reads &lt;code&gt;attestationApplicationId&lt;/code&gt;. That
is the one check that matters. Proof of possession only raises the cost for an attacker
who has somehow already cleared that bar.&lt;/p&gt;
&lt;h2 id="conclusion_1"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We set out to do two things. Understand what Android hardware attestation proves, and
get past it when it blocks an analysis. Both are done, and they point at the same
lesson.&lt;/p&gt;
&lt;p&gt;The mechanism itself is sound. A key generated in a TEE or a StrongBox is signed by a
key the device cannot choose or forge, the chain climbs to a Google root, and the
&lt;code&gt;RootOfTrust&lt;/code&gt; reports the boot state without asking the platform for permission. On our
rooted phone that machinery worked exactly as designed. It told the truth, and the
truth locked us out. There was no cryptography to break, and we did not try.&lt;/p&gt;
&lt;p&gt;What we broke was an assumption. The backend treated a signed certificate as proof that
the phone in front of it was healthy. It is not. It is proof that some phone, somewhere,
has healthy hardware and once saw our challenge. We supplied that phone. A clean device
answered the attestation, its genuine chain traveled back through a Frida hook, and the
backend validated a device it had never met. We changed neither the key nor the boot
state. We changed who got asked.&lt;/p&gt;
&lt;p&gt;That is the moral, and it is not specific to attestation. A hardware guarantee is only
as strong as the protocol that consumes it. Attestation certifies a key, on a device,
at a moment in time. It says nothing about which app requested it or whether that key is
ever used again. A backend that reads only "valid
chain, healthy device" and opens the gate has turned a hardware guarantee into a
formality. Closing the gap does not call for better silicon. It calls for the backend to
bind the attestation to the app that asked. That check is cheap and unglamorous, which is
probably why it is so often skipped.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;div class="footnote"&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id="fn:guardsquare-rka"&gt;
&lt;p&gt;Sergio Castell, &lt;a href="https://www.guardsquare.com/blog/bypassing-key-attestation-api"&gt;Bypassing Key Attestation: How Remote Devices Exploit the API&lt;/a&gt;, Guardsquare, 27 January 2026.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:guardsquare-rka" title="Jump back to footnote 1 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:guardsquare-rka" title="Jump back to footnote 1 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:guardsquare-rka" title="Jump back to footnote 1 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:aosp-attestation"&gt;
&lt;p&gt;&lt;a href="https://source.android.com/docs/security/features/keystore/attestation"&gt;Key and ID attestation&lt;/a&gt;, Android Open Source Project.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref4:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref5:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref6:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref7:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref8:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref9:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref10:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref11:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref12:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref13:aosp-attestation" title="Jump back to footnote 2 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:cdd"&gt;
&lt;p&gt;&lt;a href="https://source.android.com/docs/compatibility/cdd"&gt;Android Compatibility Definition Document&lt;/a&gt;, section 9.11, Keys and Credentials.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:cdd" title="Jump back to footnote 3 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:cdd" title="Jump back to footnote 3 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:keystore-hsm"&gt;
&lt;p&gt;&lt;a href="https://developer.android.com/privacy-and-security/keystore"&gt;Android Keystore system&lt;/a&gt;, Hardware security module, Android Developers.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:keystore-hsm" title="Jump back to footnote 4 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:verify-attestation"&gt;
&lt;p&gt;&lt;a href="https://developer.android.com/privacy-and-security/security-key-attestation"&gt;Verify hardware-backed key pairs with key attestation&lt;/a&gt;, Android Developers.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:verify-attestation" title="Jump back to footnote 5 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:verify-attestation" title="Jump back to footnote 5 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:verify-attestation" title="Jump back to footnote 5 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:verified-boot"&gt;
&lt;p&gt;&lt;a href="https://source.android.com/docs/security/features/verifiedboot"&gt;Verified Boot&lt;/a&gt;, Android Open Source Project.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:verified-boot" title="Jump back to footnote 6 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:boot-flow"&gt;
&lt;p&gt;&lt;a href="https://source.android.com/docs/security/features/verifiedboot/boot-flow"&gt;Boot flow and device state&lt;/a&gt;, Verified Boot, Android Open Source Project.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:boot-flow" title="Jump back to footnote 7 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:trust-but-verify"&gt;
&lt;p&gt;&lt;a href="https://security.googleblog.com/2019/09/trust-but-verify-attestation-with.html"&gt;Trust but verify attestation with revocation&lt;/a&gt;, Google Online Security Blog, 2019.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:trust-but-verify" title="Jump back to footnote 8 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:trust-but-verify" title="Jump back to footnote 8 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:trust-but-verify" title="Jump back to footnote 8 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref4:trust-but-verify" title="Jump back to footnote 8 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:rkp"&gt;
&lt;p&gt;&lt;a href="https://source.android.com/docs/core/ota/modular-system/remote-key-provisioning"&gt;Remote Key Provisioning&lt;/a&gt;, Android Open Source Project.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:rkp" title="Jump back to footnote 9 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:rkp" title="Jump back to footnote 9 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:rkp" title="Jump back to footnote 9 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref4:rkp" title="Jump back to footnote 9 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:set-challenge"&gt;
&lt;p&gt;&lt;a href="https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder"&gt;KeyGenParameterSpec.Builder, setAttestationChallenge&lt;/a&gt;, Android Developers.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:set-challenge" title="Jump back to footnote 10 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:base64"&gt;
&lt;p&gt;&lt;a href="https://developer.android.com/reference/android/util/Base64"&gt;android.util.Base64&lt;/a&gt;, Android Developers.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:base64" title="Jump back to footnote 11 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:frida-messages"&gt;
&lt;p&gt;&lt;a href="https://frida.re/docs/messages/"&gt;Messages&lt;/a&gt;, Frida.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:frida-messages" title="Jump back to footnote 12 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:frida-messages" title="Jump back to footnote 12 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:frida-js"&gt;
&lt;p&gt;&lt;a href="https://frida.re/docs/javascript-api/"&gt;JavaScript API&lt;/a&gt;, Frida.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:frida-js" title="Jump back to footnote 13 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:frida-js" title="Jump back to footnote 13 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:willden-attestation"&gt;
&lt;p&gt;Shawn Willden, &lt;a href="https://android-developers.googleblog.com/2017/09/keystore-key-attestation.html"&gt;Keystore Key Attestation&lt;/a&gt;, Android Developers Blog, 7 September 2017.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:willden-attestation" title="Jump back to footnote 14 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:willden-attestation" title="Jump back to footnote 14 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref3:willden-attestation" title="Jump back to footnote 14 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:android-key-attestation"&gt;
&lt;p&gt;&lt;a href="https://github.com/google/android-key-attestation/blob/master/src/main/java/com/google/android/attestation/AttestationApplicationId.java"&gt;AttestationApplicationId.java&lt;/a&gt;, google/android-key-attestation reference implementation.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:android-key-attestation" title="Jump back to footnote 15 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:line-attestation"&gt;
&lt;p&gt;&lt;a href="https://techblog.lycorp.co.jp/en/line-device-attestation-1"&gt;Ensuring device and app integrity and protecting service requests: LINE device attestation service&lt;/a&gt;, LY Corporation Tech Blog.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:line-attestation" title="Jump back to footnote 16 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:line-attestation" title="Jump back to footnote 16 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:fido-hka"&gt;
&lt;p&gt;&lt;a href="https://fidoalliance.org/wp-content/uploads/Hardware-backed_Keystore_White_Paper_June2018.pdf"&gt;Hardware-backed Keystore Authenticators on Android&lt;/a&gt;, FIDO Alliance white paper, June 2018.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:fido-hka" title="Jump back to footnote 17 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:fido-hka" title="Jump back to footnote 17 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:ncc-heist"&gt;
&lt;p&gt;&lt;a href="https://www.nccgroup.com/research/whitepaper-hardware-backed-heist-extracting-ecdsa-keys-from-qualcomm-s-trustzone/"&gt;Hardware-Backed Heist: Extracting ECDSA Keys from Qualcomm's TrustZone&lt;/a&gt;, NCC Group whitepaper.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:ncc-heist" title="Jump back to footnote 18 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:samsung-keymaster"&gt;
&lt;p&gt;Alon Shakevsky, Eyal Ronen, Avishai Wool, &lt;a href="https://www.usenix.org/system/files/sec22fall_shakevsky.pdf"&gt;Trust Dies in Darkness: Shedding Light on Samsung's TrustZone Keymaster Design&lt;/a&gt;, USENIX Security 2022.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:samsung-keymaster" title="Jump back to footnote 19 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:digital-credentials"&gt;
&lt;p&gt;&lt;a href="https://developer.android.com/identity/digital-credentials/credential-issuer/keystore-attestation"&gt;Implement hardware-backed attestation for digital credentials&lt;/a&gt;, Android Developers.&amp;nbsp;&lt;a class="footnote-backref" href="#fnref:digital-credentials" title="Jump back to footnote 20 in the text"&gt;&amp;larrhk;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="Android"></category><category term="android"></category><category term="attestation"></category><category term="keystore"></category><category term="TEE"></category><category term="StrongBox"></category><category term="Frida"></category><category term="instrumentation"></category><category term="reverse-engineering"></category><category term="tool"></category><category term="2026"></category></entry></feed>