Skip to content
Open
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

## [Unreleased]

- **Breaking, proposed: a Level 2 anchor verdict requires the registry entry (#464,
following trace-tests#92).** Registry Anchor Format §6 now says the verdict rests on
the entry itself, the third input of §5.1: a receipt's `merkle_root` and `leaf_count`
must equal the entry's or the anchor is refused, and a verifier that does not hold
the entry does not report Level 2. Before this, the conformance suite judged the
proof against a root and count the receipt supplied, so a passing TR-ANC-002 tied the
record to no entry at all. Nothing yet identifies an entry from the `transparency`
value, so a verifier states that this link was not checked; it is tracked as finding
21 of #448. What breaks: a Level 2 result produced from a receipt alone no longer
passes. What implementers do: keep the registry entry with the record and its proof,
as the anchoring tutorial already says, and pass it with `--entry`. Records, the
receipt shape and the entry shape do not change. Classified breaking under
GOVERNANCE.md as a conformance level redefinition, so adoption needs the 30-day
comment period and Project Lead sign-off. Reported by @solloek369-arch.

- **Section 3.1.5: a layer is established by its event log or by a reference value
(#279).** As merged, a layer was `established` only when its event log replayed to the
quoted value, so a quote that equals signed reference values with no log could not be
Expand Down
12 changes: 10 additions & 2 deletions conformance/.clusterfuzzlite/fuzz_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* ``tr_anc.check`` returns findings for any receipt JSON; ``parse_receipt`` and
``verify_inclusion`` raise nothing but ``InclusionError``.
* TR-ANC-002 never passes unless the receipt commits to the genuine Merkle
root and the claim is the one that root includes. Forging either takes a
root of the supplied entry and the claim is the one that root includes. Forging either takes a
SHA-256 collision, so a PASS on anything else is a defect in the replay.

Half the inputs are raw receipt bytes; the other half start from a genuine
Expand Down Expand Up @@ -67,6 +67,14 @@ def _genuine() -> tuple[dict, bytes]:


_RECEIPT, _ROOT = _genuine()
# The registry entry sealed with the genuine receipt (registry-anchor-v1 section 4).
_ENTRY = {
"ts": "2026-10-15T00:00:00Z",
"merkle_root": _RECEIPT["merkle_root"],
"leaf_count": _RECEIPT["leaf_count"],
"producer": "fuzz/1.0.0",
"batch_id": "fuzz-1",
}
if not verify_inclusion(_CLAIMS[_TARGET], *parse_receipt(_RECEIPT)):
raise SystemExit("fuzz_receipt: the genuine receipt does not verify; the harness is wrong")

Expand Down Expand Up @@ -110,7 +118,7 @@ def TestOneInput(data: bytes) -> None:

if not isinstance(receipt, dict):
return
findings = tr_anc.check(claim, receipt=receipt)
findings = tr_anc.check(claim, receipt=receipt, entry=_ENTRY)
if any(f.code == "TR-ANC-002" and f.passed() for f in findings):
assert claim is _CLAIMS[_TARGET], f"inclusion proven for the wrong claim: {receipt!r}"
root = parse_receipt(receipt)[3]
Expand Down
10 changes: 10 additions & 0 deletions conformance/.clusterfuzzlite/fuzz_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
"audit_path": [],
"merkle_root": "sha256:" + "0" * 64,
}
# Matches the receipt, so TR-ANC-002 reaches the leaf canonicalization on every record.
_ENTRY = {
"ts": "1970-01-01T00:00:00Z",
"merkle_root": _RECEIPT["merkle_root"],
"leaf_count": 1,
"producer": "fuzz/1.0.0",
"batch_id": "fuzz-1",
}


def _resolver(uri: str) -> bytes:
Expand All @@ -46,6 +54,7 @@ def TestOneInput(data: bytes) -> None:
level,
expected_nonce="fuzz-nonce",
receipt=_RECEIPT,
entry=_ENTRY,
policy_resolver=_resolver,
)

Expand All @@ -55,6 +64,7 @@ def TestOneInput(data: bytes) -> None:
(0, 1, 2),
expected_nonce="fuzz-nonce",
receipt=_RECEIPT,
entry=_ENTRY,
policy_resolver=_resolver,
)
built = report._build_from_execution(
Expand Down
13 changes: 13 additions & 0 deletions conformance/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@

### Changed

- **Breaking: TR-ANC-002 requires the registry entry (trace-spec#464).** `--entry`
takes one Anchor Format §4 line, and `run()` and `tr_anc.check()` take `entry`.
The receipt's `merkle_root` and `leaf_count` must equal the entry's; with no entry,
TR-ANC-002 fails as it does with no receipt. The pass message names the entry and
says the link from the `transparency` URI to it is not checked (trace-spec#448
finding 21). Tests cover the four worlds from trace-tests#92 and the absent entry.
- **Breaking: TR-ANC-002 proves inclusion of the complete signed claim.** For
`cmcp-runtime` the runner now gives TR-ANC the whole envelope, signature included,
as Anchor Format §1 and the reference registry already define the leaf; before, it
proved the nested `trace`, so a registry-valid anchor failed and a re-signed
envelope with changed gateway fields still passed. `tr_anc.check()` takes
`subject`. The entry `ts` must be an ISO-8601 UTC timestamp ending in `Z`.
Reported on trace-spec#533 by solloek369-arch and reproduced by dinakarjs.
- Vendor-neutral placeholders (`example-provider`, `example-model`) in the unsigned
vectors and the policy-resolution generator; the policy-resolution set is regenerated
and every verdict is unchanged. Signed vectors are untouched.
Expand Down
24 changes: 14 additions & 10 deletions conformance/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,22 @@ against AMD, Intel or a TPM manufacturer root. A record can satisfy `TR-RTE` at
syntactically perfect measurement that no hardware ever produced. Verifying the quote against the
silicon vendor is the relying party's job, and `cmcp_verify` is where that happens.

**`TR-ANC-002` proves inclusion relative to the receipt you hand it.**
In plain terms: it proves the record is in the log the receipt describes, not that the log is a
real public one.
It replays the audit path against the Merkle root carried in that receipt. It does not fetch the
`transparency` URI, and it does not establish that the root is one a public log actually
published. A self-consistent receipt over a tree the submitter built themselves will pass. What
the check rules out is a record that has been modified since anchoring, or that was never in the
tree the receipt commits to. Establishing that the tree is real is a separate step and is not in
scope here.
**`TR-ANC-002` proves inclusion relative to the receipt and entry you hand it.**
In plain terms: it proves the record is in the registry entry you supplied, not that the entry is
the one the record names, nor that the log is a real public one.
It requires the receipt's `merkle_root` and `leaf_count` to equal the supplied entry's, and
replays the audit path against that root. It does not fetch the `transparency` URI. Nothing in
the Anchor Format yet identifies an entry from that URI, so the check cannot establish that the
supplied entry is the one the record names; the pass message says so, and closing that link is
tracked as finding 21 of trace-spec#448. It also does not establish that a public log actually
published the entry. A self-consistent receipt and entry over a tree the submitter built
themselves will pass. What the check rules out is a record that has been modified since
anchoring, a record that was never in the tree, and a receipt whose root or count differs from
the entry it is presented with. Establishing that the entry is real and is the named one is a
separate step and is not in scope here.

`TR-ANC-001` is explicit that it checks the pointer rather than the anchor. Supplying no receipt
fails `TR-ANC-002`, so Level 2 cannot be reached on a well formed URI alone.
or no entry fails `TR-ANC-002`, so Level 2 cannot be reached on a well formed URI alone.

## Statuses that are easy to misread

Expand Down
2 changes: 1 addition & 1 deletion conformance/docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Look up a code from a failed or unverified check here: what it means and how to
| Code | Description | How to fix |
|------|-------------|------------|
| TR-ANC-001 | `transparency` is absent or empty, is not a string, or is not an `https://` URI with a host | Submit the record to a SCITT transparency log and set `transparency` to the returned receipt URI. The URI is not resolved and the receipt behind it is not fetched; this is a format check on the pointer, and TR-ANC-002 is what checks the anchor |
| TR-ANC-002 | No anchor receipt was supplied, the receipt is malformed, or replaying its inclusion proof does not reproduce the committed `merkle_root` | Pass the receipt with `--receipt`. Without one, nothing proves the record is in the log the URI names, so Level 2 cannot pass. If a receipt is supplied and the proof does not verify, the record is not in that tree or it has been modified since it was anchored |
| TR-ANC-002 | No anchor receipt or no registry entry was supplied, either is malformed, the receipt's `merkle_root` or `leaf_count` differs from the entry's, or replaying the inclusion proof does not reproduce the committed `merkle_root` | Pass the receipt with `--receipt` and the registry entry it commits to with `--entry`. Without both, Level 2 cannot pass. A receipt whose root or count differs from the entry's does not describe that entry. If the proof does not verify, the record is not in that tree or it has been modified since it was anchored. A pass does not check that the entry is the one the `transparency` URI names |

## TR-SCA: Provenance

Expand Down
2 changes: 1 addition & 1 deletion conformance/docs/levels.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ The `transparency` field must be an HTTPS URI with a host, pointing to a SCITT r
- `tool_transcript.hash` missing or not a valid `sha256:` or `sha384:` digest: TR-TXN-001
- `tool_transcript.call_count` negative or not an integer: TR-TXN-002
- `transparency` is absent or empty, is not a string, or is not an `https://` URI with a host: TR-ANC-001
- no anchor receipt was supplied, the receipt is malformed, or its inclusion proof does not reproduce the committed `merkle_root`: TR-ANC-002. A record cannot reach Level 2 on a URI alone: pass the receipt with `--receipt`
- no anchor receipt or no registry entry was supplied, either is malformed, the receipt's `merkle_root` or `leaf_count` differs from the entry's, or the inclusion proof does not reproduce the committed `merkle_root`: TR-ANC-002. A record cannot reach Level 2 on a URI alone: pass the receipt with `--receipt` and the entry with `--entry`
- `policy.policy_uri` is present, a resolver was supplied with `--policy-dir`, and the bundle could not be read: TR-POL-003. Unverified rather than failed, and unverified fails the run from Level 2

---
Expand Down
2 changes: 1 addition & 1 deletion conformance/docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ The table below uses the record's own field names (in code font). The [conforman
| [Policy](modules/tr-pol.md) | TR-POL | §3.1 | Policy bundle hash format, enforcement mode values, and whether the bundle at `policy_uri` has the declared digest |
| [Appraisal](modules/tr-apr.md) | TR-APR | §3.1 | Appraisal status enum, verifier URI form, `policy_ref` URI form, and timestamp plausibility. The appraisal policy is never resolved |
| [Transcript](modules/tr-txn.md) | TR-TXN | §3.1 | Tool-call transcript hash format (`sha256:` or `sha384:`) and call count |
| [Transparency](modules/tr-anc.md) | TR-ANC | §3.2 | SCITT receipt URI form (TR-ANC-001), and offline replay of the inclusion proof against the committed Merkle root when a receipt is supplied (TR-ANC-002). The URI itself is never resolved |
| [Transparency](modules/tr-anc.md) | TR-ANC | §3.2 | SCITT receipt URI form (TR-ANC-001), and offline replay of the inclusion proof against the supplied registry entry, whose root and count the receipt must match (TR-ANC-002). The URI itself is never resolved, and the URI-to-entry link is not checked |
| [Provenance](modules/tr-sca.md) | TR-SCA | §3.1 | SLSA provenance level and digest format (`sha256:` or `sha384:`) |
14 changes: 9 additions & 5 deletions conformance/docs/modules/tr-anc.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# TR-ANC: Transparency

Checks that the record was entered in a public, append-only log, so it cannot be quietly replaced later. TR-ANC-001 checks the link to the log entry. TR-ANC-002 uses a receipt you supply to prove, offline, that this exact record is in the log the receipt describes. Both apply from Level 2.
Checks that the record was entered in a public, append-only log, so it cannot be quietly replaced later. TR-ANC-001 checks the link to the log entry. TR-ANC-002 uses a receipt and the registry entry you supply to prove, offline, that this exact record is in that entry. Both apply from Level 2.

??? info "Technical detail: what the receipt proof covers"
The log follows SCITT, the IETF format for transparency services. The receipt carries an
inclusion proof (a short list of hashes linking the record to a Merkle root, the single
hash that commits to the whole log). The proof is checked against the root in that
receipt; whether a public log really published that root is outside this module, as
[Known limitations](../../LIMITATIONS.md) explains.
hash that commits to the whole log). The receipt's `merkle_root` and `leaf_count` must
equal those of the registry entry you pass with `--entry` (one line in the shape of
Anchor Format §4), and the proof is then replayed against that root. Nothing yet
identifies an entry from the `transparency` URI, so the check does not establish that
the entry is the one the record names, and the pass message says so (trace-spec#448
finding 21). Whether a public log really published the entry is also outside this
module, as [Known limitations](../../LIMITATIONS.md) explains.

## Required at Level 2+

| Test ID | Description | Positive Case | Negative Case |
|---------|-------------|---------------|---------------|
| TR-ANC-001 | `transparency` is an `https://` URI with a host. Not resolved | `https://transparency.example/entries/abc123` | missing field, empty string, non-string, `http://`, bare path, `ipfs://` |
| TR-ANC-002 | The record's inclusion proof replays to the committed `merkle_root`, per RFC 9162 over an RFC 6962 tree. Offline, no network | a receipt whose `audit_path` reproduces `merkle_root` from the record's leaf | no receipt supplied, missing `leaf_index`/`audit_path`/`leaf_count`/`merkle_root`, non-hex audit node, out-of-range `leaf_index`, proof for a different record, record modified after anchoring |
| TR-ANC-002 | The record's inclusion proof (`leaf_index` and `audit_path`) replays to the supplied registry entry's `merkle_root` and `leaf_count` from the leaf of the complete signed claim, signature included (Anchor Format §1; for `cmcp-runtime` that is the whole envelope, not the nested `trace`), per RFC 9162 over an RFC 6962 tree; any `merkle_root` or `leaf_count` the receipt carries equals the entry's, and the entry's `ts` is an ISO-8601 UTC timestamp ending in `Z`. Offline, no network. The link from the `transparency` URI to the entry is not checked | a receipt whose `audit_path` reproduces the entry's `merkle_root` from the record's leaf, with or without its own copy of `merkle_root` and `leaf_count` | no receipt supplied, no entry supplied, malformed entry or `ts`, a receipt-carried `merkle_root` or `leaf_count` differing from the entry's, missing `leaf_index`/`audit_path`, non-hex audit node, out-of-range `leaf_index`, proof for a different record, record modified after anchoring, a `cmcp-runtime` proof over the nested `trace` only, an envelope re-signed or with a changed signature after anchoring |
44 changes: 39 additions & 5 deletions conformance/src/trace_tests/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,38 @@ def _print_report(path: str, fmt: str, level: int, results: dict[str, list[Any]]



def _load_receipt(path: str | None) -> dict | None:
"""Load and shape-check an anchor receipt, or return None when not supplied."""
def _load_json_object(path: str | None, label: str) -> dict | None:
"""Load and shape-check a JSON object input, or return None when not supplied."""
if path is None:
return None
try:
with open(path, "rb") as fh:
data = loads_strict(fh.read())
except OSError as exc:
click.echo(f"Error: cannot read receipt {path}: {exc}", err=True)
click.echo(f"Error: cannot read {label} {path}: {exc}", err=True)
sys.exit(2)
except LoadError as exc:
click.echo(f"Error: receipt {path} is not valid JSON: {exc}", err=True)
click.echo(f"Error: {label} {path} is not valid JSON: {exc}", err=True)
sys.exit(2)
if not isinstance(data, dict):
click.echo(
f"Error: receipt {path} must be a JSON object, got {type(data).__name__}",
f"Error: {label} {path} must be a JSON object, got {type(data).__name__}",
err=True,
)
sys.exit(2)
return data


def _load_receipt(path: str | None) -> dict | None:
"""Load and shape-check an anchor receipt, or return None when not supplied."""
return _load_json_object(path, "receipt")


def _load_entry(path: str | None) -> dict | None:
"""Load and shape-check a registry entry, or return None when not supplied."""
return _load_json_object(path, "entry")


def _load_policy_resolver(policy_dir: str | None) -> Callable[[str], bytes] | None:
"""Build a policy-bundle resolver from DIR/resolutions.json, or None when not supplied.

Expand Down Expand Up @@ -206,6 +216,17 @@ def main() -> None:
"says where the anchor lives, the receipt is what proves the record is in it."
),
)
@click.option(
"--entry",
default=None,
type=click.Path(),
help=(
"Path to the registry entry (JSON, one registry-anchor-v1 section 4 line) the "
"receipt commits to. Required for TR-ANC-002 at Level 2: the receipt's "
"merkle_root and leaf_count must equal the entry's. The link from the "
"transparency URI to this entry is not checked."
),
)
@click.option(
"--policy-dir",
"policy_dir",
Expand All @@ -223,6 +244,7 @@ def verify(
max_age: int,
expected_nonce: str | None,
receipt: str | None,
entry: str | None,
policy_dir: str | None,
) -> None:
"""Verify a TRACE trust record against the conformance suite."""
Expand All @@ -233,6 +255,7 @@ def verify(
sys.exit(2)

receipt_data = _load_receipt(receipt)
entry_data = _load_entry(entry)
policy_resolver = _load_policy_resolver(policy_dir)

results = run(
Expand All @@ -242,6 +265,7 @@ def verify(
max_age_seconds=max_age,
expected_nonce=expected_nonce,
receipt=receipt_data,
entry=entry_data,
policy_resolver=policy_resolver,
)
exit_code = _print_report(record, fmt, level, results)
Expand Down Expand Up @@ -289,6 +313,13 @@ def verify(
type=click.Path(),
help="Path to the anchor receipt (JSON). Required for TR-ANC-002 at Level 2.",
)
@click.option(
"--entry",
default=None,
type=click.Path(),
help="Path to the registry entry (JSON) the receipt commits to. Required for "
"TR-ANC-002 at Level 2.",
)
@click.option(
"--policy-dir",
"policy_dir",
Expand All @@ -308,6 +339,7 @@ def report(
fail_under: int | None,
expected_nonce: str | None,
receipt: str | None,
entry: str | None,
policy_dir: str | None,
) -> None:
"""Produce a conformance report you can hand to someone else.
Expand All @@ -323,6 +355,7 @@ def report(
sys.exit(2)

receipt_data = _load_receipt(receipt)
entry_data = _load_entry(entry)
policy_resolver = _load_policy_resolver(policy_dir)

execution = _run_levels(
Expand All @@ -332,6 +365,7 @@ def report(
max_age_seconds=max_age,
expected_nonce=expected_nonce,
receipt=receipt_data,
entry=entry_data,
policy_resolver=policy_resolver,
)

Expand Down
Loading
Loading