Skip to content

fix(tr-sig): canonicalize with RFC 8785, which is the rule this suite… - #66

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/canonicalization-rfc8785
Aug 20, 2026
Merged

fix(tr-sig): canonicalize with RFC 8785, which is the rule this suite…#66
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/canonicalization-rfc8785

Conversation

@lywinged

Copy link
Copy Markdown
Collaborator

What is wrong

TR-SIG verifies signatures over

json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)

Specification §3.2.2 names that form, in as many words:

Implementations MUST use an RFC 8785-conformant library. Using json.dumps(sort_keys=True) (Python) or equivalent ad-hoc sorting is insufficient.

The suite that scores conformance was failing the one requirement it had no way to see itself failing.

Why nothing here noticed

The two forms agree byte-for-byte on any record whose strings are ASCII, and every record this suite carried was ASCII. All 176 tests passed.

Pointed at trace-spec's examples/canonicalization-boundary/ — four schema-valid records, correctly signed over their RFC 8785 bytes, each declaring expected: {"outcome": "verified"} — the runner failed all four:

vector declared shipped runner
non-ascii-values verified FAIL TR-SIG-001
non-bmp-values verified FAIL TR-SIG-001
utf16-key-order verified FAIL TR-SIG-001
utf16-key-order-nested verified FAIL TR-SIG-001

That corpus exists in trace-spec precisely to separate a conforming serializer from json.dumps with every option set carefully. The defect is here, the corpus is there, and nothing ran one against the other — the same shape as #136, one repository further out.

report.py:57 already draws the distinction explicitly ("matching the registry anchor's canonicalization rather than RFC 8785, because this is an identifier for a reader to compare against"). The distinction was understood; it just was not applied on the one path where it is cryptographic.

Severity, in the honest direction

This rejects valid records; it does not accept invalid ones. Fail-closed, so an interoperability defect rather than a security hole.

What it costs is real anyway: any record with a non-ASCII character in any string value — a data_class, a model version, a subject path, any human-language field — is reported by the official suite as having a failed signature. The suite tells a conformant implementation that it is not one.

The fix

One function. agentrust_trace.sign._canonical_bytes is already rfc8785.dumps, so the signer and the verifier were applying different rules; rfc8785>=0.1.2 joins the dependencies so the verifier uses the same one.

The regression material

The four vectors come in copied, not vendored, and nothing compares them to the originals — a guard that needs another repository checked out is a guard that gets skipped. Each is held instead to the two properties that make it worth having:

  • genuinely valid — the signature verifies over the record's own RFC 8785 bytes
  • genuinely a boundary — the signature does not verify under any ad-hoc form its diverges_under names

A vector edited into something that is no longer a boundary fails, rather than sitting in the directory looking like coverage.

A fifth test compares _canonical_json's output against the reference directly, on a probe every ad-hoc form gets wrong, so a reintroduced shortcut is reported as a serializer problem rather than as a failed signature — one step closer to the cause than a red TR-SIG-001.

Verified by reverting

A regression test that passes before and after the fix is not one. With the one-line change reverted:

5 failed, 9 passed
  FAILED test_the_runner_accepts_it[01-non-ascii-values]
  FAILED test_the_runner_accepts_it[02-non-bmp-values]
  FAILED test_the_runner_accepts_it[03-utf16-key-order]
  FAILED test_the_runner_accepts_it[04-utf16-key-order-nested]
  FAILED test_the_module_canonicalizes_with_rfc_8785
      At index 13 diff: b'\\' != b'\xc3'

The nine that still pass are the vector self-validation assertions, which correctly do not depend on the fix.

Also in this commit

The module docstring claimed plain trace records could not be cryptographically verified, while check() has verified them against an embedded signature field for some time. Corrected in the same pass — a reader following the old text would not have looked at the path this fixes.

Checks

190 passed, 5 xpassed

The 5 xpassed are pre-existing: identical count with and without this change. The six ruff findings in tr_sig.py are the six that were there before, with line numbers moved; the new test file is clean.

How this was found

Building a delegation-link conformance corpus in trace-spec and running it through every verifier in the org that would accept it. This suite was the fourth of those, and it agreed with the corpus on all 65 records — because all 65 were ASCII. Turning the corpus around and pointing this suite at trace-spec's canonicalization vectors is what surfaced it.


Generated by Claude Code

… scores

TR-SIG verified signatures over

    json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)

which specification section 3.2.2 names, in as many words, as insufficient:
"Implementations MUST use an RFC 8785-conformant library. Using
json.dumps(sort_keys=True) (Python) or equivalent ad-hoc sorting is
insufficient." The suite that scores conformance was failing the one requirement
it had no way to see itself failing.

The two forms agree byte-for-byte on any record whose strings are ASCII, and
every record this suite carried was ASCII, so all 176 tests passed while the
runner rejected valid records. Pointed at trace-spec's
examples/canonicalization-boundary/ -- four schema-valid records, correctly
signed over their RFC 8785 bytes, each declaring expected outcome "verified" --
it failed all four.

Direction matters for reading the severity: this rejects valid records, it does
not accept invalid ones, so it is an interoperability defect rather than a
security hole. What it costs is real anyway. Any record carrying a non-ASCII
character in any string value -- a data_class, a model version, a subject path,
any human-language field -- is reported by the official suite as having a failed
signature, which tells a conformant implementation that it is not one.

The fix is the one function. agentrust_trace.sign already canonicalizes with
rfc8785.dumps, so the signer and the verifier were using different rules;
rfc8785 joins the dependencies for the verifier to use the same one.

The four vectors come in as regression material, copied rather than vendored.
Nothing compares them to the originals: a guard that needs another repository
checked out is a guard that gets skipped. Each is instead held to the two
properties that make it worth having -- it verifies over its own RFC 8785 bytes,
and it does not verify under any ad-hoc form its diverges_under names -- so a
vector edited into something that is no longer a boundary fails rather than
sitting in the directory looking like coverage. A fifth test compares
_canonical_json's output to the reference directly, on a probe every ad-hoc form
gets wrong, so a reintroduced shortcut is reported as a serializer problem
rather than as a failed signature.

Verified by reverting the one-line change: the four runner assertions and the
direct byte comparison fail, and the eight vector self-validation assertions
pass, which is the shape a regression test should have.

The module docstring also claimed plain trace records could not be
cryptographically verified, while check() has verified them against an embedded
signature field for some time. Corrected in the same pass, since a reader
following it would not have looked at the path this commit fixes.

190 passed. The six ruff findings in this file are the six that were there
before; the line numbers moved.

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

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Contributor Check: UNKNOWN

Check Result
Profile UNKNOWN
Credential LOW
Overall UNKNOWN

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:UNKNOWN Contributor check flagged UNKNOWN risk label Aug 18, 2026
@lywinged lywinged closed this Aug 18, 2026
@lywinged lywinged reopened this Aug 18, 2026
@imran-siddique
imran-siddique merged commit c725bbb into agentrust-io:main Aug 20, 2026
11 of 13 checks passed
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