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

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.

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 an activation bearer

--token is an activation bearer: a short-lived credential minted from your license key. The SDK obtains one automatically when it activates your license (Octet.start, see License & Activation). To fetch from a script or CI, mint one yourself against the public activation endpoint with your license key:

export OCTET_TOKEN=$(curl -fsS https://api.octetproof.com/v1/activate \
  -H "Content-Type: application/json" \
  -d '{
    "token":     "<your-license-key>",
    "device_fp": "verifier-cli",
    "os":        "ios",
    "rt":        "native"
  }' | jq -r '.bearer')
  • token: your license key (from sdk.octetproof.com/signup).
  • device_fp: any stable identifier for this caller (≤128 chars).
  • osios/android/windows/macos/linux/web and rtnative/flutter/reactnative/web. Set them to a platform your license covers.

The response's bearer field is what you pass to --token. The CLI then exchanges it for a short-lived, read-scoped token internally. Re-run the activation if the bearer expires.

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