@sanning/proof
v0.4.1
Published
TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1, sanning.mlflow/v1, sanni
Readme
@sanning/proof
Verify Sanning evidence-plane provenance records — with no Sanning code or service in the trust path.
This is the TypeScript verification kernel for the Verifiable Event Envelope family: the signed JSON records that producers such as the verification agent daemon and the MLflow plugin anchor permanently to Arweave. Given an envelope (fetched from any Arweave gateway), this package answers: is it authentic, and does it commit to the bytes I'm holding?
- RFC 8785 (JCS) canonicalization
- SHA-256 payload binding (WebCrypto)
- Ed25519 signature verification (
@noble/ed25519) - RFC 9162 binary Merkle inclusion proofs (checkpoint leaves)
- Zero config, two small dependencies, ESM, browser + Node ≥ 20
Install
npm install @sanning/proofVerify an envelope
Fetch the raw transaction from any Arweave gateway and verify it client-side:
import { verifyEnvelope } from "@sanning/proof";
const env = await (await fetch("https://arweave.net/raw/<tx_id>")).json();
const result = await verifyEnvelope(env);
result.ok; // spec_version + payload_hash + Ed25519 all passed
result.signatureOk; // Ed25519 over the signed scope, against env.public_key
result.payloadHashOk; // SHA-256(JCS(payload)) === env.payload_hash
result.errors; // [] when ok — machine-readable reasons otherwiseverifyEnvelope never throws on hostile input — a malformed envelope (or a lying gateway) yields ok: false with reasons, not an exception.
Bind an envelope to bytes you hold (reverse provenance)
The check that defeats a lying gateway: Arweave tags are unsigned search hints, so after finding a candidate envelope, confirm it actually commits to your artifact's hash:
import { sha256Hex, verifyEnvelope, contentHashes } from "@sanning/proof";
const myHash = await sha256Hex(fileBytes); // 64-char lowercase hex
const result = await verifyEnvelope(env, myHash);
result.contentHashOk; // true ⇔ the envelope commits to exactly these bytes
result.contentRole; // how it commits: e.g. the registered baseline,
// an observed (tampered) hash, …
contentHashes(env); // all content hashes an envelope commits to, by event typeVerify a whole trace bundle — one command
The @sanning/anchor SDK hands a producer a set of InclusionReceipts and can serialize them into one portable, self-verifying sanning.evidence/v1 bundle (body_type: sanning.anchor.trace/v1). Verify the entire bundle — every event's signature + payload binding + Merkle inclusion, all offline — with the bundled CLI:
npx @sanning/proof verify trace-bundle.json
# also re-fetch each checkpoint on-chain to confirm it's anchored:
npx @sanning/proof verify trace-bundle.json https://arweave.net,https://permagate.ioIt prints a per-event + rollup verdict and exits on a pinned code — 0 verified · 1 a real failure (bad signature / tamper / broken inclusion) · 2 malformed bundle · 3 gateway-unavailable when an on-chain re-fetch was requested. The producer's asserted verdict is shown but never trusted — the displayed verdict is always recomputed from the body. The same CLI also verifies the agent's sanning.agent.proof/v1 inclusion bundle (it sniffs spec_version), so one command covers anchor + agent.
The
verifyCLI is live in@sanning/proof≥ 0.2.2 —npx @sanning/proof verify <bundle> [gateways].verifyEvidenceBundleis the programmatic API.
Programmatically:
import { verifyEvidenceBundle } from "@sanning/proof";
const bundle = JSON.parse(await fs.readFile("trace-bundle.json", "utf8"));
const result = await verifyEvidenceBundle(bundle, { gateways: ["https://arweave.net"] });
result.status; // "verified" | "partial" | "failed" | "malformed" (recomputed)
result.signatureOk; // wrapper Ed25519 signature
result.bodyHashOk; // body_hash === SHA-256(JCS(body))
result.checkpoints; // per-checkpoint envelope / merkle-root / on-chain results
result.events; // per-event signature / payload-binding / inclusion resultsA withheld record_bytes (external commitment, record not disclosed) leaves that event's payload binding undetermined — surfaced, never failed (the bundle stays cryptographically sound).
…including the raw logs, when the holder discloses them
The checks above prove the stamps. To also confirm a holder's raw logs are the bytes whose hash was anchored, the verifier closes the final rawLog → content_hash link: SHA-256(disclosed bytes) must equal the content_hash the event's record committed to.
A bundle a producer built with disclosure carries the bytes in the signed body (events[].content), so they're checked automatically — no flag, and the disclosure is itself covered by the wrapper signature:
npx @sanning/proof verify trace-bundle.json # in-body content verified inlineIf the logs travel alongside a minimal bundle instead, pass them with --logs — a JSON object mapping event_id → bytes, where an even-length lowercase-hex string is read as hex and anything else as UTF-8 text:
npx @sanning/proof verify trace-bundle.json --logs logs.jsonEither way each event gains a contentOk: true match · false mismatch (the event and the rollup fail, exit 1) · null undetermined — nothing disclosed, or no committed content_hash (a withheld record); never a failure, mirroring the payload-binding rule. The CLI prints a logs ✓/✗/~ mark per event and a logs: N/M disclosed verified line.
Programmatically the same side input is content, keyed by event_id — a Uint8Array is used as-is, a string is hex:
const result = await verifyEvidenceBundle(bundle, {
content: { [eventId]: rawLogBytes }, // Uint8Array, or a hex string
});
result.events[0].contentOk; // true | false | nullIn-body events[].content (signed) takes precedence over the side input; if both are present for an event and disagree, the event fails.
Verify a Merkle inclusion proof
Agents close daily checkpoints over per-cycle leaves (RFC 9162, §2.1 domain separation — not the Bitcoin duplicate-leaf variant). Verify a leaf's inclusion against an anchored root:
import { leafHash, verifyInclusion, hexToBytes } from "@sanning/proof";
const ok = await verifyInclusion(
await leafHash(leafBytes), // Uint8Array leaf hash
leafIndex, // 0-based position
totalLeaves,
auditPath.map(hexToBytes), // sibling hashes, leaf → root
hexToBytes(expectedRootHex),
);API
| Export | What it does |
|---|---|
| verifyEnvelope(env, optionsOrContentHash?) | The three load-bearing checks (spec-version registry, payload-hash recompute, Ed25519 over the signed scope) + optional content bind. Pass { payloadBytes } for external-commitment envelopes. Returns VerificationResult. |
| verifyEvidenceBundle(bundle, { gateways?, fetchImpl? }) | Verify a signed sanning.evidence/v1 / sanning.anchor.trace/v1 bundle: wrapper signature + body_hash + every checkpoint + every event's inclusion, offline (optional on-chain re-fetch). Returns EvidenceBundleResult. Powers the npx @sanning/proof verify CLI. |
| verifyAgentProofBundle(bundle, { gateways?, fetchImpl? }) / isAgentProofSpec(v) | Verify the agent producer's sanning.agent.proof/v1 inclusion bundle. |
| contentHashes(env) | The content hash(es) an envelope commits to, by event type — the reverse-provenance join keys. |
| specVersionSupported(v) | Fail-closed accept-check: sanning.agent/v1 and additive minors (sanning.agent/v1.<n>). Unknown majors/profiles are rejected. |
| jcs(value) | RFC 8785 canonical JSON bytes. |
| sha256Hex(bytes) / sha256Bytes(bytes) | SHA-256 via WebCrypto. |
| ed25519Verify(sig, msg, pubKey) | Raw Ed25519 verification. |
| leafHash / nodeHash / merkleRoot / auditPath / verifyInclusion / EMPTY_TREE_ROOT_HEX | RFC 9162 binary Merkle tree primitives. |
| utf8 / bytesToHex / hexToBytes | Encoding helpers. |
| Envelope / VerificationResult / ContentRole / Subject | Types. |
Signed scope
The primary signature covers JCS(envelope minus signature minus co_signatures) (envelope-spec §2/§7.1). The reserved co_signatures carve-out lets countersignatures be added later without invalidating the primary signature.
Why you can trust it (without trusting us)
Three independent implementations of this kernel exist — this package, the Python sanning-proof (PyPI), and the MIT-licensed Go reference, vendored into this repo under cross-kernel/vendor-agent/ so anyone can build and run it. The TypeScript and Python kernels are conformance-gated byte-for-byte in CI against the shared test-vectors corpus (per-file SHA-256 pins; current cut tag test-vectors-v2.0), and the cross-kernel agreement gate re-runs the corpus plus adversarial negatives through both, asserting identical verdicts on identical bytes. The Go reference runs as a third leg of that same gate, built at the commit its vendoring pins.
That mutual gate is the point: anyone can verify these provenance records with no Sanning code in the trust path — write your own verifier against the public specs and the corpus will tell you if it's conformant.
What this package deliberately does NOT do
- Single-envelope scope. Chain walking (
previous_hash), checkpoint reconciliation, and gateway/transport logic are consumer-layer concerns composed above the kernel. - Provenance is history, not endorsement. A verified envelope proves these bytes have this signed, anchored history — never "safe," "approved," or "currently in production."
- Key identity is out-of-band. The kernel proves the signature matches
env.public_key; binding that key to a real-world identity is yours to establish.
Requirements
ESM-only. Needs WebCrypto (globalThis.crypto.subtle): any modern browser, Node.js ≥ 20 (≥ 19 works), Deno, Bun, workers.
License
MIT — verification must be open.
