Docs/Getting Started/Verifier Quick Start

Verifier Quick Start

From source to a VALID verdict in a few minutes, using octet-verify, the open-source command-line tool that independently checks an Octet proof.

Note

This page is the how-to. Verifying Proofs explains what the verifier does and doesn't establish, and why it can run offline.


1. Build it

The verifier is public Rust. Clone and build:

git clone https://github.com/octetproof/octet-verify
cd octet-verify
cargo build --release
# binary at ./target/release/octet-verify

No proof of your own yet? The clone ships real ones under test-vectors/golden/1.0.0/. They are historical and CI-signed, so widen the freshness window and expect attestation-root to report NOT-CHECKED:

./target/release/octet-verify test-vectors/golden/1.0.0/android-onearth.bin \
  --hardware-pubkey test-vectors/golden/1.0.0/android-onearth.pubkey \
  --max-age-seconds 9999999999

The default build is offline and dependency-light. It pulls no networking or JSON libraries, so the cryptographic path you're trusting stays small and auditable. Two optional features sit behind build flags:

  • --features net adds backend fetch mode.
  • --features appattest adds offline hardware-attestation validation: iOS App Attest to Apple's root, and the Android key-attestation chain to Google's root. Without it, the attestation-root check stays NOT-CHECKED.

To check a session-bound proof (one made with a sessionNonce, see Proof Binding), build octet-verify at 2.0 or later. An earlier build reports the session binding as NOT-CHECKED and still validates the rest of the proof.


2. Verify a proof you received

If you have the proof bytes as a file (the SDK can export one, or a backend can hand you the raw bytes), that's all you need. There is no network, and no keys to trust beyond what is inside the proof:

octet-verify proof.bin
# or: cat proof.bin | octet-verify

A passing run looks like this:

== octet-verify ==
verdict: VALID

  [       PASS] freshness              42 s old (limit 300 s)
  [       PASS] nullifier              replay token present (32 bytes)
  [       PASS] stage-chain            8 stages, hash linkage intact
  [       PASS] stage-signatures       all 8 stage signatures verify
  [       PASS] chain-assembly         final stage binds all 7 prior signatures
  [       PASS] field-binding          commitment, nullifier, zkProof bound to signed stage hashes
  [       PASS] semantic-binding        verdict / region / level / integrity / commitment bound
  [       PASS] region-claim           claims country:US
  [NOT-CHECKED] attestation-root       hardware key trusted as carried (rebuild --features appattest to validate)
  …

11 pass · 0 fail · 0 warn · 3 not-checked

The exit code is the automation-friendly signal:

Code Meaning
0 authentic: valid and signatures cryptographically verified
1 invalid: a check failed
2 usage / IO / decode / backend error
3 inconclusive: structurally valid but signatures not verified (e.g. no hardware key)

So a CI gate is just octet-verify proof.bin && deploy. Exit 0 means authentic, and an unverified proof exits 3, never 0.

Add --json for one machine-readable object per proof (valid reports authenticity, plus signatures_verified and the verdict string):

octet-verify proof.bin --json | jq 'select(.valid == false)'

Useful flags: --hardware-pubkey <file> (supply the device key for iOS proofs that carry no certificate), --expect-region <id> (assert a claimed region), and --max-age-seconds <n> (widen the freshness window). See the repo's INTEGRATION.md for the full reference.


3. Fetch from the ingestion API

If your proofs are uploaded to the Octet proof ingestion API, the verifier can fetch and check them in one step. This needs the network-enabled build:

cargo build --release --features net

The backend is untrusted. Fetched bytes run through the exact same checks as a local file. You just need a token to read your license's proofs.

Getting a read credential

--token is an octet_read_ read credential: a read-scoped, server-side token that is not tied to a device. Hold it on your backend or CI like an API key. It is not a license key, and it does not come from an app bootstrap.

Octet issues the credential. Request one, then export it:

export OCTET_TOKEN="octet_read_…"

The CLI exchanges it for a short-lived, read-scoped session internally.

Confirm fetch mode is enabled for your account before you rely on it in production.

Fetch and verify

octet-verify fetch <proof-id> --backend https://api.octetproof.com --token "$OCTET_TOKEN"

Two more modes:

  • octet-verify watch …: poll for new proofs and verify each as it arrives (handy for a live demo or dashboard).
  • octet-verify range --since <ts> --until <ts> …: verify every proof in a window. Exits non-zero if any fails.

Proofs are stored for less than 24 hours, then purged. Fetch and verify promptly. An aged-out id returns 404.


4. The full loop, end to end

Putting the pieces together, from device to independent verdict:

flowchart LR
    A[Your app + OctetSDK<br/>generate a proof] -->|upload| B[Ingestion API<br/>stores under 24h, bytes only]
    B -->|fetch by id| C[octet-verify<br/>checks signatures]
    C --> D[VALID / INVALID]
  1. Produce. Your app calls the SDK. On a YES/NO it gets a signed proof and uploads it. (Setup is in the iOS and Android quick starts.)
  2. Store. The ingestion API keeps the raw bytes for less than 24 hours, keyed by id. It never decodes or signs anything.
  3. Verify. octet-verify fetch <proof-id> … pulls those bytes and validates them against the keys inside the proof. The verdict depends only on the bytes, not on anything the backend says.

The result: a proof produced on a phone is confirmed by a separate, public binary, using only the bytes and the keys inside the proof. If the proof had been mutated, replaced, or fabricated anywhere along the way, the verifier rejects it.


Troubleshooting

Symptom Fix
octet-verify: command not found Build it (step 1) and use ./target/release/octet-verify, or put it on your PATH.
verdict: INCONCLUSIVE The signatures weren't checked, usually an iOS proof with no embedded certificate. Pass the device key with --hardware-pubkey <file>.
verdict: INVALID A check failed. The output names which one. Common during development: a stale proof from before a wire change, or a key that isn't the one that signed the proof.
404 / no proof on fetch The proof hasn't been uploaded yet, or it has aged past the retention window (less than 24 hours). Generate a fresh one and fetch within the day.
… requires a build with the net feature Rebuild with cargo build --release --features net to use fetch / watch / range.

Reference