Docs/Concepts/Device Attestation

Device Attestation

In short

Every signed proof carries hardware-backed device attestation: Apple App Attest on iOS, hardware key attestation on Android. A relying party can confirm the proof came from a genuine app on a genuine device, not an emulator or a script. No integration code is required. It is part of proof generation.

A proof of location answers where the device is. Device attestation answers a different question: is this a real app instance on real hardware? The two travel together. Each proof the SDK signs also carries an attestation produced by the operating system's own attestation service, so the verifier can tie the proof to genuine hardware rather than take the device's word for it.

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

Configuring how often it runs

Attestation is on by default. How often a fresh attestation is produced is the one knob you set, on OctetConfig.advanced.attestationCadence:

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(
    licenseKey: "",
    advanced: AdvancedConfig(
        attestationCadence: .periodic(interval: 300)  // the default
    )
)
let octet = try await Octet.start(config: config)
val config = OctetConfig(
    licenseKey = "",
    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. It vouches for the key.
  • Play Integrity reports on the app and device environment: a genuine, unmodified app on a genuine device with a licensed install. It vouches for the runtime.

The verdict is attempted on every Android proof and rides in the proof envelope. It resolves to one of VERIFIED_COMPLIANT, VERIFIED_NON_COMPLIANT, PENDING, or UNAVAILABLE. On a build without Google Play, or with no Google Cloud project linked, it resolves to UNAVAILABLE.

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.

When attestation fails

If the platform attestation service returns a verdict that is not compliant, the SDK does not sign a proof it cannot stand behind. The location query returns indeterminate with reason attestationFailed. Treat it as untrusted and do not retry blindly. See Verdicts for the full reason-code table.

Verifying attestation off the device

The on-device half is automatic. The off-device half lives in the verifier. octet-verify validates attestation when built with the appattest feature:

cargo build --release --features appattest

With that build:

  • iOS App Attest is validated offline against Apple's embedded App Attest root. No network call, no secret held. The same proof verifies identically anywhere.
  • 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 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.