Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ entry are enough for us to reproduce; please do not paste anything you consider
sensitive, a Trust Record can carry identifiers you may not want public. -->

```bash
trace-verify --claim ... --proof ... --entry ...
# or, from a clone of this repository:
python tools/verify_inclusion.py --claim ... --proof ... --entry ...
```

Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ jobs:
--proof samples/inclusion-proof.json \
--entry registry/2026/06/12.ndjson \
--json | python -c "import json,sys; r=json.load(sys.stdin); assert r['verified'], r"
# The other two questions the CLI answers, run the way a reader
# who only ran `pip install` runs them. Both were reachable
# solely by cloning this repository until 0.4.0, so a regression
# here is a regression in what a third party can check without
# our tree.
trace-verify chain registry/2026/06/12.ndjson registry/2026/09/01.ndjson
trace-verify chain registry/2026/09/01.ndjson \
--json | python -c "import json,sys; r=json.load(sys.stdin); assert r['verified'] and r['checkpoints'] == 1, r"
trace-verify receipt \
--checkpoint docs/evidence/witness-2026-09-07/checkpoint-1.json \
--response docs/evidence/witness-2026-09-07/witness-post.json \
--expected-log-id trace-registry/v1 \
--registry-key bc133259c094f63694b4ec48a295d7501a9a0cd536df5631fb4663c155f7bc90 \
--witness-key 39bb654c9dc0afe1c0edef0deffaa69099b8518836c9ba26e0491535840f96b5 \
| python -c "import json,sys; r=json.load(sys.stdin); assert r['verified'] and r['leaf_index'] == 936, r"

- name: Determine base SHA for append-only check
id: base
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ jobs:
- name: Install build tools
run: pip install --require-hashes -r requirements/publish.txt

- name: Check the tag matches the packaged version
# A release event checks out the tag, so this compares what is about to
# be built against what the release is called. Without it the two can
# disagree silently and PyPI keeps whichever the build produced: a
# sibling repository once shipped a release with no version bump at all,
# and trace-verify 0.3.0 and 0.3.1 both reported 0.1.0 to --version.
if: github.event_name == 'release'
env:
TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
packaged=$(python -c "import re,pathlib; print(re.search(r'^version = \"([^\"]+)\"', pathlib.Path('pyproject.toml').read_text(), re.M).group(1))")
runtime=$(python -c "import re,pathlib; print(re.search(r'^__version__ = \"([^\"]+)\"', pathlib.Path('src/trace_verify/__init__.py').read_text(), re.M).group(1))")
echo "tag=$TAG pyproject=$packaged __version__=$runtime"
test "$TAG" = "v$packaged" || { echo "::error::tag $TAG does not match pyproject version $packaged"; exit 1; }
test "$runtime" = "$packaged" || { echo "::error::__version__ $runtime does not match pyproject version $packaged"; exit 1; }

- name: Build wheel and sdist
run: python -m build

Expand Down
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,64 @@ Two different things are versioned here and they move independently:

## Unreleased

- **One install answers all three questions (`trace-verify` 0.4.0 CLI).**
Inclusion had a command. Chain verification was a Python API with no command.
Witness-receipt verification was not in the package at all. The second and
third were reachable only by cloning this repository, which is the wrong ask
for a public accountability layer.

```
trace-verify --claim C.json --proof P.json --entry E.ndjson (unchanged)
trace-verify chain E.ndjson [MORE...]
trace-verify receipt --checkpoint CP.json --response POST.json \
--registry-key HEX --witness-key HEX --expected-log-id ID
```

The inclusion check keeps its bare-flag form, so every invocation written
against 0.3.x runs unchanged; only the two literal subcommand tokens route.

The substantive half is `verify_chain_against_entries`, moved out of
`tools/verify_checkpoint_chain.py` into `trace_verify._checkpoint` and
exported. It rebuilds the MMR from the raw entries and compares it to what
each checkpoint claims, which is what catches a quiet edit to an
already-anchored entry: such an edit leaves every checkpoint record's own math
self-consistent, so the chain check still passes and only the recompute fails.
That check existed here and nowhere a pip user could reach it.

The witness verifier moves from `tools/verify_witness_receipt.py` into
`trace_verify._witness`. cbor2 and scitt-cose stay imported inside `verify()`,
so the inclusion-only reader still imports the package without a CBOR stack,
and they arrive through a new extra: `pip install "trace-verify[witness]"`.
Both `tools/` scripts stay, re-exporting rather than reimplementing.

- **The published CLI reported the wrong version, and now cannot.**
`trace-verify` 0.3.0 and 0.3.1 both answered `--version` with `0.1.0`:
`__version__` had not moved since the package was first cut in #19, and
nothing compared it to anything. A reader reporting a bug was giving us a
version string that named neither the release they had nor the code in it.
A test now pins `__version__` to the `pyproject.toml` version, and the publish
workflow refuses to build unless the release tag matches both.

- **`scitt-cose` pinned to 0.3.0 and the witness lock made reproducible** (#74).
0.3.0 reports `iat` and unrecognised protected labels, so a third party
verifying with the neutral library alone sees that the September 7 receipt
carries no signed witness time, rather than not looking for one. It verifies
that packet identically: same signing digest, root, coordinates and the same
four false limits. `requirements/witness.in` was added because `witness.txt`
was the only lock in the repository whose hashes could only be moved by hand.

- **Package URLs point back at this repository.** Homepage, Bug Tracker and
Changelog moved to trace-spec while trace-registry was private, because a link
into a private repo is a 404 to every reader of the package. It is public now.
The tutorial and anchor-format links stay at trace-spec, which is where the
normative text belongs. A test asserts that any URL naming a file in this tree
names one that exists.

- **The README leads with `pip install` instead of `git clone`.** The three
questions are one command each, and `--entry-url` fetches a registry entry
over https from an allowlisted host, so answering the first needs no clone at
all.

- **`trace-verify` 0.4.0: the CLL (Checkpointed Local Log) checkpoint chain
-- cryptographic consistency between anchoring runs, not just git commit
history.** Every anchored registry entry now also folds as one leaf into a
Expand Down
75 changes: 55 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,40 +84,75 @@ entry per line, validated by CI against
Entries are append-only. See [docs/anchor-format.md](docs/anchor-format.md)
for field semantics.

## Verifying a claim
## Using the registry

You need three things: your signed claim (Trust Record), the inclusion proof
your producer gave you, and the registry entry for the batch. Then:
There are three questions you can ask of this registry, and one command each.
Install once:

```bash
git clone https://github.com/agentrust-io/trace-registry.git
cd trace-registry
python tools/verify_inclusion.py \
pip install trace-verify
```

**Is my claim in the registry?** You need three things: your signed claim (Trust
Record), the inclusion proof your producer gave you, and the registry entry for
the batch.

```bash
trace-verify \
--claim samples/example-trust-record.json \
--proof samples/inclusion-proof.json \
--entry registry/2026/06/12.ndjson
# OK: claim is included in batch '2026-06-12-001' (root sha256:9279..., ts 2026-06-12T18:09:41Z)
# OK: claim is included in batch '2026-06-12-001' (root sha256:9279..., ts 2026-06-12T18:09:41Z), signature valid
```

Exit code 0 means the claim is proven included; 1 means it is not. The
verifier is a single standard-library Python file, so you can audit it (or
reimplement it from the spec) rather than trust it. The `samples/` files above
are a real anchored example you can use to exercise the tooling.
Exit code 0 means the claim is proven included and its producer's signature
verified; 1 means one of those failed. You do not need this repository: pass
`--entry-url` with a raw GitHub URL instead of `--entry` and the entry is
fetched over https, from an allowlisted host only.

Inclusion proves the signed claim bytes were anchored at the entry's timestamp.
It does not prove the claim is true, and it is not a statement about anything
the claim asserts.

Inclusion verification proves the signed claim bytes were anchored at the
entry's timestamp. Validating the claim's signature against the producer key
is a separate TRACE step.
**Does the registry's own history hold?** Batches anchored via the aggregator
carry a signed `mmr_checkpoint` proving, by math, that each entry honestly
extends the previous one, not just that git history was not rewritten.

Batches anchored via the aggregator (below) also carry a signed `mmr_checkpoint`
proving, by math, that each entry honestly extends the previous one -- not
just that git history was not rewritten. Verify the whole chain with:
```bash
trace-verify chain registry/2026/06/12.ndjson registry/2026/09/01.ndjson
```

Two independent checks run. The first asks whether the checkpoints are
consistent with each other, which a forged or forked chain fails. The second
rebuilds the Merkle Mountain Range from the entries themselves and compares it
to what each checkpoint claims, which is what catches a quiet edit to an entry
that was already anchored. See
[docs/mmr-checkpoint.md](docs/mmr-checkpoint.md) for what each does and does not
catch.

**Does an outside witness agree?** Verifying a witness receipt needs COSE, so it
ships as an extra rather than in the base install:

```bash
python tools/verify_checkpoint_chain.py registry/2026/06/12.ndjson
pip install "trace-verify[witness]"
trace-verify receipt \
--checkpoint docs/evidence/witness-2026-09-07/checkpoint-1.json \
--response docs/evidence/witness-2026-09-07/witness-post.json \
--expected-log-id trace-registry/v1 \
--registry-key bc133259c094f63694b4ec48a295d7501a9a0cd536df5631fb4663c155f7bc90 \
--witness-key 39bb654c9dc0afe1c0edef0deffaa69099b8518836c9ba26e0491535840f96b5
```

See [docs/mmr-checkpoint.md](docs/mmr-checkpoint.md) for how this works and
what it does and does not catch.
Both keys are arguments and neither is ever fetched. A receipt verified under a
key the receipt itself named would prove nothing about who signed it, so you
pin the keys you accept and the command refuses anything else.

Nothing above requires trusting us. The verifier is a small package you can
audit, the anchor construction is specified in
[docs/anchor-format.md](docs/anchor-format.md), and a third party can
reimplement the whole thing from that document. The `samples/` files are a real
anchored example to exercise the tooling against, and the `tools/` scripts in
this repository are the same code reached by a different path.

## Anchoring claims

Expand Down
15 changes: 15 additions & 0 deletions docs/evidence/witness-2026-09-07/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ python tools/verify_witness_receipt.py \
--witness-key 39bb654c9dc0afe1c0edef0deffaa69099b8518836c9ba26e0491535840f96b5
```

Without a clone, the same verifier and the same arguments, against the packet
files wherever you saved them:

```sh
pip install "trace-verify[witness]"
trace-verify receipt --checkpoint checkpoint-1.json --response witness-post.json \
--expected-log-id trace-registry/v1 \
--registry-key bc133259c094f63694b4ec48a295d7501a9a0cd536df5631fb4663c155f7bc90 \
--witness-key 39bb654c9dc0afe1c0edef0deffaa69099b8518836c9ba26e0491535840f96b5
```

The pinned lock and the extra are two ways to install the same two libraries.
The lock fixes exact hashes and is what this repository's CI uses; the extra
takes a version range and is what a reader without the lock gets.

Expected: `verified: true`, seven successful checks, leaf 936, tree size 937.
The command does not fetch keys or make network calls. Tests run it with sockets
disabled and exercise altered bodies/signatures, wrong key pins, a different
Expand Down
2 changes: 1 addition & 1 deletion docs/evidence/witness-2026-09-07/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
c4d8973a50201d32704bf573803a5e64a03093a427ab2ae3026c5f3fc7c4355a capture-manifest.json
fbe911000ca9727e6c5e0ec90aa97fd4651c2139ee949ae3d6f4d8bbd42dea6e checkpoint-1.json
2273bb45c8175c081da484fb05aef5c6d05801d71a142789a255159a577a601c README.md
02396ef05494e6b85407ae3ea8cbad26209bd0bb504a6e3167e5b051b0a68490 README.md
72a12273200f90f82dc01fdb63e91c6a2dfd656ddee9c50cf89c7c7620e82f8d verification.json
be7b4a5ee1c64201e9719af53a0152ba2e47fa5fed87f5625b9a1398438c3d30 witness-did.json
8a6f1edc211e3bab861e1c9eb3bb051bec8ae4e0d7b3bc13e6b1d0f45358e824 witness-inclusion.json
Expand Down
10 changes: 8 additions & 2 deletions docs/mmr-checkpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,14 @@ exact `batch_id`/`mmr_size` where it first becomes provable.
`pip install trace-verify` ships `trace_verify._mmr` and
`trace_verify._checkpoint` -- the same MMR/checkpoint algorithm this
registry's aggregator uses to produce checkpoints, reachable by any third
party independent of this repository. `tools/verify_checkpoint_chain.py` is
a CLI wrapper over that package:
party independent of this repository. Since 0.4.0 it also ships the command:

```
trace-verify chain registry/2026/06/12.ndjson
```

`tools/verify_checkpoint_chain.py` is the same code reached from a clone, and
takes the same arguments:

```
python tools/verify_checkpoint_chain.py registry/2026/06/12.ndjson
Expand Down
33 changes: 23 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,37 @@ classifiers = [

[project.optional-dependencies]
signature = []
# The witness verifier needs COSE. Base trace-verify deliberately does not:
# inclusion and chain verification are the common case, and a reader who
# only wants those should not be made to install a CBOR stack for them.
witness = ["cbor2>=5.5", "scitt-cose>=0.3.0"]

[project.scripts]
trace-verify = "trace_verify.__main__:main"

# trace-registry is a private repository, so EVERY link into it returns 404 to a
# reader of this package. Only Bug Tracker was moved when that was first noticed;
# Homepage, Documentation and Changelog were left pointing at the private repo and
# 404d for a week. All four now point at trace-spec, which is public, and which is
# where the first external report of a trace-verify defect was filed after the
# reader found nowhere else to put it (trace-spec#138).
# These pointed at trace-spec because trace-registry was private and every link
# into it returned 404 to a reader of this package, for a week in the case of
# Homepage, Documentation and Changelog. The condition that comment set has been
# met: trace-registry is public. Homepage, Bug Tracker and Changelog come back
# here, because this is where the code, the issues and the changelog actually
# live and a reader filing a trace-verify defect should not have to find that
# out by guessing (they once filed it at trace-spec#138 instead).
#
# Do not point any of these back at trace-registry until that repository is public.
# The tutorial and the anchor format stay at trace-spec. Those are spec
# documents that happen to describe this implementation, not documents about
# this package, and moving them here would put the normative text behind an
# implementation's URL.
#
# tests/test_trace_verify_pkg.py asserts that any URL naming a file inside this
# repository names one that exists, so a rename fails in CI rather than on PyPI.
# The trace-spec links cannot be checked from here; they are the reason this
# block is commented at all.
[project.urls]
Homepage = "https://github.com/agentrust-io/trace-spec"
Homepage = "https://github.com/agentrust-io/trace-registry"
Documentation = "https://github.com/agentrust-io/trace-spec/blob/main/docs/tutorials/anchoring-to-the-registry.md"
"Anchor format" = "https://github.com/agentrust-io/trace-spec/blob/main/spec/registry-anchor-v1.md"
"Bug Tracker" = "https://github.com/agentrust-io/trace-spec/issues"
Changelog = "https://github.com/agentrust-io/trace-spec/blob/main/CHANGELOG.md"
"Bug Tracker" = "https://github.com/agentrust-io/trace-registry/issues"
Changelog = "https://github.com/agentrust-io/trace-registry/blob/main/CHANGELOG.md"

[tool.hatch.build.targets.wheel]
packages = ["src/trace_verify"]
2 changes: 2 additions & 0 deletions src/trace_verify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"verify_checkpoint_signature_offline",
"verify_checkpoint_link",
"verify_checkpoint_chain",
"verify_chain_against_entries",
]

from trace_verify._verify import (
Expand All @@ -28,6 +29,7 @@
)
from trace_verify._checkpoint import (
CheckpointRecord,
verify_chain_against_entries,
verify_checkpoint_chain,
verify_checkpoint_link,
verify_checkpoint_signature_offline,
Expand Down
Loading