- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
provenant-anchor as published with the "Computed and Asserted" article. Earlier history is kept on the maintainers' private forge. Signed-off-by: Justin Simpson <jsimpson@artefactual.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| .forgejo/workflows | ||
| cmd | ||
| internal | ||
| LICENSES | ||
| .gitignore | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| DCO | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
| REUSE.toml | ||
provenant-anchor
Status: disposable spike (created 2026-08-04). De-risking build for the identity-plane problem: institutional signing identity has no witnessed home. Graduation to a roadmap component requires an explicit roadmap edit; until then, everything here is evidence, not product. It is a prototype: one package at a time, not integrated with shipped products.
The problem
A verifier such as provenant-verify checks a preservation package offline, with no access to the system that produced it: hashes, signatures, timestamps. But the signature check rests on one input the verifier cannot audit, the public-key file somebody handed it. Whose key is it? Since when? Is it still the institution's? Nobody witnesses the answers. That is the identity-plane problem.
This spike gives the institution's signing identity a witnessed home, using the design that already guards the web's certificate authorities. The institution runs an append-only transparency log of its signing events, holding digests only and published as static files. A peer institution witnesses that the log only ever grows. Signing keys live for one signing run, and each is warranted in the log before it is used.
What this holds
Two small CLIs around one tile-based transparency log:
provenant-anchor: a minimal log personality embedding Tessera (v1.0.4, POSIX storage). Appends attestation digests and warrant records as leaves; the checkpoint is a C2SP signed note; the read path is static files. Subcommands:keygen(a log or witness key pair),add(anchor a package's attestations and write one inclusion proof per envelope into the package),witness(a peer countersigns append-only growth),export(the printable checkpoint page),declare(log a compromise declaration),selftest.provenant-mint(from batch 3): mints a short-lived warrant key at signing time, logs the warrant (key, officer, institution, validity window) before use, hands the key toprovenant-attest, then deletes it. A warrant is signature AND witnessed inclusion: no log, no warrant.
Verification lives in provenant-verify (checks F13 and F14), never here.
In the article
This repository is experiment four of the article "Computed and Asserted": a
public timeline instead of a handed file. The article reports three results.
Each is below with the commands the demonstration driver runs (paths as the
driver lays them out: work/ for output, trust/ for keys, and the schema file
at the root of the umbrella repository, where the driver runs).
Figure 4a: an unlogged copy is VOID
A package is anchored, then copied with its anchor sidecar
(provenant/anchor/) removed. The copy is checked under the policy that every
signature must be logged:
provenant-verify --package work/unlogged \
--public-key trust/cosign.pub \
--tsa-root trust/tsa/ca.crt \
--require-anchor
An excerpt of the output (omitted lines are marked ...; exit code 1):
provenant-verify: FAIL
...
[PASS] B4 Authorized signature, made at a proven time
[PASS] B5 Timestamp valid
...
[FAIL] F13 Anchored in transparency log
! package carries no anchor sidecar (provenant/anchor/) and --require-anchor is set: an unlogged signature is VOID (no log, no warrant)
...
first failing check: F13 Anchored in transparency log
F13 is the only check that fails. The bytes still match the manifest and the signatures and timestamps still verify (A1 to C10 all pass). A valid signature that the log has never heard of is still rejected: a warrant is a signature plus witnessed inclusion.
The ten-minute key
A fresh key is minted on the officer's machine, its warrant is logged, it signs one run of attestations, and it is destroyed, all inside one command:
provenant-mint --log work/log \
--log-key trust/anchor.key --log-pub trust/anchor.pub \
--officer records-officer@marsh.example \
--institution 'Marsh County Archive' \
--lifetime 10m --package work/mint/object \
-- provenant-attest -object work/mint/object -all \
-import-event work/mint/reports/import-event.json \
-tsa-config trust/tsa/tsa.cnf -tsa-cacert trust/tsa/ca.crt \
-schema preservation-event-v0.1.schema.json -reports work/mint/reports
The -schema file is the PreservationEvent v0.1 predicate schema, kept at the
root of the umbrella repository
provenant; run the command from
there, or give the path to your copy of that file.
An excerpt of the output:
ceremony officer records-officer@marsh.example, Marsh County Archive
key minted fresh keypair, this machine only, lifetime 10m0s
warrant logged at index 46 BEFORE use (no log, no warrant)
...
key destroyed the private key no longer exists anywhere
ok the warrant record in the log is now the only durable trace
The warrant record in the log names the officer, the institution, the digest of the key and a validity window ten minutes long. In reruns on 2026-09-21 (two) and 2026-09-22 (one) the whole ceremony, signing included, took under half a minute. After the package's attestations are anchored in the same log, the verifier is given no signer key, only the log's public key and the time authority's root:
provenant-verify --package work/mint/object \
--tsa-root trust/tsa/ca.crt \
--log-key trust/anchor.pub
It passes (exit code 0), and says where the signer's key came from:
provenant-verify: PASS
...
[PASS] B4 Authorized signature, made at a proven time
...
[PASS] F13 Anchored in transparency log
...
- package disposition verified (signed by logged warrant: records-officer@marsh.example (Marsh County Archive) (ecdsa-p256)): 11 attested, 7 declared exclusions accounted for
Figure 4b: the witnessed checkpoint on one page
A peer institution's witness checks that the log only grew since it last looked, and countersigns. Then the checkpoint is printed:
provenant-anchor witness --log work/log --log-pub trust/anchor.pub \
--witness-key trust/witness.key --state work/witness-state \
--package work/mint/object
provenant-anchor export --package work/mint/object \
--log-pub trust/anchor.pub --witness-pub trust/witness.pub \
--institution 'Marsh County Archive' --out work/checkpoint-export.txt
The witness reports:
witness provincial-archive.example/witness
observed marsh-county-archive.example/anchor at tree size 93
consistency PROVEN append-only from size 46 to 93 (from the public tile files)
countersigned checkpoint now carries the witness's timestamped cosignature
learned an origin name, a size, and a root hash; nothing about holdings
The page states that "As of entry 93" one fingerprint pins the institution's
whole timeline, and lists both signatures as [VERIFIED]: the log operator's,
and the peer's countersignature "after checking the timeline only ever grew".
The 93 entries are the 46 attestation envelopes of the first package, the one
warrant, and the 46 envelopes of the package signed with the minted key. The
witness first saw the log at 46 entries and has now proved it grew to 93 without
rewriting anything.
Reading the figures against your own run
The figures were captured on 2026-09-04 with builds from August. Reruns on 2026-09-21 and 2026-09-22, with all four tools built from their current source and every key generated fresh, give the same verdicts, exit codes, tree sizes (46, then 93), warrant index (46) and page layout. Some text differs, and none of it changes a result:
- The fingerprint, the signatures and the times on the page change on every run, because the keys and timestamps are new each time.
- The verifier's wording has changed since the capture (its own README says how): plain ASCII punctuation, a reworded heading for what it does not yet prove, D11 reads "recorded as a stub", and B4 reads "Authorized signature, made at a proven time".
- The signature lines of the checkpoint keep their leading em dash (U+2014): the C2SP signed-note format requires it.
Keys
No key material is committed here, and none is expected from a reader. Every key is made at run time:
- The log's key pair:
provenant-anchor keygen --name marsh-county-archive.example/anchor --out trust/anchorwritestrust/anchor.key(private; signs checkpoints) andtrust/anchor.pub(handed to verifiers). - The witness's key pair: the same command with the witness's name and
--out trust/witness. - The warrant key: minted by
provenant-mintinside the ceremony and destroyed after one signing run. Only its warrant record outlives it, in the log.
Signing also needs a time authority, and the demonstration's first package is
signed the older way, with a long-lived cosign key pair. Both come from
provenant-attest, whose scripts/setup-signing.sh makes throwaway ones: a
cosign key pair and a local RFC 3161 time authority. The article's public
driver, in the umbrella repository
provenant, generates all of these at
run time. .gitignore excludes *.key, *.pub and .keys/.
Build and test
go build -o bin/ ./cmd/... # bin/provenant-anchor and bin/provenant-mint
go test ./...
The module needs Go 1.26; CI builds with go1.26.8, which carries standard-library fixes this code reaches. The tests are self-contained: they generate their keys, logs and packages in temporary directories.
Scope guards
- The log anchors attestation digests and warrant records only. Whether the domain event log should itself live in a transparency log is a separate, open question; nothing here touches it.
- Tessera is used here as evidence toward an open choice (a log personality on Tessera, or a fork of Sunlight), not as the decision. The read side, C2SP tiles and signed-note checkpoints, is identical either way.
- No commitment layer, no officer authentication, no witness federation beyond one local witness. Deferred, deliberately.
The ladder
Small batches: each batch is one demo-visible increment. Batch 1: the anchor drops (walking skeleton, real leaves, F13 in verify). Batch 2: no log, no warrant (negative paths). Batch 3: the mint. Batch 4: the witness. Batch 5: the paper checkpoint. Batch 6: compromise declaration. Batch 7: the write-up.
AI assistance
This repository, code and documentation, was written with an AI coding assistant
(Claude, by Anthropic), directed by the maintainer. The project's policy is
ADR-08,
which mirrors the Linux kernel's: AI-assisted contributions are permitted and
must be disclosed with an attribution trailer such as Co-Authored-By: or
Assisted-by:; an AI cannot provide the Signed-off-by. A human signs off every
commit on main under the Developer Certificate of Origin and so takes
responsibility for the contribution's origin and licence.
Continuous integration
CI of record runs on the maintainers' sovereign runner; the public copy runs the
same workflows where the host allows. The workflows are in .forgejo/workflows/:
ci.yml (build, vet, tests, govulncheck) and compliance.yml (REUSE licensing
and the DCO sign-off check). Their checkout step tries the forge's own URL first
and falls back to the maintainers' in-network forge.
Conventions, licensing and contributing
The binding conventions live in
development-conventions.md:
TDD, REUSE (Apache-2.0 code, CC-BY-4.0 prose), DCO sign-off, one branch per
change. Every file carries an SPDX header or is declared in REUSE.toml, and the
licence texts are in LICENSES/.
You are welcome to fork the repository, try it out, and open issues. For now the repository is read-only except to its maintainer, and pull requests are not accepted. CONTRIBUTING.md says how commits here are made.