feat(canonicalization): use RFC 8785 (JCS) for sign/verify#74
Merged
Conversation
Replace the ad-hoc json.dumps(sort_keys=True, ensure_ascii=True) canonicalization in _canonical_bytes with the RFC 8785-conformant rfc8785 library. Both sign_record and verify_record share _canonical_bytes, so this single change covers signing and verification. The spec (spec/trace-v0.1.md §3.2.2) normatively mandates RFC 8785 (JCS). json.dumps was not JCS-compliant: it escapes non-ASCII as \uXXXX instead of raw UTF-8, zero-pads number exponents (1e-07 vs JCS 1e-7), and sorts keys by Unicode code point rather than UTF-16 code unit. These divergences broke cross-implementation verification and allowed signature-preserving mutation. Also fixes the doc snippets in docs/verification.md and docs/tutorials/signing-your-first-trust-record.md that showed json.dumps as the canonicalization, so docs match the code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
_canonical_bytesinsrc/agentrust_trace/sign.pypreviously usedjson.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=True), which is not RFC 8785 (JCS) conformant. The spec (spec/trace-v0.1.md§3.2.2) normatively mandates JCS as the signature pre-image._canonical_bytesis now backed by therfc8785library.sign_recordandverify_recordalready share_canonical_bytes(confirmed: it is the only producer of the signing pre-image, called once in each), so this single change applies the same canonicalization to both signing and verification.Why JCS, not json.dumps
json.dumps(sort_keys=True, ensure_ascii=True)diverges from RFC 8785 in three load-bearing ways:\uXXXX; JCS emits raw UTF-8 (e.g.ü→\xc3\xbc, notü).1e-07); JCS uses the ECMAScript shortest round-trip form (1e-7), per RFC 8785 §3.2.2.3.Each divergence breaks cross-implementation verification and opens a signature-preserving mutation surface.
Library vs hand-rolled
Used the
rfc8785PyPI package (added todependenciesinpyproject.toml, pinned>=0.1.2) rather than hand-rolling JCS. Justification: a vetted library is the spec's own recommendation ("Implementations MUST use an RFC 8785-conformant library"), avoids re-implementing the subtle ECMAScript Number-to-String and UTF-16 ordering rules, and installs cleanly. I cross-checked it against the RFC 8785 Appendix B number vectors (1e-7,1e+21,9.999999999999997e+22,5e-324, etc.) and it matches.Tests (in
tests/test_sign.py)test_round_trip_with_non_ascii_payload— sign then verify a record carrying non-ASCII data (modèle français 🤖); round-trip still passes.test_canonical_bytes_known_answer_non_ascii— literal expected-bytes assertion{"id":"caf\xc3\xa9","msg":"h\xc3\xbcllo"}, plus an explicit assertion that the json.dumps form (é/ü) differs. A regression to json.dumps fails this.test_canonical_bytes_known_answer_numbers— literal{"n":1e-7}, plusassert json.dumps(1e-7) == "1e-07"to show the form we moved away from.test_canonical_bytes_matches_reference_library— full record cross-checked againstrfc8785.dumps.test_jcs_distinguishes_unicode_key_order_from_json_dumps— object with azkey and aU+1F600key; JCS (UTF-16 code unit) putszfirst and the two schemes produce different bytes.test_canonical_bytes_sorts_keys_no_whitespace— basic JCS shape.All 71 tests pass (including the #73 freshness / trusted-key tests).
ruff check src/ tests/ --select E,F,W --ignore E501is clean.Scope
This PR is canonicalization only. Two related items remain separate Wave 2 work:
cnfshould be inside or outside the signed pre-image — currentlycnfis signed and onlysignatureis excluded).🤖 Generated with Claude Code