Verifier guide (for auditors)
You are handed an Primary Path audit bundle. This guide shows how to prove — on your own machine, offline — that it is authentic and untampered, without trusting the operator, the server, or the vendor.
Since F24, each agent is its own chain, signed under its own evidence profile. The working procedure is: pick the agent you are auditing, pin the ring, verify that agent’s chain — you never process the other agents’ logs. The control plane (who changed which policy/budget/gate, agent-to-unit mappings, evidence changes, break-glass reads) is a separate per-tenant governance chain, verified the same way.
What verification proves
-
Chain integrity: every event’s hash recomputes from its own fields (
sha256("{seq}|{ts_ms}|{type}|{payload}|{prev_hash}")), everyprev_hashequals the prior hash from a"genesis"start, and the chain reaches the bundle’s declaredhead. Any modified byte fails naming the exact record; any omitted event fails. -
Authenticity: the Ed25519 signature over
canonical({count, first_seq, head, last_seq})verifies against the org’s signing key. Run-scoped bundles addevents_hash(a commitment to every event hash) to the signed record, because a slice has no checkable continuity to bind its contents. -
Execution grants: every
approval_grantedpayload may carry a signed execution grant (grant), and eachgrant_redeemedevent names the executor that ran the action. The CLI verifies every grant signature against the same ring and checks that each redemption names a grant that exists, so the bundle proves not only that a decision was made but that the executor ran exactly that action — and could run it only once.
Points 2 and 3 only mean something if you know the key is the org’s. That is pinning.
Pin once, out of band
At deployment time (or on your first engagement), obtain the public-key ring directly from the operator through a channel you trust — not from inside a bundle you are later asked to verify:
curl -s -H "Authorization: Bearer $PRIMARYPATH_ADMIN_TOKEN" \
https://primarypath.internal/api/audit/keys | jq .keys > pubring.json
Keep pubring.json with your working papers. It only ever grows (key
rotation adds entries; old bundles keep verifying), so one pin serves all
future audits.
Verify
pip install primarypath-verify # one dependency: cryptography
primarypath-verify bundle.json --pubring pubring.json
# OK chain OK: 812 events, head 3fc4a1b2… ; signature OK: key 7caa296f (pinned in ring)
# fetch + verify ONE agent's chain (its own head, its own key):
primarypath-verify https://primarypath.internal --token $ADMIN_TOKEN \
--agent payments-bot --pubring pubring.json
# the control-plane chain:
primarypath-verify https://primarypath.internal --token $ADMIN_TOKEN \
--governance --pubring pubring.json
Failure modes name themselves: broken at seq N (a modified record),
prev_hash mismatch (splice), events were dropped from the tail,
SIGNATURE INVALID, key … is not in the pinned public-key ring
(rewritten-and-re-signed history).
Unpinned runs are labeled. Without --pubring the CLI still checks
the chain and the signature against the bundle’s own embedded key, and
prints that this proves self-consistency only — a malicious operator could
rewrite history and re-sign with a fresh key. The pinned check catches
exactly that (test: server/tests/test_fuzz_verify.py::test_resigned_history_needs_pinning_to_catch).
A pinned run requires a signature. With --pubring, a bundle that
carries no signature FAILS (UNSIGNED: …, exit 1): anyone who can edit a
bundle but cannot forge its signature can still delete it, so absence is
treated like a bad signature. For a chain that is unsigned on purpose
(signing_backend: none), pass --allow-unsigned to ask for an
integrity-only check explicitly; the verdict then says “integrity only, NOT
authenticity” and never “signature OK” (test:
server/tests/test_review_0921.py::test_pinned_ring_rejects_a_signature_stripped_bundle).
In a browser
The inbox’s audit panel recomputes the same chain with WebCrypto and
checks the same Ed25519 signature (“Verify chain”): useful for reviewers
without a terminal. The CLI on an air-gapped machine remains the
gold-standard procedure; the two always agree
(server/tests/test_fuzz_verify.py::test_clean_bundles_always_agree).
Scope caveats
- A per-agent export (
scope: "agent:<id>") is a complete chain from its own genesis: it proves integrity and completeness of that agent’s history up to its head. The bundle carries the agent’s evidence profile; an agent profiledsigning_backend: noneexports unsigned (the chain is still tamper-evident; with--pubringit verifies only under--allow-unsigned). Altering one agent’s log can never affect another agent’s verification. - The governance export (
scope: "governance") is the same, for the control plane. - A full export (
{"chains": [...]}) carries every chain, each verified independently, with one signature over all the heads. - A run-scoped export (
scope: "run:<id>") proves each record is what was written, but cannot prove no record was omitted from that run — ask for the full export when completeness matters. The CLI output labels run-scoped results accordingly. Its signature commits to every event hash (events_hash): the verifier recomputes the commitment, so an edited event fails even with its own hash recomputed, and stripping the field breaks the signature. Legacy run bundles withoutevents_hashverify with an explicit warning that the event contents are not bound. - The verifier needs no network: a saved
bundle.json+pubring.jsonis a complete, portable audit artifact.
Verifying a release (the software itself)
The same discipline applies to what you deploy: every release’s image
and chart are signed with cosign (keyless, logged in Rekor), every file
on the releases page carries its own signature, and each release ships
with SBOMs and checksums. The commands are in
Verifying a download; a running deployment
reports its version on /healthz.
The evidence pack (for humans)
primarypath-verify bundle.json --pubring pubring.json --report pack.html
verifies as usual and additionally writes a self-contained HTML
evidence pack: the verification verdict, the signing key and pinning
status, every decision in the bundle (approval, action and arguments,
outcome, who decided as what role, the endorser under dual control, the
reason, timestamps), and the control-plane changes (policy, budgets,
gates, admin actions with their who/why). Everything in the pack is
folded from the verified events themselves; a failed verification is
rendered just as loudly, because that too is evidence. Hand the pack to
the auditor, keep the bundle: the pack is a rendering, the bundle plus
your pinned ring is the proof.