Docs/Concepts/Device Attestation

Device Attestation

In short

Every signed proof carries hardware-backed device attestation: Apple App Attest on iOS, Google Play Integrity 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 service the operating system provides: Apple App Attest and Google Play Integrity. Device attestation is the check described on this page, which combines that service with a hardware-backed key. Hardware attestation is the offline validation of the platform roots, available when octet-verify is built with the appattest feature.

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 Google Play Integrity The app binary, the device, and the Play install are recognized by Google.
Android Key attestation The signing key lives in TEE or StrongBox, attested by a certificate chain rooted in Google's hardware-attestation root.

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: "<your-license-key>",
    advanced: AdvancedConfig(
        attestationCadence: .periodic(interval: 300)  // the default
    )
)
let octet = try await Octet.start(config: config)
val config = OctetConfig(
    licenseKey = "<your-license-key>",
    advanced = AdvancedConfig(
        attestationCadence = AttestationCadence.Periodic(intervalSeconds = 300),
        // Optional: bind Play Integrity to a specific Google Cloud project NUMBER
        // (not the project ID). Omit to use the project linked in the Play Console.
        playIntegrityCloudProjectNumber = null
    )
)
val octet = Octet.start(context, config)

Android only: verifying a Play Integrity token needs a Google Cloud project. By default the SDK uses the project linked to your app in the Play Console. To bind a specific one, set playIntegrityCloudProjectNumber to your Google Cloud project number. The number is a non-secret identifier. iOS App Attest has no equivalent setting.

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.

Play Integrity verification goes through Google

Verifying an Android Play Integrity token is not an offline operation, by Google's design: decoding and checking the token requires a round-trip to a Google Cloud project. octet-attest-verify does the proof-side half (it decodes the returned payload and confirms the token binds to the proof, by nonce and package). You supply the Google call with your own Cloud project. All three attestation paths are supported. The difference is only where the check runs: App Attest and Android key attestation verify fully offline against Apple's and Google's embedded roots. Play Integrity verifies through Google.

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.