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.
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 netadds backend fetch mode.--features appattestadds offline hardware-attestation validation: iOS App Attest to Apple's root, and the Android key-attestation chain to Google's root. Without it, theattestation-rootcheck staysNOT-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).os∈ios/android/windows/macos/linux/webandrt∈native/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]
- Produce. Your app calls the SDK. On a
YES/NOit gets a signed proof and uploads it. (Setup is in the iOS and Android quick starts.) - Store. The ingestion API keeps the raw bytes for less than 24 hours, keyed by id. It never decodes or signs anything.
- 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
README.md: overview and what the verifier does and does not check.INTEGRATION.md: full CLI reference, flags, and consumer guide.VERIFICATION-SPEC.md: the byte-exact signing contract every check implements.