Docs/Mobile SDK/Concepts

Device Attestation

In short

By default, every signed proof carries hardware-backed device attestation: Apple App Attest on iOS, hardware key attestation on Android. Proofs from a sandbox session, or with osAttestationInGatewayModes set to false, carry none. A relying party that runs octet-verify built with the appattest feature can confirm the proof came from a genuine app on a genuine device. No integration code is required. It is part of proof generation.

A proof of location records where the device was. Device attestation establishes that the proof came from a genuine app instance on genuine hardware. By default, each proof the SDK signs also carries an attestation produced by the operating system's attestation service, so the verifier can tie the proof to genuine hardware instead of trusting the signing key as carried.

Device attestation is separate from the device-key security tier (HARDWARE_STRONGBOX / HARDWARE_TEE / SOFTWARE), which records where the signing key lives. Attestation establishes that the app and OS are genuine. The security tier records the strength of the key that signed. A verifier reads both.

Three uses of the word appear in these docs. Platform attestation is the hardware-rooted evidence the platform provides: Apple App Attest on iOS, hardware key attestation on Android. Device attestation is the check described on this page, which ties that evidence to the key that signed the proof. Hardware attestation is the offline validation of the platform roots, available when octet-verify is built with the appattest feature.

On Android, the device attestation that binds each attested proof to genuine hardware is hardware key attestation. Android proofs can also carry a Google Play Integrity verdict, a separate signal that covers the app and device environment rather than the signing key. See Play Integrity on Android.

What gets attested

Platform Mechanism What it shows
iOS Apple App Attest The proof came from your App ID, running on a genuine Apple device with a Secure Enclave key.
Android Hardware key attestation The signing key lives in TEE or StrongBox, attested by a certificate chain rooted in Google's hardware-attestation root. The chain also carries the app identity: package name and signing-certificate digests.

The evidence is bound into the signed proof chain. Editing it after signing breaks verification. On iOS the App Attest assertion carries an anti-replay counter, so a captured assertion cannot be reused on a later proof.

Activation and proof time

Activation is the step where attestation is required. A device without working App Attest (iOS) or hardware key attestation (Android) cannot activate, and Octet.start fails with bootstrapFailed(attestationInvalid). On both platforms the only way past this is a sandbox session (sandboxBypassToken set), which a store build rejects. See License types.

After activation, an attestation failure at proof time does not block the proof. The SDK still produces it with weaker attestation, and the verifier reports what was attested.

Attestation cadence

Attestation is on by default. OctetConfig.advanced.attestationCadence sets how often a fresh attestation is produced:

Cadence Behaviour
per-session One attestation per SDK session. Lowest overhead.
periodic (interval) Re-attest on the interval you give. Default (5 minutes).
per-proof A fresh attestation on every proof. Highest assurance, highest cost.
let config = OctetConfig(
    advanced: AdvancedConfig(
        attestationCadence: .periodic(interval: 300)  // the default
    )
)
let octet = try await Octet.start(config: config)
val config = OctetConfig(
    advanced = AdvancedConfig(
        attestationCadence = AttestationCadence.Periodic(intervalSeconds = 300)
    )
)
val octet = Octet.start(context, config)

Android device attestation uses hardware key attestation: the certificate chain is verified offline against Google's bundled hardware-attestation roots. The backend also checks Google's public keybox-revocation list and rejects revoked devices.

Play Integrity on Android

On Android, each proof can carry a Google Play Integrity verdict alongside the hardware key attestation. The two cover different layers:

  • Hardware key attestation proves the signing key is hardware-backed (TEE or StrongBox) and chains to Google's hardware-attestation root.
  • Play Integrity reports on the app and device environment: a genuine, unmodified app on a genuine device with a licensed install.

The Play Integrity token is opaque on the device, so the SDK attaches it to the proof without reading it. The token is bound into the proof through its nonce, which is part of the data the device key signs. The proof records the status as INTEGRITY_PENDING when the SDK obtained a token, or INTEGRITY_UNAVAILABLE when it did not, for example on a build without Google Play or with no Google Cloud project linked. With osAttestationInGatewayModes set to false in a gateway mode, the SDK does not request a token, and the proof carries no Play Integrity status at all. The verifier decodes the token and decides what the Play Integrity result means for the proof.

AdvancedConfig.playIntegrityCloudProjectNumber binds the verdict to a specific Google Cloud project. Leave it null (the default) to use the project linked to your app in the Play Console. Play Integrity is a per-proof signal only: it is not part of bootstrap, and activation uses hardware key attestation, so a proof still generates without it.

Non-compliant Play Integrity results

A Play Integrity result that is not compliant, or missing, does not stop the SDK from producing the proof. The token is bound into the proof, and the verifier decides whether to accept it. See Verifying proofs.

Verifying attestation off the device

The SDK attaches attestation on the device with no integration code. Off the device, octet-verify validates it when built with the appattest feature:

cargo build --release --features appattest

With that build:

  • iOS App Attest is validated against Apple's embedded App Attest root, with no secret held, so the same proof verifies identically on any machine.
  • Android key attestation validates the Keystore certificate chain to Google's embedded, fingerprint-pinned hardware-attestation root, confirming the key is TEE or StrongBox backed.

Both paths are provided by octet-attest-verify, a standalone library you can also use directly if you write your own verification logic. Both verify fully offline against Apple's and Google's embedded roots. No network call is required.

Establishing the hardware root ahead of time

Added in 1.2. A verifier normally learns a device's hardware root from the attestation evidence that rides on that device's first proof. Octet.attestationEnrolmentBundle() returns that evidence directly, so a verifier can enrol the device's key before any proof arrives. This helps a verifier that was freshly deployed, scaled out, or migrated. On iOS the App Attest object is produced once per key, so pre-enrolment removes the wait for the first proof. On Android every attested proof already carries the full Key Attestation chain, so the bundle is a convenience mirror. See attestationEnrolmentBundle().

Two Android fixes in 1.2 affect verification:

  • The proof now carries the device-key security level (device_attestation.security_level) on the wire. A 1.1 serialization bug dropped it, so a verifier gating on a hardware tier could reject a healthy TEE or StrongBox device. A 1.2 proof reports the true tier.
  • The SDK regenerates the device key before its key-attestation chain ages out. The chain's intermediate is short-lived (about 14 days). The earlier build could present an aged-out chain about two weeks after enrolment, which fails verification. The key is now re-attested while the chain is still valid.

Where to go next

  • Verifying Proofs: what the verifier checks, including the attestation-root step.
  • Verdicts: the attestationFailed reason and the rest of the trichotomy.
  • Proof of Location: the location half of the proof, and the device-key security tier.
TermsPrivacy© 2026 Understone, Inc. d/b/a Octet