# Device Attestation

:::note[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](/docs/v2.0/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/v2.0/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/v2.0/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 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](#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. |

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

```swift
let config = OctetConfig(
    licenseKey: "",
    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 = "",
    advanced = AdvancedConfig(
        attestationCadence = AttestationCadence.Periodic(intervalSeconds = 300)
    )
)
val octet = Octet.start(context, config)
```

</TabItem>
</Tabs>

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