[Bug]: §8.4.1 canonicalization is under-determined - two SDKs produce different bytes for the same card, and neither reproduces the section's own worked example
- 主要言語
- Shell
- スター
- 25.7k
- フォーク
- 2.6k
- 平均マージ
- 3日 6時間
- マージ済み PR(30日)
- 16
説明
**What happened?**
Section 8.4.1 requires Agent Cards to be canonicalized per RFC 8785 before signing. Two first-party SDKs implement this and they produce different bytes for the same card, so a signed card that verifies under one is rejected by the other. A third implementation, written from the section text rather than from either SDK, would produce a third answer, because the section's summary of RFC 8785 does not match RFC 8785 and its field-presence rule cannot be satisfied by the serializer ADR-001 makes normative. I am filing this against the specification rather than against either SDK because two of the three problems are in the text, and the third is a bug I am reporting separately.
Concretely, `a2a-python` 1.1.2 and `@a2a-js/sdk` 1.0.1 diverge on three axes. On string serialization, the Python SDK emits `\uXXXX` escapes for every non-ASCII character while RFC 8785 §3.2.2.2 requires literal UTF-8. On object key ordering, the Python SDK sorts by Unicode code point while RFC 8785 §3.2.3 requires ordering on UTF-16 code units, and the two orders differ whenever one key contains a non-BMP character and another contains a BMP character at or above U+E000; this is reachable in a conformant card through the `security_schemes` map and through `AgentExtension.params`. On numbers, the Python SDK emits `1e-06` where RFC 8785 §3.2.2.3 requires the ECMAScript algorithm and therefore `0.000001`; this is reachable through `AgentExtension.params`, which is a `google.protobuf.Struct`. I re-canonicalized both SDKs' output with two independent RFC 8785 implementations in two languages, `rfc8785.py` and `gowebpki/jcs`, and both oracles agree that the JS output is conformant on all three axes and the Python output is not on any of them.
The first spec-side issue is that §8.4.1 rule 2 glosses RFC 8785 as "Predictable ordering of object properties (lexicographic by key)", plus "Consistent representation of numbers, strings, and other primitive values", plus whitespace removal. "Lexicographic by key" is not the RFC's rule and it is exactly the phrase that makes `sort_keys=True` in Python or `sort.Strings` in Go read as compliance. The gloss also omits the two rules that actually cause divergence in practice, literal UTF-8 output and ECMAScript number serialization, while presenting itself as a summary of what the RFC specifies. An implementer who reads those three bullets, implements them faithfully and stops, produces a canonicalizer that is not RFC 8785. I would suggest either quoting §3.2.2.2, §3.2.2.3 and §3.2.3 by number and naming what each requires, or deleting the gloss entirely and letting the normative reference stand alone, because a partial restatement of a normative reference is worse than none.
The second spec-side issue is deeper. Rule 1 says REQUIRED fields "MUST always be present, even if the field value matches the default", and the worked example at the end of the section derives from that: `description: ""` is a REQUIRED field and is included, `skills: []` is a REQUIRED field and is included, and the section prints the canonical result. Neither SDK produces that result. Given the section's own example fragment, both `a2a-python` and `@a2a-js/sdk` produce `{"capabilities":{"pushNotifications":false,"streaming":false},"name":"Example Agent"}`, dropping both `description` and `skills`. They are byte-identical to each other here, so this particular divergence is invisible to any cross-implementation test and would only surface when a third implementation is written from the text. I believe the cause is structural rather than careless. `description` is declared `string description = 2 [(google.api.field_behavior) = REQUIRED]`, a proto3 singular string with no presence, and ADR-001 makes ProtoJSON the normative serialization. ProtoJSON does not read `field_behavior` and cannot distinguish "REQUIRED and defaulted" from "unset", so rule 1 asks for an output that the normative serializer cannot express without a field-behavior-aware encoder that no stock library provides. The section needs to say how those bytes are produced, or change rule 1 to match what a ProtoJSON encoder can actually emit and update the worked example accordingly. Either resolution is fine; the current state is that the specification prints an example neither implementation can reproduce.
A third, much smaller thing while §8.4 is open: §8.4.2 lists `typ` under the heading "The protected header MUST include:" while the bullet itself says `typ` "SHOULD be set to \"JOSE\" for JWS". A SHOULD nested inside a MUST list reads two ways and the two SDKs read it differently. The JS verifier hard-rejects any signature whose protected header lacks `typ`, and the Python SDK, through PyJWT's defaults, emits `typ: "JWT"` when the caller does not set it. Whether presence of `typ` is required and whether the value `"JWT"` is acceptable are both currently unanswerable from the text.
I have a reproduction for all of the above, executed against the published packages with passing ASCII-only controls in both directions, and I am happy to open a PR against §8.4.1 once the direction on rule 1 is settled. I would also like to offer a language-neutral conformance corpus for this section, since the four `CARD-SIGN-*` requirements in the TCK are currently tagged `not-automatable` and carry no vectors; I will raise that separately on `a2a-tck` so this issue stays about the text.
**Relevant log output**
```
a2a-python 1.1.2 signs, @a2a-js/sdk 1.0.1 verifies:
C1 ascii-control PASS canonical bytes identical
C2 name="Café Agent" FAIL canonical bytes differ
C3 astral securityScheme key FAIL canonical bytes differ
C4 params {tolerance: 0.000001} FAIL canonical bytes differ
@a2a-js/sdk 1.0.1 signs, a2a-python 1.1.2 verifies:
J1 ascii-control PASS
J2 name="Café Agent" FAIL InvalidSignaturesError
J3 astral securityScheme key FAIL InvalidSignaturesError
J4 params {tolerance: 0.000001} FAIL InvalidSignaturesError
RFC 8785 adjudication (rfc8785.py and gowebpki/jcs v1.0.1, independent, agree):
C2/C3/C4 a2a-python output == RFC 8785 : NO
C2/C3/C4 a2a-js output == RFC 8785 : YES
Spec §8.4.1 worked example:
spec says : {"capabilities":{"pushNotifications":false,"streaming":false},"description":"","name":"Example Agent","skills":[]}
a2a-python: {"capabilities":{"pushNotifications":false,"streaming":false},"name":"Example Agent"}
a2a-js : {"capabilities":{"pushNotifications":false,"streaming":false},"name":"Example Agent"}
```
コントリビューションガイド
評価
この issue はまだ評価されていません。