Skip to content

test(conformance): portable vectors for the RFC 8785 requirement - #126

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:test/canonicalization-boundary-vectors
Aug 8, 2026
Merged

test(conformance): portable vectors for the RFC 8785 requirement#126
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:test/canonicalization-boundary-vectors

Conversation

@lywinged

@lywinged lywinged commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What this is not

§3.2.2 requires an RFC 8785-conformant canonicalizer and names json.dumps(sort_keys=True) as insufficient. tests/test_sign.py already covers that, with four literal-byte known-answer tests over _canonical_bytes. I swapped the canonicalizer for each ad-hoc form in turn to check, and every one of them fails those tests. This PR is not reporting a hole in them and does not replace them.

I mention it because my first read of this was that the requirement was untested, and that was wrong — I had not read test_sign.py before concluding it.

What it adds

1. Portability. A known-answer test over a private function runs only from Python, in this package. ROADMAP.md targets Go, Rust and TypeScript verification libraries for v1.0, and none of them can run test_sign.py. These three fixtures are signed Trust Records: another implementation runs them against its own verifier and finds out whether its canonicalizer agrees.

That matters because no existing record in the repository can tell. Every example is ASCII-only with schema-fixed keys, and on such records every serializer produces identical bytes — so an implementation built on ad-hoc sorting verifies all of them correctly.

2. One vector separates key ordering, which nothing here currently does.

test_jcs_distinguishes_unicode_key_order_from_json_dumps compares {"z": 1, "\U0001f600": 2}. Under RFC 8785's UTF-16 code-unit sort and under Python's code-point sort, that object serializes in the same order — the test's own docstring notes it ("order matches here") — so what it actually detects is the ensure_ascii escaping, which it hardcodes on the comparison side:

>>> obj = {"z": 1, "\U0001f600": 2}
>>> rfc8785.dumps(obj) == json.dumps(obj, sort_keys=True, separators=(",",":"), ensure_ascii=False).encode()
True          # a code-point sorter emitting raw UTF-8 passes this test

Vector 03 uses two keys sharing a zk prefix and differing only after it, where the orderings genuinely disagree:

# Escapes throughout, so nothing depends on how your terminal renders it.
>>> obj = {"zk\U0001f600": 1, "zk\uFFFD": 2}   # U+1F600 is D83D DE00 in UTF-16
>>> rfc8785.dumps(obj)
b'{"zk\xf0\x9f\x98\x80":1,"zk\xef\xbf\xbd":2}'    # D83D < FFFD
>>> json.dumps(obj, sort_keys=True, separators=(",",":"), ensure_ascii=False).encode()
b'{"zk\xef\xbf\xbd":2,"zk\xf0\x9f\x98\x80":1}'    # 0xFFFD < 0x1F600

cnf.jwk is the one schema object open to additional members, which RFC 7517 permits, so this is expressible in a schema-valid record.

The ladder

Ad-hoc form Fails on
json.dumps(o, sort_keys=True) any record
… separators=(",", ":") 01, 02, 03
… separators=(",", ":"), ensure_ascii=False 03 only

Each fixture declares diverges_under; the test recomputes it rather than trusting it, and separately asserts the set as a whole still kills every rung — so a vector that quietly stopped discriminating fails rather than passing while documenting a distinction it no longer makes.

Deliberately no number-formatting vector

RFC 8785's IEEE 754 serialization is the other divergence §3.2.2 warns about, and it is unreachable inside a schema-valid record: no field in schema/trace-claim.json is typed number, and integers below 2^53 serialize identically everywhere. test_number_divergence_is_still_unreachable pins that and fails the day a numeric field is added, at which point the right response is a vector here rather than an edit to the test.

Verification

  • 114 tests pass (105 before, plus 9)
  • ruff check src tests examples clean; verified on Python 3.11 and 3.14
  • gen_boundary_vectors.py regenerates the set byte-for-byte; only public JWKs appear in the files
  • Every fixture is schema-valid and verifies through verify_record, so none can pass or fail for an unrelated reason
  • No normative text, schema, or record field changed. Test material only.
  • DCO signed

Section 3.2.2 requires an RFC 8785-conformant canonicalizer and names
json.dumps(sort_keys=True) as insufficient. tests/test_sign.py already
covers that at unit level, with four literal-byte known-answer tests over
_canonical_bytes; a swap to json.dumps fails them. These vectors are not a
replacement for those and do not claim a gap in them.

They add two things the known-answer tests cannot do.

Portability. A known-answer test over a private function runs only from
Python, in this package. The roadmap targets Go, Rust and TypeScript
verification libraries for v1.0, and none of them can run test_sign.py.
These are signed Trust Records: any implementation runs them against its
own verifier. Every other record in the repository is ASCII-only with
schema-fixed keys, where all serializers agree byte-for-byte, so no
existing record's acceptance depends on canonicalizing correctly.

Key ordering. test_jcs_distinguishes_unicode_key_order_from_json_dumps
compares {"z": 1, "\U0001f600": 2}, which serializes in the same order
under RFC 8785's UTF-16 code-unit sort and under Python's code-point sort
— the docstring says so — so it detects divergence through ensure_ascii
escaping rather than through ordering. A canonicalizer that sorts by code
point and emits raw UTF-8 passes it. Vector 03 uses two keys sharing a
prefix and differing only after it, where the two orderings disagree; it
is the first object here that separates them.

The three vectors form a ladder: default separators fail on any record,
ensure_ascii fails on 01 and 02, and json.dumps with compact separators
and ensure_ascii=False survives both and fails only 03. Each declares
diverges_under and the test recomputes it rather than trusting it, and
asserts the set still catches every rung.

No number-formatting vector: no schema field is typed number, so RFC
8785's IEEE 754 serialization is unreachable inside a schema-valid record.
A test pins that and fails the day a numeric field is added.

gen_boundary_vectors.py regenerates the set byte-for-byte. Only public
JWKs appear in the files. 114 tests pass (105 before, plus 9).

Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Contributor Check: UNKNOWN

Check Result
Profile UNKNOWN
Credential LOW
Overall UNKNOWN

Automated check by AgenTrust Contributor Check.

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good reasoning, and the honest note that test_sign.py already covers the requirement from Python is why this is worth having: the gap is portability, not coverage. Signed fixtures a Go, Rust or TypeScript verifier can run against its own canonicalizer are exactly what the v1.0 roadmap needs, and no existing ASCII-only example can distinguish a conformant canonicalizer from ad-hoc sorting.

@imran-siddique
imran-siddique merged commit d09d4e0 into agentrust-io:main Aug 8, 2026
2 of 3 checks passed
lywinged added a commit to lywinged/trace-spec that referenced this pull request Aug 10, 2026
Upstream merged agentrust-io#148, the last large piece this fork was carrying, which the
maintainer rebased and opened himself under this fork's authorship. With agentrust-io#122,
agentrust-io#125, agentrust-io#126, agentrust-io#136 and agentrust-io#137 already merged, most of the thirty-three commits here
described work that now lives upstream with better provenance than this fork can
give it: a PR number and a maintainer's review.

`git rebase upstream/main` was tried first and abandoned. It stopped on the oldest
commit in the set, a schema-and-version alignment whose content upstream has since
taken, superseded and released three times over. Replaying thirty-three commits
against fifteen of upstream's resolves early commits into shapes that no longer
mean anything.

So: main reset to upstream/main, fork-only material re-applied. The old history is
tagged `archive/pre-576507b` and pushed rather than discarded.

What is held here, and why it is held:

- agentrust-io#117 gap disclosure: design note, two normative drafts, 18 vectors, generator
- agentrust-io#116 verifier compatibility: 8 vectors, generator, normative draft
- the normative crosswalk, mapping every RFC 2119 statement to whom it binds
- `docs/conformance-method.md`, `coverage-report/` (historical), DECISIONS.md
- the independent signature path and the package-consistency test

Each is an unaccepted proposal or a method write-up, not unfinished work.

Four files needed a real merge, and not in the same direction. `models.py` and
`__init__.py` are upstream's plus this fork's two profile constants, because
upstream had moved on with `origin` (agentrust-io#135) and the `declared` enforcement mode
(agentrust-io#143) and a wholesale copy would have dropped both. `sign.py` and `test_sign.py`
are this fork's `accepted_profiles` version, which supersedes the minimal cutover
check upstream took from agentrust-io#125 -- a supersession CLAUDE.md predicted when agentrust-io#125 was
offered. All four merged cleanly three-way against a817621, the last commit the
two histories agree on.

The crosswalk guard earned its place in the same run: upstream added two normative
statements this fork had never seen, and `test_normative_crosswalk.py` failed until
both had rows. A source-derived inventory noticing its subject moved is the
property that document exists to have.

432 passed, 1 skipped. ruff and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
@lywinged
lywinged deleted the test/canonicalization-boundary-vectors branch August 18, 2026 05:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:UNKNOWN Contributor check flagged UNKNOWN risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants