Skip to content

Commit a1d4eef

Browse files
test: guard the ten conformance failure paths the measurement found unverified
The harness in measurement/ reported ten of thirty-three Finding(..., FAIL, ...) sites at margin zero: rewriting any of them so it could never fail left the whole suite green. Landing the instrument without the tests would have published a gap list against our own conformance suite and fixed nothing. tr_txn and tr_sca had no unit tests at all, which is why every one of their failure paths measured zero. TR-TXN-001 carried the most weight: it is the only place the Level 2 tool-transcript requirement is enforced anywhere in the suite. New: tests/unit/test_tr_txn.py, tests/unit/test_tr_sca.py. Extended: test_tr_sig.py with the two check_cmcp_runtime key-shape paths, the unsupported kty path, and the signature-present-but-uncheckable path; test_tr_anc.py with a URI that raises during parsing rather than parsing to a wrong scheme. The two TR-SIG-002 vectors are deliberately separable: an implementation that checks kty and crv without checking x passes one and fails the other, which is what makes them two vectors rather than one written twice. Re-measured: 18 of 18 checks and 33 of 33 sites verified, none unguarded. Eight checks sit at margin 1, which the report now names as the number to watch. REPORT.md and README.md updated to describe the state they measured rather than the state they found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0747158 commit a1d4eef

6 files changed

Lines changed: 241 additions & 18 deletions

File tree

measurement/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ Does this repository's own suite notice when one of its conformance checks stops
55
**None of the checks measured here is wrong.** They behave correctly today. This measures
66
something narrower: whether a *regression* inside a conformance module would be caught.
77

8-
For 23 of 33 failure paths, it would. For ten, it would not — including all three failure
9-
paths of `TR-TXN-001`, which is the only place the Level 2 tool-transcript requirement is
10-
enforced.
8+
It does now, for all 33. When this was first run, ten failure paths were unguarded,
9+
including all three of `TR-TXN-001`, which is the only place the Level 2 tool-transcript
10+
requirement is enforced. Those ten got tests, and the measurement is how they were found.
11+
12+
Re-run it after touching any conformance module. Eight checks sit at margin 1, so a single
13+
deleted test puts one of them back to zero.
1114

1215
[`REPORT.md`](REPORT.md) has the findings, the method, and what the method does not
1316
establish.

measurement/REPORT.md

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ level up worth asking, and as far as the repository shows, unasked so far:
1010
> If a check inside a conformance module silently stopped working, would the
1111
> repository's own test suite catch it?
1212
13-
For most checks, yes. For ten of the thirty-three failure paths, no.
13+
When this was first measured, the answer was no for ten of the thirty-three failure
14+
paths. Those ten are now guarded, and the measurement is what established which ones
15+
needed it.
1416

1517
---
1618

@@ -21,25 +23,32 @@ modules 7 src/trace_tests/modules/tr_*.py
2123
checks 18 distinct TR-xxx-nnn codes
2224
sites 33 Finding(..., Status.FAIL, ...) constructions
2325
24-
by check 15 of 18 verified, 3 unverified
25-
by site 23 of 33 verified, 10 unverified
26-
27-
site margin distribution 0→10 1→12 2→6 3→1 4→3 5→1
26+
by check 18 of 18 verified, 0 unverified
27+
by site 33 of 33 verified, 0 unverified
2828
```
2929

30-
**Counting by check understates it by more than threefold.** A check code emitted from
31-
three places can have one failure path nothing verifies while the other two are covered,
32-
and it still counts as verified. The sites are where the checks actually live.
30+
Eight checks sit at margin 1, meaning exactly one test stands between them and silent
31+
regression. That is a floor, not a comfortable state, and it is the number to watch.
3332

34-
### The three checks nothing verifies at all
33+
### What the first measurement found
3534

36-
| Check | What it enforces | Sites |
35+
| Check | What it enforces | Sites then unverified |
3736
|---|---|---|
38-
| `TR-SIG-002` | `cnf.jwk` is an OKP/Ed25519 key and carries `x` | 2 of 2 unverified |
39-
| `TR-TXN-001` | `tool_transcript` is present at Level 2, is an object, and its hash is a well-formed digest | 3 of 3 unverified |
40-
| `TR-TXN-002` | `tool_transcript.call_count` is a non-negative integer | 1 of 1 unverified |
41-
42-
Rewriting any of these so it can never fail leaves the suite green.
37+
| `TR-SIG-002` | `cnf.jwk` is an OKP/Ed25519 key and carries `x` | 2 of 2 |
38+
| `TR-TXN-001` | `tool_transcript` is present at Level 2, is an object, and its hash is a well-formed digest | 3 of 3 |
39+
| `TR-TXN-002` | `tool_transcript.call_count` is a non-negative integer | 1 of 1 |
40+
| `TR-SCA-001` | `build_provenance` is an object | 1 site |
41+
| `TR-SIG-004` | `cnf.jwk.kty` is a supported key type | 1 site |
42+
| `TR-SIG-005` | a signature that cannot be checked fails rather than going unverified | 1 site |
43+
| `TR-ANC-001` | a transparency URI that cannot be parsed fails cleanly | 1 site |
44+
45+
`tr_txn` and `tr_sca` had no unit tests at all, which is why every one of their failure
46+
paths measured zero. `TR-TXN-001` carried the most weight: it is the Level 2 requirement
47+
that a tool transcript exist, and nothing else in the suite enforced it.
48+
49+
**Counting by check understates the problem by more than threefold.** A check code emitted
50+
from three places can have one failure path nothing verifies while the other two are
51+
covered, and still count as verified. The sites are where the checks actually live.
4352

4453
`TR-TXN-001` carries the most weight. It is the Level 2 requirement that a tool
4554
transcript exist at all. If it regressed, implementations would continue to be stamped

tests/unit/test_tr_anc.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,16 @@ def test_non_string_transparency_fails():
2626
def test_non_uri_scheme_fails():
2727
failed = [f for f in check({"transparency": "ftp://example.org/log"}) if f.failed()]
2828
assert any(f.code == "TR-ANC-001" for f in failed)
29+
30+
31+
def test_unparseable_transparency_uri_fails():
32+
"""A URI that raises during parsing, rather than parsing to a wrong scheme.
33+
34+
`https://[` raises ValueError (Invalid IPv6 URL) inside urlparse. This is the
35+
only input that reaches the except branch, which measured margin 0: deleting
36+
that handler turned a clean FAIL into an uncaught exception with no test
37+
noticing.
38+
"""
39+
failed = [f for f in check({"transparency": "https://["}) if f.failed()]
40+
assert any(f.code == "TR-ANC-001" for f in failed)
41+
assert any("could not parse" in f.message for f in failed)

tests/unit/test_tr_sca.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Unit tests for TR-SCA module.
2+
3+
This module had no unit tests. Its build_provenance-must-be-an-object path measured
4+
margin 0, meaning it could have been deleted silently.
5+
"""
6+
7+
from trace_tests.modules.tr_sca import check
8+
9+
VALID_DIGEST = "sha256:" + "a" * 64
10+
11+
12+
def _prov(**over):
13+
prov = {"slsa_level": 3, "digest": VALID_DIGEST}
14+
prov.update(over)
15+
return {"build_provenance": prov}
16+
17+
18+
def test_valid_provenance_passes():
19+
failed = [f for f in check(_prov()) if f.failed()]
20+
assert not failed, failed
21+
22+
23+
def test_missing_provenance_fails():
24+
failed = [f for f in check({}) if f.failed()]
25+
assert any(f.code == "TR-SCA-001" for f in failed)
26+
assert any("required at Level 1+" in f.message for f in failed)
27+
28+
29+
def test_non_object_provenance_fails():
30+
"""A string where an object belongs. Reading slsa_level off it would raise, not fail cleanly."""
31+
failed = [f for f in check({"build_provenance": "slsa3"}) if f.failed()]
32+
assert any(f.code == "TR-SCA-001" for f in failed)
33+
assert any("must be an object" in f.message for f in failed)
34+
35+
36+
def test_list_provenance_fails():
37+
failed = [f for f in check({"build_provenance": [{"slsa_level": 3}]}) if f.failed()]
38+
assert any(f.code == "TR-SCA-001" for f in failed)
39+
40+
41+
def test_out_of_range_slsa_level_fails():
42+
failed = [f for f in check(_prov(slsa_level=4)) if f.failed()]
43+
assert any(f.code == "TR-SCA-001" for f in failed)
44+
45+
46+
def test_string_slsa_level_fails():
47+
""""3" is not 3; a coercing comparison would accept it."""
48+
failed = [f for f in check(_prov(slsa_level="3")) if f.failed()]
49+
assert any(f.code == "TR-SCA-001" for f in failed)
50+
51+
52+
def test_missing_slsa_level_fails():
53+
failed = [f for f in check({"build_provenance": {}}) if f.failed()]
54+
assert any(f.code == "TR-SCA-001" for f in failed)

tests/unit/test_tr_sig.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,52 @@ def test_plain_trace_tampered_embedded_signature_fails():
151151
findings = check(trace, trace, "trace")
152152
sig_findings = [f for f in findings if f.code == "TR-SIG-005"]
153153
assert any(f.failed() for f in sig_findings), sig_findings
154+
155+
156+
# --- the four TR-SIG paths that measured margin 0 --------------------------
157+
#
158+
# Each of these sites could have been deleted with no test failing. The two
159+
# check_cmcp_runtime key-shape sites matter most: they are the guard that stops a
160+
# record being read as Ed25519-verified when it carries some other key entirely.
161+
162+
163+
def test_cmcp_runtime_non_ed25519_key_fails():
164+
"""kty/crv is not OKP/Ed25519. Deleting this site would let the verifier
165+
proceed toward an Ed25519 verification on a key that is not one."""
166+
record = _make_signed_record()
167+
record["trace"]["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "irrelevant"}
168+
failed = [f for f in check(record["trace"], record, "cmcp-runtime") if f.failed()]
169+
assert any(f.code == "TR-SIG-002" for f in failed), failed
170+
171+
172+
def test_cmcp_runtime_ed25519_without_x_fails():
173+
"""Right key type, no public key. Separated from the case above because an
174+
implementation that checks kty/crv without checking x passes one and fails
175+
the other, which is what makes them two vectors rather than one."""
176+
record = _make_signed_record()
177+
record["trace"]["cnf"]["jwk"] = {"kty": "OKP", "crv": "Ed25519"}
178+
failed = [f for f in check(record["trace"], record, "cmcp-runtime") if f.failed()]
179+
assert any(f.code == "TR-SIG-002" for f in failed), failed
180+
181+
182+
def test_unsupported_key_type_fails():
183+
"""kty present and not in the supported set, as distinct from kty absent."""
184+
record = _make_signed_record()
185+
trace = record["trace"]
186+
trace["cnf"]["jwk"] = {"kty": "RSA", "n": "...", "e": "AQAB"}
187+
failed = [f for f in check(trace, record, "trace") if f.failed()]
188+
assert any(f.code == "TR-SIG-004" for f in failed), failed
189+
assert any("unsupported key type" in f.message for f in failed)
190+
191+
192+
def test_signature_present_with_wrong_key_type_fails():
193+
"""A signature that cannot be checked must fail, never quietly go unverified.
194+
Distinct from the no-signature path, which is UNVERIFIED at Level 0."""
195+
record = _make_signed_record()
196+
trace = record["trace"]
197+
trace["signature"] = record["signature"]
198+
trace["cnf"]["jwk"] = {"kty": "EC", "crv": "P-256", "x": "irrelevant"}
199+
sig_findings = [f for f in check(trace, record, "trace") if f.code == "TR-SIG-005"]
200+
assert sig_findings, "TR-SIG-005 must be reported when a signature is present"
201+
assert any(f.failed() for f in sig_findings), sig_findings
202+
assert not any(f.status == Status.UNVERIFIED for f in sig_findings)

tests/unit/test_tr_txn.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Unit tests for TR-TXN module.
2+
3+
This module had no unit tests, which is why all four of its failure paths measured
4+
margin 0 in `measurement/`: every one of them could have been deleted without a
5+
single test failing. TR-TXN-001 is the only place the Level 2 tool-transcript
6+
requirement is enforced anywhere in the suite.
7+
8+
Each test below targets one `Finding(..., Status.FAIL, ...)` site, so deleting that
9+
site takes exactly this test with it.
10+
"""
11+
12+
from trace_tests.modules.tr_txn import check
13+
14+
VALID_HASH_256 = "sha256:" + "a" * 64
15+
VALID_HASH_384 = "sha384:" + "b" * 96
16+
17+
18+
def _txn(**over):
19+
txn = {"hash": VALID_HASH_256}
20+
txn.update(over)
21+
return {"tool_transcript": txn}
22+
23+
24+
def test_valid_transcript_passes():
25+
findings = check(_txn())
26+
assert all(f.passed() or f.code == "TR-TXN-002" for f in findings), findings
27+
28+
29+
def test_sha384_hash_is_accepted():
30+
failed = [f for f in check(_txn(hash=VALID_HASH_384)) if f.failed()]
31+
assert not failed, failed
32+
33+
34+
# --- TR-TXN-001, the Level 2 obligation ------------------------------------
35+
36+
37+
def test_missing_transcript_fails():
38+
"""The Level 2 requirement itself. Nothing else in the suite enforces it."""
39+
failed = [f for f in check({}) if f.failed()]
40+
assert any(f.code == "TR-TXN-001" for f in failed)
41+
assert any("required at Level 2" in f.message for f in failed)
42+
43+
44+
def test_non_object_transcript_fails():
45+
failed = [f for f in check({"tool_transcript": "sha256:deadbeef"}) if f.failed()]
46+
assert any(f.code == "TR-TXN-001" for f in failed)
47+
assert any("must be an object" in f.message for f in failed)
48+
49+
50+
def test_malformed_hash_fails():
51+
failed = [f for f in check(_txn(hash="deadbeef")) if f.failed()]
52+
assert any(f.code == "TR-TXN-001" for f in failed)
53+
54+
55+
def test_missing_hash_fails():
56+
failed = [f for f in check({"tool_transcript": {}}) if f.failed()]
57+
assert any(f.code == "TR-TXN-001" for f in failed)
58+
59+
60+
def test_truncated_digest_fails():
61+
"""63 hex characters, not 64. A prefix comparison would let this through."""
62+
failed = [f for f in check(_txn(hash="sha256:" + "a" * 63)) if f.failed()]
63+
assert any(f.code == "TR-TXN-001" for f in failed)
64+
65+
66+
def test_uppercase_digest_fails():
67+
"""The pattern is lowercase hex; a case-insensitive match would accept this."""
68+
failed = [f for f in check(_txn(hash="sha256:" + "A" * 64)) if f.failed()]
69+
assert any(f.code == "TR-TXN-001" for f in failed)
70+
71+
72+
# --- TR-TXN-002, optional but constrained when present ---------------------
73+
74+
75+
def test_absent_call_count_is_skipped_not_failed():
76+
"""Optional means optional: absence must not be reported as a failure."""
77+
codes = {f.code: f for f in check(_txn())}
78+
assert "TR-TXN-002" in codes
79+
assert not codes["TR-TXN-002"].failed()
80+
81+
82+
def test_negative_call_count_fails():
83+
failed = [f for f in check(_txn(call_count=-1)) if f.failed()]
84+
assert any(f.code == "TR-TXN-002" for f in failed)
85+
86+
87+
def test_non_integer_call_count_fails():
88+
failed = [f for f in check(_txn(call_count="3")) if f.failed()]
89+
assert any(f.code == "TR-TXN-002" for f in failed)
90+
91+
92+
def test_zero_call_count_is_valid():
93+
"""A transcript with no calls is a real outcome, not an error."""
94+
failed = [f for f in check(_txn(call_count=0)) if f.failed()]
95+
assert not failed, failed

0 commit comments

Comments
 (0)