# Verifying Proofs

:::note[In short]

The proof is self-contained. With the open-source verifier and the bytes alone,
anyone can confirm offline that a proof is signed by the key it carries and has not been altered since. The SDK establishes the location on the device, and the verifier proves that record is genuine.

:::

A [proof of location](/docs/concepts/proof-of-location/) is only useful if a third
party can verify it independently. `octet-verify` is the reference verifier: a
standalone command-line tool whose source you can read, build, and audit at
[github.com/octetproof/octet-verify](https://github.com/octetproof/octet-verify).
It contains none of the SDK's proof-creation or spoof-detection logic, and
confirms only that a proof is signed by the key it carries and has not been
altered since. That narrow scope is what makes it safe to open-source.

To run it, see the [Verifier Quick Start](/docs/getting-started/verifier-quickstart/).

## The trust model

Three components, three levels of trust:

```mermaid
flowchart LR
    A[Device + OctetSDK<br/>produces & signs the proof] -->|uploads| B[Ingestion API<br/>api.octetproof.com<br/>stores bytes only]
    B -->|serves the same bytes| C[octet-verify<br/>independent CLI]
    A -.->|or export the proof to a file| C
    C --> D{Verdict}
```

The ingestion API is optional. A proof can travel straight from the device to
the verifier (the dashed path above). The API is a store-and-serve relay: it
holds uploaded proof bytes by id for less than 24 hours so they can be fetched and
verified later. Nothing about verifying a proof depends on it.

| Component | Trusted to... | **Not** trusted to... |
|---|---|---|
| **OctetSDK** (on device) | produce a proof correctly, including determining the location honestly, and sign each stage with the hardware-backed key | vouch for its own signatures. The proof carries everything needed to re-check them, so you verify the signing yourself rather than take the SDK's word. |
| **Ingestion API** (`api.octetproof.com`) | receive proof bytes, store them for less than 24 hours, and serve them back | decode, change, sign, or vouch for any proof. It is **transport and index only**. |
| **octet-verify** | check signatures, chain linkage, and field bindings against the keys embedded in the proof | nothing. This is the one piece a recipient must trust, and its source is public. |

The load-bearing property: **if the backend were fully compromised, the verifier
would still reject a tampered or fabricated proof.** None of the verifier's checks rely on
anything the backend says. They rely only on the bytes and the cryptographic
keys inside the proof. A broken or hostile backend can only fail to return a
proof. It cannot fabricate a proof that the verifier will accept.

## What the verifier checks

Against the bytes of a single proof, the default build of `octet-verify` confirms:

- **Stage signatures.** Every stage of the proof is signed by the same
  hardware-backed P-256 key the proof carries.
- **Chain linkage.** Each stage links to the one before it. The final assembly
  stage binds every prior signature into a single value. Change any stage and
  the chain breaks.
- **Field bindings.** The commitment, the nullifier, and the ZK bytes match the
  hashes they were signed under. A field present with no signed binding fails, so
  nothing rides along unverified.
- **Semantic-field binding.** The spoofing verdict, the region, the level, the
  integrity status, and the position commitment are bound to the signed proof.
  Editing any of them after signing is rejected.
- **Freshness.** The proof falls within the age window you allow, judged against
  the signed timestamp rather than the editable top-level field.
- **Wire-format guard.** A proof smuggling a duplicate of a single-value field is
  rejected.

Two more checks run when you pass the upload envelope (`--envelope`):

- **Transport signature.** An Ed25519 signature ties the whole proof to the
  enrolled device identity.
- **Replay-control binding.** The backend-supplied upload nonce, nullifier, and
  signed timestamp are confirmed against what the proof itself signed.

Semantic-field binding and replay-control binding are new in the v1.1 verifier.
The exact byte layout of every signature is documented in the repo's
[`VERIFICATION-SPEC.md`](https://github.com/octetproof/octet-verify/blob/main/VERIFICATION-SPEC.md).

## What a verdict means

The verifier's verdict is not the same as the [SDK's verdict](/docs/concepts/verdicts/).
Keep them separate:

- The **SDK** answers *"is the device in the region?"* It returns `YES`, `NO`, or `INDETERMINATE`.
- The **verifier** answers *"do this proof's signatures verify, and is it unaltered?"* It returns one of three states:

| Verdict | Meaning |
|---|---|
| **VALID** | The proof passed every check and its signatures verified cryptographically. |
| **INCONCLUSIVE** | The proof is structurally fine, but its signatures could not be checked (for example, the hardware public key was not available). This is **not** a pass. Assurance was not established. |
| **INVALID** | A check failed. The proof is rejected. |

Each individual check is reported as `PASS`, `FAIL`, or `NOT-CHECKED`. A
`NOT-CHECKED` line never fails a proof and never makes it valid. It is shown so
the boundary of what was confirmed is explicit and never overstated.

### Hardware attestation: the `appattest` build

The default build trusts the signing key as carried: a passing proof shows the
proof is signed by that key and unaltered, not that the key is genuine device
hardware. To close that gap, build with the `appattest` feature:

```bash
cargo build --release --features appattest
```

That build validates the [device attestation](/docs/concepts/device-attestation/)
offline:

- **iOS App Attest** verified against Apple's embedded App Attest root.
- **Android key attestation** validated up the Keystore certificate chain to
  Google's embedded hardware-attestation root, confirming a TEE or StrongBox key.

These two offline checks run through [`octet-attest-verify`](https://github.com/octetproof/octet-attest-verify),
and the `attestation-root` line in the output moves from `NOT-CHECKED` to a real
`PASS` or `FAIL`. The default build keeps it `NOT-CHECKED` so the boundary of what
was confirmed stays explicit. The Android Play Integrity token is a separate
signal that verifies through Google (a Cloud-project round-trip), not offline. See
[Device Attestation](/docs/concepts/device-attestation/) for the full picture.

## Where to go next

- [Verifier Quick Start](/docs/getting-started/verifier-quickstart/): build it and verify your first proof.
- [Device Attestation](/docs/concepts/device-attestation/): what attestation proves, and the `appattest` verification path.
- [Proof of Location](/docs/concepts/proof-of-location/): what a proof is and why it can be checked.
- [Verdicts](/docs/concepts/verdicts/): the SDK's `YES`, `NO`, and `INDETERMINATE`, which answer a different question.
