Skip to content

docs: add jcs_edge_v1 conformance appendix for Content Integrity Profile (#1140) - #2219

Draft
eriknewton wants to merge 4 commits into
a2aproject:mainfrom
eriknewton:jcs-edge-v1-conformance-appendix
Draft

eriknewton wants to merge 4 commits into
a2aproject:mainfrom
eriknewton:jcs-edge-v1-conformance-appendix

Conversation

@eriknewton

Copy link
Copy Markdown

Adds the jcs_edge_v1 conformance vectors as a standalone appendix for the
Content Integrity Profile discussed in #1140.

jcs_edge_v1 was authored by chopmob-cloud (AlgoVoi) and posted to #1140 on
2026-07-19. It pins the RFC 8785 edge cases a single self-check misses:
U+2028/U+2029 as literal UTF-8 bytes, property-name ordering by UTF-16 code
unit rather than code point, and 1.0 folding to 1. I reproduced all ten
vectors byte-for-byte on 2026-07-19 and turned that into a standing CI check
in Concordia (eriknewton/concordia-protocol#212). kuangmi-bit's a2a-go#368
canonicalizer already passes the same section 3.2.2.2 cases and is expected
to cross-run this corpus.

The profile spec text itself hasn't landed here yet (#1141 closed without
merging), so this lands as a standalone proposal document under
proposals/content-integrity-profile/ rather than inside
docs/specification.md. It can move once the profile text is accepted.

Retention layout mirrors concordia-protocol#212: vectors, upstream LICENSE
and NOTICE, and a PROVENANCE.json pinning the SHA-256 of each file plus the
source commit.

Refs #1140.

…ile (a2aproject#1140)

Retains chopmob-cloud's jcs_edge_v1 RFC 8785 canonicalization edge-case
vectors byte-verbatim under proposals/content-integrity-profile/, using
the retention layout established in eriknewton/concordia-protocol#212
(pinned SHA-256, upstream LICENSE/NOTICE, provenance record).

The Content Integrity Profile spec text itself has not landed in this
repository (PR a2aproject#1141 closed without merging), so this lands as a
standalone proposal document rather than inside docs/specification.md.

Refs a2aproject#1140.
@eriknewton
eriknewton requested review from a team as code owners September 7, 2026 15:09
@msampathkumar

Copy link
Copy Markdown
Member

Hi @eriknewton. Thank you for the pro-active work on this.

For the major changes and updates like this PR, please present your ideas and proposal in A2A TSC to vet this idea and implementations. As the TSC composes of the teams that maintain the A2A SDKs, it would be super valuable.

Till then, I suggest you to mark this PR as DRAFT.

@msampathkumar msampathkumar added the extension-proposal Proposal for an A2A protocol extension or binding label Sep 9, 2026
@eriknewton
eriknewton marked this pull request as draft September 9, 2026 14:08
@eriknewton

Copy link
Copy Markdown
Author

Thanks @msampathkumar. Marked as draft. Happy to bring the conformance appendix to the TSC: a standalone vector corpus with SHA-256 pins and AlgoVoi attribution, no spec text. What is the right way onto the agenda, and is there a preferred write-up? The corpus and cross-language results are on #1140 for anyone reading before then.

@kuangmi-bit

Copy link
Copy Markdown

Cross-implementation check on the corpus this PR lands — I ran jcs_edge_v1 through the Go canonicalizer in a2aproject/a2a-go #368 (head ea003f9), which is the same routine that produces the Agent Card signing payload.

Corpus integrity first. sha256(jcs_edge_v1.json) = a8a1a1a8839553ea5309c381b39ba156e6b6a23a5a3e6aab59b53940cc386033, matching retained_sha256 in PROVENANCE.json — so what was under test is the byte-verbatim retained artifact, not a re-fetch. Worth keeping this check in the harness: it is the difference between a cross-check that proves something and one that cross-checks a copy against itself.

Result: 10/10 byte-identical, on both decode paths (json.Number preserved, and direct float64). Each row below matches both expected_jcs_bytes_b64 and expected_sha256:

# vector RFC 8785 § expectation canonical bytes (b64) sha256 verdict
001 sep-in-value 3.2.4 literal utf8 separators eyJrIjoiYeKAqGLigKljIn0= cf1040eecddd4dbe… ✅ byte-identical
002 sep-in-key 3.2.4 literal utf8 separator in key eyJh4oCoYiI6IngifQ== 4a95154197d0428e… ✅ byte-identical
003 nonbmp-key-order 3.2.3 utf16 code-unit order, emoji first eyLwn5iAIjoyLCLvv78iOjF9 48c5d098713870a7… ✅ byte-identical
004 nonbmp-key-order-multi 3.2.3 utf16 code-unit order (multi) eyJ6Ijo0LCLDqSI6Mywi8J+SqSI6NSwi8J+YgCI6MSwi77+/IjoyfQ== 84c14af1e8d9ec26… ✅ byte-identical
005 number-one-float 3.2.2.3 1.0 → 1 eyJuIjoxfQ== 2bfd14f43d17fc7c… ✅ byte-identical
006 number-one-int 3.2.2.3 1 → 1 eyJuIjoxfQ== 2bfd14f43d17fc7c… ✅ byte-identical
007 mandatory-short-escapes 3.2.2.2 \b \f \n \r \t short forms eyJzIjoiXGJcZlxuXHJcdCJ9 86e432364fb52224… ✅ byte-identical
008 control-u-escape 3.2.2.2 lowercase \u00xx eyJzIjoiXHUwMDAwXHUwMDAxXHUwMDFmIn0= 3c7d87189d5a604e… ✅ byte-identical
009 solidus-and-html-literal 3.2.2.2 /, &, <, > literal eyJzIjoiYS9iJmM8ZD5lIn0= b4f255e4d4875eb6… ✅ byte-identical
010 accented-nfc-literal 3.2.4 NFC accented char literal UTF-8 eyJzIjoiY2Fmw6kifQ== 298ebe9dfd002291… ✅ byte-identical

Method: full file sha256 recorded before the run; each preimage decoded twice (number-preserving and plain); canonical output compared as bytes against expected_jcs_bytes_b64 and re-hashed against expected_sha256. No vector was special-cased or skipped; the two paths agree byte-for-byte on all ten (i.e. for these inputs the integer-vs-binary64 branch in the Go implementation collapses to a single behaviour).

Two notes for the profile, both from this run:

  1. Vector 003 is the highest-value case in the set. The UTF-16 code-unit ordering rule is precisely where a "sort keys as code points" implementation diverges — silently, and only on non-BMP keys. A self-check that round-trips through the same implementation cannot see it; a pinned corpus can.
  2. 005/006 pin the number form, not the number. Both expectations are the single byte string {"n":1}, which is what makes them useful as a conformance pair: an implementation that preserves the source token (1.0) instead of the ECMAScript Number::toString form fails 005 while passing 006.

This is offered as cross-implementation evidence for the profile, not as a merge request on the corpus — the appendix is doing its job as written.

One caveat I want to state plainly rather than leave to inference: this is one independent implementation agreeing with a corpus authored elsewhere. The reproducibility claim the profile actually needs is three-way (the vector set's own reference implementation, plus at least one implementation per SDK language). If it's useful, I can extend the harness to run the same corpus through the Python rfc8785 reference and the JS canonicalize package and post a three-way table — say the word and I'll add it.

@eriknewton

Copy link
Copy Markdown
Author

This is the check the profile needs, and the corpus-integrity step first is the part that makes it evidence rather than a copy compared to itself. Ten for ten byte-identical through the Go card-signing path, with vector 003 and the 005/006 pair doing their job, is a strong data point. Yes, please extend it to the three-way table with the Python rfc8785 reference and the JS canonicalize package. A reference plus one implementation per SDK language is exactly the reproducibility bar, and having it posted before the TSC review would help the case directly. Thank you for running it against the retained bytes.

@kuangmi-bit

Copy link
Copy Markdown

Cross-implementation results for jcs_edge_v1 (three-way table)

Evidence, not a review. I ran the retained jcs_edge_v1 corpus through three independent JCS implementations and compared each output byte-for-byte and re-hashed it against the vector's own expected_jcs_bytes_b64 / expected_sha256. No implementation was given the expected bytes as input — only the preimage.

Corpus (this PR):

  • proposals/content-integrity-profile/vectors/jcs_edge_v1/jcs_edge_v1.json
  • PR head sha: 14df0824560345fb0430a241e9c7728d9cd95b69
  • sha256 (verified against PROVENANCE.json retained_sha256 before running): a8a1a1a8839553ea5309c381b39ba156e6b6a23a5a3e6aab59b53940cc386033

Implementations:

  • Go: a2aproject/a2a-go PR feat: Testing Docs CI publish and build workflow #368, head ea003f9afb3312d88cc06fc7f38a98d1627beeb4 — unexported a2acrypto.jcsMarshal, probed from a package-internal test through both the production decode path (json.NewDecoder + UseNumber) and a direct json.Unmarshal into any (float64).
  • Python: rfc8785 0.1.4 (Trail of Bits reference implementation); preimage parsed with plain json.loads (binary64 doubles), no float hand-tuning.
  • JS: canonicalize 5.0.0 (erdtman) on Node v22.23.1; preimage via JSON.parse.
vector expected sha256 (12) Go (UseNumber) Go (float64) Python rfc8785 JS canonicalize verdict
jcs-edge-001-sep-in-value cf1040eecddd ✓ ✓ ✓ ✓ PASS
jcs-edge-002-sep-in-key 4a95154197d0 ✓ ✓ ✓ ✓ PASS
jcs-edge-003-nonbmp-key-order 48c5d0987138 ✓ ✓ ✓ ✓ PASS
jcs-edge-004-nonbmp-key-order-multi 84c14af1e8d9 ✓ ✓ ✓ ✓ PASS
jcs-edge-005-number-one-float 2bfd14f43d17 ✓ ✓ ✓ ✓ PASS
jcs-edge-006-number-one-int 2bfd14f43d17 ✓ ✓ ✓ ✓ PASS
jcs-edge-007-mandatory-short-escapes 86e432364fb5 ✓ ✓ ✓ ✓ PASS
jcs-edge-008-control-u-escape 3c7d87189d5a ✓ ✓ ✓ ✓ PASS
jcs-edge-009-solidus-and-html-literal b4f255e4d487 ✓ ✓ ✓ ✓ PASS
jcs-edge-010-accented-nfc-literal 298ebe9dfd00 ✓ ✓ ✓ ✓ PASS

✓ = canonical output bytes equal the base64-decoded expected_jcs_bytes_b64 and sha256 equal expected_sha256. All 10 vectors, 4 output columns each (40 cross-checks): all agree.

Method: each vector's preimage was canonicalized independently by each implementation; no vector was special-cased, skipped, or re-generated, and no implementation was seeded with the expected output.

Limits: the Go leg probes the canonicalizer (jcsMarshal) that the Agent Card signing path uses, via a temporary in-package test, but does not exercise the full canonicalPayload end-to-end over a complete AgentCard JWS (the signatures-stripping and payload wiring are separate from JCS byte production and are not what these vectors test). All three legs ran clean; no divergence to report.

@eriknewton

Copy link
Copy Markdown
Author

@msampathkumar quick housekeeping ahead of the TSC.

  1. The PR is ready for the 22 September 09:00 PT slot you suggested. The three-way cross-implementation table from @kuangmi-bit above is the evidence I plan to walk through; ten minutes is enough.

  2. Check linked issues is red only because this appendix adds vectors to Proposal: Content Integrity Profile for A2A artifacts (hash + signature + schemaRef) #1140 rather than closing it. Could a maintainer apply the no-issue label? If you would rather I reference an issue in the body, say which and I will edit.

Thanks. Draft status stays until you say otherwise.

@astrogilda

Copy link
Copy Markdown

Opened #2246 with jcs_depth_v1 beside this appendix, for the nesting bound RFC 8785 leaves open. @kuangmi-bit, the pair I would point the Go canonicalizer at is jcs-depth-004 against jcs-depth-103, because a counter charging per parsed child takes the deeper one. That is the discriminating case. I read writeJCS and sortObjectKeys at ea003f9 and found no depth counter, without having run the vectors there.

@eriknewton

Copy link
Copy Markdown
Author

Gentle nudge before Tuesday's TSC: could a maintainer apply no-issue (or tell me the linked-issue shape you want)? The appendix adds vectors to #1140 rather than closing it.

@kuangmi-bit

Copy link
Copy Markdown

Ran it against the Go SDK. Corpus as downloaded from #2246 and pinned before running: jcs_depth_v1.json sha256 7e262412c3dea78713a950219f0413ee0382a48d43cf4698bd6d67214c78cdce, PROVENANCE.json 123ebefa29582557f0660146557db71fbffd0a5bc0f86542d0dbb94944f07662, README.md 6efefe6c7f317e6d4b07fefa121fc1fa1a89b4d8a58838b511ea6aedc0063ba0. Preimages were materialized from the rules and checked against preimage_sha256 before canonicalizing, per the README. Code: a2a-go at eddcf62 (current head of #368).

vector depth expected ours
jcs-depth-001-object-at-bound 128 accept bytes and SHA-256 match
jcs-depth-002-array-at-bound 128 accept bytes and SHA-256 match
jcs-depth-003-alternating-at-bound 128 accept bytes and SHA-256 match
jcs-depth-004-empty-object-leaf-at-bound 128 accept bytes and SHA-256 match
jcs-depth-101-object-one-past-bound 129 reject accepted, eeb23e8c9c09
jcs-depth-102-array-one-past-bound 129 reject accepted, 84aaf90be326
jcs-depth-103-empty-object-leaf-one-past-bound 129 reject accepted, 47ec83edd0d1
jcs-depth-104-unbounded-recursion 10^7 reject without exhausting the stack refused to the caller: exceeded max depth, no crash

On the pair you named, we accept both halves. jcs-depth-004 reproduces its expected_sha256 exactly, and jcs-depth-103 is accepted too, which by the corpus's own rule means the implementation has no bound. You read the code correctly: there is no depth counter in writeJCS, sortObjectKeys, or anywhere in the package.

Two things from the run that I would want the profile to have, because they change how a Go reader should interpret the jcs-depth-104 row.

  1. The refusal on jcs-depth-104 does not come from us. It comes from encoding/json's jsontext decoder, which caps nesting at maxNestingDepth = 10000 and returns exceeded max depth. So the effective bound on this path is 10,000, chosen by a stdlib detail a reader of our code cannot see, and 78 times the proposed 128. Our own walk is recursive and carries no counter, so it would have the same failure mode as gowebpki/jcs on that input if the decoder ever handed it over. Nothing here is a violation of RFC 8785, which states no bound, but the row reads as a property of this implementation when it is a property of the toolchain.
  2. Both decode paths in the package (UseNumber and float64) agree on all eight vectors, includes the three we wrongly accept, so none of this splits on number handling.

If the profile settles on 128 counted as open containers, implementing it here is small and contained: a counter advanced when a container opens, checked before the recursive walk, returning an error to the caller. I am glad to do it, as its own change rather than folded into the signing work in #368, and only once the profile's bound is settled rather than while it is a proposal.

@eriknewton

Copy link
Copy Markdown
Author

Thank you for pinning the corpus and running all eight. This is the result the corpus exists to produce: the at-bound rows match, and 101 to 103 accepted means no bound, as you read from the code. Your two notes change the appendix text and I will fold them: the 104 row must say that a refusal supplied by the decoder (jsontext's 10,000) is a toolchain property, and the profile must require the implementation to count open containers itself, so a Go reader does not mistake the stdlib cap for conformance. The bound's value (128, counted as open containers) is what I am asking the TSC to settle on Tuesday; your counter as its own change after that is the right sequencing.

astrogilda added a commit to astrogilda/A2A that referenced this pull request Sep 23, 2026
The README now says 128 is a proposed profile choice, required by neither
RFC 8785 nor an adopted A2A rule, and states the grounds the in-toto
proposal gives for it. The unsourced second-serializer claim is withdrawn
from both the README and PROVENANCE.json, and replaced by the third-party
run against the a2a-go canonicalizer reported on a2aproject#2219, including its
finding that the jcs-depth-104 refusal there came from the encoding/json
decoder rather than from the canonicalizer. The gowebpki/jcs measurement
now names its version, and notes that v1.0.2 adds a limit of 10,000.
PROVENANCE.json pins the new README digest and names the source corpus by
its current repository path.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extension-proposal Proposal for an A2A protocol extension or binding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants