No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Justin Simpson a6c71de833 Public baseline (article: experiment four)
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>
2026-09-22 22:24:36 +01:00
.forgejo/workflows Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
cmd Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
internal Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
LICENSES Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
.gitignore Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
CLAUDE.md Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
CONTRIBUTING.md Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
DCO Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
go.mod Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
go.sum Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
README.md Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00
REUSE.toml Public baseline (article: experiment four) 2026-09-22 22:24:36 +01:00

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 to provenant-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/anchor writes trust/anchor.key (private; signs checkpoints) and trust/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-mint inside 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.