signafAIde
Docs menu

Verify

Terminal
signafaide verify                       # the local .signafaide state: your own agent keys and head
signafaide verify pack.json             # a pack from `signafaide export` (every agent; --agent for one)

# an auditor, with the pack and two small files from the customer (the one command, everywhere):
npx signafaide verify pack.json --keys https://<service>/.well-known/signafaide-keys.json --agent-keys agent-keys.json --head head.json

The three files have the names the command uses:

  • pack.json: the verification pack (from the console, an auditor link, or signafaide export -o pack.json).
  • agent-keys.json: from the customer, who runs signafaide keys export -o agent-keys.json in the project that runs the agents.
  • head.json: the customer's .signafaide/head.json, the newest receipt their SDK got for each agent. With it, the check fails if any record was cut from the end of the pack. When the customer has not sent one, leave out --head head.json: the pack's own signed heads are still checked (see The head).

An auditor link carries all three when the customer attaches the keys and the head file, and shows this command with the right address.

The Python and TypeScript CLIs give the same result (npx signafaide verify for TypeScript). Before anything was sent (no API key yet), signafaide verify makes no request: it checks the signatures, chain links and content hashes on your machine, shows the witness's checks as NOT CHECKED ("nothing co-signed yet"), and ends with what is still missing:

co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)

Once records are co-signed, the local state holds their receipts but not the hourly anchors: a batch less than 2 hours old shows anchors and public timestamp as PENDING, as a fresh pack does. For an older batch, verify an export (signafaide export -o pack.json, then signafaide verify pack.json) to check its public timestamp.

The checks #

#checkpasses when
1Envelope schemathe pack holds at least one record, and every envelope has exactly the required fields with valid values and names the pack's project and env
2Record hasheseach record hash matches its envelope
3Client signatureseach record is signed by a key from the customer's agent keys that was active at the record's time and not revoked before the service received it
4Agent chainssequence numbers and previous-record links are unbroken per agent, times never go back, and no record is dated more than 300 s after its batch
5Merkle proofseach receipt's proof leads to its batch root
6Batch hashes and signaturesthe service signed each batch
7Batch chainthe project's batches link up and their times never go back
8Signed headsthe service signed the export statement (what the pack covers, when it was made, and exactly which records it holds) and each head; every record is inside the export's filter; each agent's records reach its newest head (in the pack, and in --head)
9drand roundseach batch was written after its round and at most 120 s later, and the round's BLS signature verifies (every verifier checks it; without a BLS library a verifier says NOT CHECKED)
10Anchorseach batch's path into its anchor, the anchor signature, the service key list it commits to, and its time; a batch older than two hours that no anchor covers shows not timestamped
11OpenTimestampseach public timestamp covers its anchor and matches the public record it names: confirmed passes; pending (co-signed, the public timestamp not confirmed yet) is counted as pending, not passed; not checked offline
12Content hashesthe content you hold and its salt hash to each record's content_hash: sha256(salt || JCS(content)); content without its salt fails

The output is one line per check, then the result line. It says how many of the 12 checks passed, which are pending, which could not run, what the pack covers and when the service made it. Whenever a check is pending, a line under the checks says what that means: co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); never counted as passed.

PASS: 12 of 12 checks passed; export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS: 11 of 12 checks passed, 1 pending (public timestamp); export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS: 11 of 12 checks passed, 1 not checked (public timestamp); export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS (agent keys taken from the pack, not independent): 12 of 12 checks passed; export of agent billing-bot, records from 2026-11-01T00:00:00.000Z, generated 2026-11-13T12:00:00.000Z
FAIL: 1 of 12 checks failed (record hashes), 9 passed, 2 not checked; export of every agent, all records, generated 2026-11-13T12:00:00.000Z

Most checks run offline. The public timestamp check needs a network lookup of the block it names (or --timestamp-check pointing at an explorer you run); with --offline it is reported as not checked.

PENDING and NOT CHECKED never count as a pass, and nothing is called confirmed that was not checked. A pack with no records fails (no records): an empty pack proves nothing. The service signs each pack's export statement, so anyone who passes the pack on cannot drop, add or reorder records, or change its filter, heads or time, without check 8 failing. A record missing inside an agent's chain fails. A pack that ends before the agent's newest head fails (records cut off at the end). Only an export made with --from may start mid-chain (and only when --from is after the agent's first key), and only one made with --to may end before the head. Two different records with the same sequence number fail.

In code #

signafaide.verify() (Python) or await verify() (TypeScript) returns a report whose ok is true or false.

Python

import signafaide

report = signafaide.verify()        # or signafaide.verify("pack.json")
print(report)                       # one line per check, then the result line
assert report.ok

TypeScript

import { verify } from "signafaide";

const report = await verify();      // or await verify("pack.json")
console.log(String(report));        // one line per check, then the result line
if (!report.ok) process.exit(1);

Exit codes #

0 PASS, 1 FAIL, 2 when an input can't be read (a missing file, invalid JSON, no service keys, an agent-key file whose self-signatures do not check).

Service keys #

The service keys must come from a source that is not the pack: --keys <file|url>, or by default the service's /.well-known/signafaide-keys.json. --trust-embedded-keys uses the keys inside the pack and prints a warning, because a forged pack could carry forged keys. Keys over plain http:// are accepted only from this machine (for example http://127.0.0.1), with a warning, and do not count as independent; from anywhere else they are refused. Each anchor commits to the key list (keys_hash), and the public timestamp covers the anchor, so a key list rewritten later fails check 10.

Agent keys #

The agent keys must also come from a source that is not the pack: otherwise the service could add a key of its own and sign records in your agent's name. The customer writes them with:

Terminal
signafaide keys export -o agent-keys.json

and gives the file to the auditor (or publishes it). Each entry is signed by its own key. A key replaced with signafaide keys rotate stays in the file with the time the service retired it, signed by that key: records it signs later fail check 3. The auditor passes --agent-keys agent-keys.json. Without it, check 3 says NOT INDEPENDENT: agent keys came from the pack and the result is FAIL; --trust-embedded-agent-keys accepts the pack's keys and the result then reads PASS (agent keys taken from the pack, not independent). On your own machine, signafaide verify pack.json uses this project's own agent keys in .signafaide/agents/ automatically.

The head #

--head head.json (the customer's .signafaide/head.json) is the newest signed head the customer's SDK received for each agent. The verifier fails a pack that ends before it, so records cut off at the end cannot hide, even by the service. On your own machine signafaide verify pack.json uses this project's .signafaide/head.json automatically, and signafaide verify with no pack always checks the local head.

The public timestamp check #

Public timestamps use OpenTimestamps, an open standard that commits the hash to the Bitcoin blockchain; anyone can check it independently. verify looks up the block each proof names (--timestamp-check <url>, default https://mempool.space/api; https://blockstream.info/api works too), checks that the block's header hashes to that block and carries its proof of work, and shows confirmed only when the proof matches that header. With --offline, over plain http://, or when the lookup fails, the check shows NOT CHECKED and the detail names the block the proof claims: it is never shown as confirmed.

The drand check #

Both SDKs check it by default: @noble/curves is a dependency of the TypeScript SDK, and py_ecc of the Python SDK (the Python install is about 40 MB in all, about 23 MB of it the BLS library and its dependencies). If the BLS library cannot be loaded, verify says drand: not checked and the drand check shows NOT CHECKED, never a pass. The round's time is checked either way.

BLOCKED chains and CI #

signafaide verify with no pack also says when a chain cannot ship. A chain is BLOCKED when the witness refused its records (a revoked agent key, a record dated before its key or more than 5 minutes ahead of the witness's clock) or when it was written so that no witness can take it (an older SDK let the time go back after a clock step). The line names the chain, the first record and the next step:

BLOCKED prj_01JCZQ8Z5X3V7N2M4K6H8G0F1D/live/claims-bot: seq 7 has time ..., earlier than the record before it ...; 18 records cannot ship. Next: run `signafaide repair --agent claims-bot` ...
FAIL: 1 chain cannot ship (prj_01JCZQ8Z5X3V7N2M4K6H8G0F1D/live/claims-bot); 9 of 12 checks passed, 3 not checked (...)

A BLOCKED chain always fails (exit 1), and so does signafaide status. For a CI gate, add --fail-on-pending-send: it also fails while any record waits to be co-signed. signafaide repair sets the records that can never ship aside in .signafaide/held/ (kept, and still checked by verify for their signatures, links and content hashes), gives the agent a new key, and writes a signafaide.repair note that commits to the held records' hashes, so the new segment's first co-signed record fixes them in time. Co-signed records are never touched.

When verify fails #

A failure is evidence: do not edit .signafaide/ to make it pass. The failing lines name the record and the reason. See Errors for service errors and Concepts for what each part proves.