Trustless, offline verification of signed agreements. Take a piece of content, have two people sign it with WebAuthn passkeys (ES256), freeze it into a deterministic manifest, and anchor that manifest's hash to Bitcoin via OpenTimestamps. The result is a single JSON proof bundle that anyone can verify offline — no server, no trust in the issuer required.
Extracted from the provy contract-signing app as a reusable, product-agnostic core.
Only two things in a bundle are unforgeable even if the issuing server is fully compromised:
- The ES256 assertions — signed by authenticator-held private keys the server never sees.
- The Bitcoin anchor — a public, permissionless clock.
Everything else (audit hash-chain, HMAC timestamps) is recomputable by whoever
holds it, so it counts as operational logging, not evidence. See SPEC.md.
crates/core # proof-bundle-core — the Rust library (WASM-friendly, no server deps)
crates/cli # proof-bundle — the `verify` / `audit` command-line tool
ots/ # Node helpers for the OpenTimestamps steps (the OTS lib is Node-only)
The cryptographic verification (content hash, manifest/final_hash, ES256 assertions, distinct signers) is pure Rust. Only the OpenTimestamps Bitcoin-anchor check is delegated to Node, because the OpenTimestamps library does not run outside Node.
# full check, including the OpenTimestamps anchor (needs Node + `npm i` in ots/)
cargo run -p proof-bundle-cli -- verify bundle.json
# crypto-only, skip the OTS/Node step
cargo run -p proof-bundle-cli -- verify bundle.json --no-otsFor the OTS step:
cd ots && npm install
node ots/ots-verify.mjs bundle.json# stamp final_hash to the OTS calendars (status: pending)
node ots/ots-stamp.mjs --bundle bundle.json # prints {finalHash, proof, status}
# hours later, upgrade the proof once it's in a Bitcoin block
node ots/ots-upgrade.mjs --bundle bundle.json # prints {..., status: "bitcoin"}These helpers are storage-agnostic: they take a hash or bundle in and print the proof out. Wiring the proof back into your datastore is up to you.
use proof_bundle_core::{bundle, manifest};
// Build a manifest + final_hash from your own signatures:
let (manifest_str, final_hash) = manifest::build(id, content_hash, &parties, &sig_refs);
// Verify a bundle offline (crypto layers only):
let b: bundle::Bundle = serde_json::from_str(&json)?;
let report = bundle::verify(&b);
assert!(report.ok());cargo testThe vector tests synthesize a real two-signer bundle with genuine ES256
assertions, confirm it verifies, then apply each tampering case (modified content,
forged signature, wrong final_hash, single signer, mutated audit row) and confirm
verification breaks.
Dual-licensed under either of Apache-2.0 or MIT, at your option.