# Device Attestation

:::note[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](/docs/concepts/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](/docs/concepts/verifying-proofs/) 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](/docs/concepts/proof-of-location/)
(`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. |

<Tabs groupId="platform">
<TabItem value="ios" label="iOS (Swift)">

```swift
let config = OctetConfig(
    licenseKey: "<your-license-key>",
    advanced: AdvancedConfig(
        attestationCadence: .periodic(interval: 300)  // the default
    )
)
let octet = try await Octet.start(config: config)
```

</TabItem>
<TabItem value="android" label="Android (Kotlin)">

```kotlin
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)
```

</TabItem>
</Tabs>

**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](/docs/concepts/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`](/docs/concepts/verifying-proofs/) validates attestation when built
with the `appattest` feature:

```bash
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`](https://github.com/octetproof/octet-attest-verify),
a standalone library you can also use directly if you write your own
verification logic.

:::note[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()`](/docs/api-reference/octet-start/).

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](/docs/concepts/verifying-proofs/): what the verifier checks, including the attestation-root step.
- [Verdicts](/docs/concepts/verdicts/): the `attestationFailed` reason and the rest of the trichotomy.
- [Proof of Location](/docs/concepts/proof-of-location/): the location half of the proof, and the device-key security tier.
