test: add non-JCS signature rejection controls - #306
Conversation
Signed-off-by: Noah Ingwers <98993329+noah-ing@users.noreply.github.com>
|
🟡 Contributor Check: MEDIUM
Automated check by AgenTrust Contributor Check. |
lywinged
left a comment
There was a problem hiding this comment.
Approved on 1b935ea over 178e148.
I rebuilt the mutant matrix rather than reading the tests that assert it. Five verifiers written from scratch against the six fixtures, two unconditional and three deciding by trying their own preimage forms with cryptography directly:
| verifier | disagrees with the set on |
|---|---|
| JCS only, conformant | nothing |
| accept everything | 05, 06 |
| reject everything | 01, 02, 03, 04 |
| JCS then ASCII-escaped fallback | 05 only |
| JCS then code-point-order fallback | 06 only |
Those five rows are what the two mutant tests assert, test_both_unconditional_verifiers_are_killed for the first three and test_each_alternate_fallback_is_a_distinct_killed_defect for the last two, arrived at without running them. The set is two-directional now, and the two negatives are not interchangeable: each kills a different fallback and leaves the other alive.
The property that makes them hold up is that they are signatures over the wrong preimage rather than broken signatures, and the tests assert that rather than assuming it. I checked it the way it can go wrong. Replacing 05's signature with 64 random bytes fails three: test_negative_preimages_are_exact_and_signature_failure_is_isolated[ascii-escaped-signature], test_each_alternate_fallback_is_a_distinct_killed_defect[fallback-accepts-ascii-escaping], and the generator reproduction guard. Substituting 01's valid signature into 05 fails four, including test_both_unconditional_verifiers_are_killed. So a later edit cannot quietly turn either negative into a corrupted-signature vector while the suite stays green.
Also confirmed at this head: both signatures verify over the declared alternate preimage and raise InvalidSignature over the RFC 8785 bytes; the unsigned payloads of 05 and 06 are identical to 01 and 03, and their canonical bytes equal each fixture's own canonical_input_utf8; the four positives are byte-identical to the base; gen_boundary_vectors.py reproduces all six with git status clean, from a seed in the file; all six are schema-valid; verify_record gives four verified and two InvalidSignature with now=iat and the key pinned. Suite 1,314 passed, 1 skipped. Ruff, check_dashes.py and mypy clean.
The adequacy guard still bites with the entry gone. Deleting the two negatives makes test_no_set_is_satisfied_by_an_unconditional_answer[canonicalization-boundary] fail with NO CONTROL: a verifier that answers 'accept' to everything passes this set, so removing the exemption did not remove the check.
The removal is also what that file said would happen. The base docstring recorded the shortfall with the entry deleted when the missing direction is added, and this deletes it; the docstring and the comment above the dict are rewritten to match, so nothing is left claiming an exemption that no longer exists. KNOWN_ONE_DIRECTIONAL is now an empty dict with its branch still live, which is the shape KNOWN_THIN has carried since #186.
Tool-assisted: the runs and this write-up.
imran-siddique
left a comment
There was a problem hiding this comment.
Approving on 1b935ea. I ran the fixtures against the repository's own verifier rather than reading the tests that assert them, because the claim here is about what the corpus can discriminate and that is only checkable by executing it.
Clean venv, editable install, import confirmed from this branch's src/agentrust_trace. verify_record(..., allow_embedded_key=True, max_age_seconds=10**9) over all six fixtures:
| fixture | result |
|---|---|
01-non-ascii-values |
accepted |
02-non-bmp-values |
accepted |
03-utf16-key-order |
accepted |
04-utf16-key-order-nested |
accepted |
05-ascii-escaped-signature |
rejected, InvalidSignature |
06-codepoint-order-signature |
rejected, InvalidSignature |
That is the property the set was missing. Before this, every vector expected acceptance, so an implementation that accepts unconditionally satisfied the whole set and the corpus could not tell it from a conformant one. Two records signed over a non-JCS preimage make the set two-directional, which is why KNOWN_ONE_DIRECTIONAL can go to {} rather than merely shrink. tests/test_canonicalization_boundary.py and tests/test_adequacy_all_sets.py: 31 passed.
Emptying the exemption dict rather than deleting the mechanism is the right call. The assertion still exists to catch the next set that drifts into the same shape.
Worth recording that this is not an isolated gap. #245 has the same defect in a different corpus: all 23 delegation-link vectors are ASCII, so they match under RFC 8785 and under json.dumps(sort_keys=True) alike and discriminate neither rule. A corpus that agrees under the wrong rule is evidence of nothing, and the cure is the same one used here, a vector that only the correct rule accepts. This PR is the worked example for that.
@noah-ing thank you, and @lywinged for rebuilding the mutant matrix from scratch rather than reading the assertions. Merging needs an admin push on this repo, so it lands separately.
Summary
Close the explicitly recorded acceptance-only gap in
examples/canonicalization-boundary, under the already accepted section 3.2.2 signature-byte contract.The four existing fixtures all expect
verified. The repository's adequacy loader therefore recordscanonicalization-boundaryinKNOWN_ONE_DIRECTIONAL: an implementation that accepts everything satisfies the whole set. This is the gap described in #186, not a newly proposed verification requirement.This PR keeps the four positive fixtures byte-identical and adds two portable, schema-valid negative controls:
05-ascii-escaped-signature.jsonrejected,signature_invalid06-codepoint-order-signature.jsonrejected,signature_invalidThese are genuine Ed25519 signatures over the wrong preimages, not valid TRACE signatures, malformed records, or random signature corruptions. Their unsigned payloads match positive fixtures 01 and 03 exactly.
Why the new cases are load-bearing
cryptographyverification succeeds over the declared alternate bytes and raisesInvalidSignatureover the JCS bytes.verify_record()rejects specifically withInvalidSignature, with schema validity, configured trust key, andnow=iatcontrolled.Scope and relation to #66
The exact-byte action/intent-binding discussion in #66 is why I checked this corpus. This supplies missing portable rejection evidence for a settled prerequisite. It does not implement the two-axis model, decide receipt coverage/cardinality, define a new schema or public API, or close #66/#279. It also does not claim independent-producer interoperability or overlap #245's delegation parent-record-hash evidence.
The production verifier already rejects these cases. This is conformance-test hardening, not a newly discovered production vulnerability. Outer JSON file formatting is not the signature preimage.
Validation
InvalidSignaturenegatives against the six fixtures.DCO-signed commit:
1b935ea88bce44eb4fa213b172975c65d8a9f8f8.