Repository navigation
Proposal: require canonical base64url encoding for the embedded signature (#247) - #401
Conversation
…trust-io#247) Section 3.2.2 already names the embedded `signature` field's encoding as base64url, no padding, but does not say whether a different spelling of the same bytes is the same record. A 64-byte signature (Ed25519 or ES256) spends 86 base64url characters on 512 bits, leaving 4 unused bits in the final character. RFC 4648 section 3.5 requires those bits to be zero. Add one sentence to the embedded-signature bullet requiring the canonical spelling and requiring a verifier to reject a record whose signature is not canonically encoded. Tighten the schema `pattern` so an 86-character value's final character must be one of the four canonical values (A, Q, g, w). Apply the same change to the packaged schema copy and to the v0.3 draft schema (regenerated from the canonical schema by its own generator script, which propagates the change unchanged). Update validate.py's ECMA-to-Python pattern registry to match, and extend docs/schema.md's field description. No other schema pattern is touched. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
…ner (agentrust-io#247) Add examples/signature-encoding/: a canonical signed Trust Record and two non-canonical respellings of the same 64 signature bytes, one with the final character's unused bits set to 0001 and one to 1111. Two rejecting vectors rather than one, so the boundary is not covered by a single bad string a shortcut implementation could special-case. The canonical vector reuses the record, key, and signature already published in examples/verifier-compatibility/, introducing no new signing key. Add scan_published_signatures.py, a re-runnable script that walks any number of given roots for 86-character `signature` values and reports how many are already canonically encoded. Add tests/test_signature_encoding.py, which recomputes from the committed bytes (not the fixtures' own declarations) that all three signatures decode to the same 64 bytes, that canonicality matches each fixture's `encoding` field, that the canonical vector validates and verifies, that both respellings are rejected by schema validation, by verify_record, and by the pydantic model, and that the raw signature bytes still verify cryptographically under every spelling, isolating the encoding as the rejection cause rather than a broken signature. Register the new vector set in the adequacy framework and add a CHANGELOG entry. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
|
Alex, classify this as an incompatible tightening of the draft’s acceptance rules. I support requiring canonical encoding. Please document which previously accepted spellings are rejected and how existing records should be handled. Keep the scan’s conclusion limited to the repositories and revisions examined. The normative wording will need a maintainer to carry it; this scope decision is not implementation approval. |
…spellings, say how existing records are handled, scope the scan (agentrust-io#247) Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
|
@chernistry |
…lper (agentrust-io#247) main gained agentrust-io#412 (shared schema regex semantics) and agentrust-io#418 (strict base64url decoding) after this branch was cut, and both touch the files this proposal changes. - models.py: `signature` takes `_SIGNATURE_RE` through `_require_pattern`, the same way `subject`, the digests and `retention` do. - _patterns.py: the fourth shipped pattern form is the signature pattern, so the table holds the proposed string in place of the previous one. - validate.py: main's version; the table it carried now lives in _patterns.py. - CHANGELOG.md: both sides kept, this entry first under Unreleased. - tests/test_signature_encoding.py: `verify_record` still refuses a non-canonical respelling, but since agentrust-io#418 the decoder refuses it before the schema step runs, so the test pins the refusal and the field it names, not which step raises it. The schema, the specification sentence and the vectors are unchanged. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
….0; hold the schema and the model to one answer (agentrust-io#247) The README and the CHANGELOG entry described the reference library as verifying a non-canonical respelling. That was true through 0.10.0. Since 0.11.0 (agentrust-io#418) its decoder refuses the spelling, and verify_record reports that refusal before the schema step. Both texts now say so, and say what the proposal still adds: the refusal on the two surfaces that never decode the value, schema validation and the models. Tests: - the non-canonical vectors are refused by the schema at `signature`, by `pattern`, checked through iter_errors; - a boundary sweep runs the schema and the model over the same eleven values (85, 86 canonical, 86 non-canonical, 87, 128, empty, four line terminators, padding) and requires the same verdict from both; - the independent ECMA-262 comparison gains 85- and 86-character samples, so Node checks the branch of the pattern this proposal adds. Scan re-run at today's revisions: examples/ at 63f4d15, trace-tests at da4369b, trace-registry at e26b85a: 232 signatures, all canonical. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
Conflicts in CHANGELOG.md, src/agentrust_trace/intent_bridge.py and tests/test_adequacy_all_sets.py, after agentrust-io#401, agentrust-io#432 and agentrust-io#451. Not mechanical, so listed: - intent_bridge.py. agentrust-io#432 moved the observation out of the bridge transcript into the successor envelope and its signed artifact. The envelope's `observed_at` keeps main's wording and this branch's decision by value. The artifact's own `observed_at`, which main added with a type test, is decided by value as well: the artifact is signed over its RFC 8785 form and is compared with the envelope, so `150.0` there was a spelling the signature covers and only the type test refused. Each refusal names its reason, as the envelope's does. - tests/test_integer_by_value.py. The two observation-time tests are rewritten against sign_successor_artifact and verify_successor_artifact, with the same refusals for the artifact's copy of the time. - examples/number-spelling/scan_published_numbers.py. `observed_at` is read from schema/pic-trace-successor-v1.json now, so it leaves the short list of members named only in prose. The scan's member list gains the integer members of the schemas added on main; it still finds no published example the rule changes. - tests/test_adequacy_all_sets.py keeps both sets, number-spelling and signature-encoding. - CHANGELOG.md: main's entries, then this proposal's, with the bridge sentence naming the successor envelope and artifact. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
What this changes
Section 3.2.2 already names the embedded
signaturefield's encoding asbase64url, no padding. It does not say whether a different spelling of the
same bytes is the same record. This proposal adds one sentence that says it
isn't: the value must be canonically encoded, and a verifier must reject a
record whose signature is not.
This is a proposal, not a merge request for accepted text. Opened per the
request on #247 for a small, focused change with vectors and a scan of
existing data, rather than a bundle covering every related field.
What's open today
A signature over a Trust Record's canonical JSON form is 64 raw bytes for
the two algorithms records use in practice (Ed25519, and ES256 over P-256 as
raw
r‖s); ES384, which section 3.2.1 also lists for JWT contexts, yields96 bytes, 128 characters and no unused bits, so the question does not arise
there. Base64url without padding spends 86 characters (516 bits) encoding
512 real bits, so the final character carries 4 bits that no signature
algorithm produces. RFC 4648 section 3.5 requires those bits to be zero;
a decoder that only restores padding and decodes (rather than also checking
them) silently accepts a spelling where they aren't. Section 3.2.2 is silent
on which behavior is required, so two conformant-looking verifiers can
currently disagree about whether the same 64 bytes, spelled two different
ways, are the same record.
What this change does
spec/trace-v0.2.md, section 3.2.2, embedded-signature bullet):adds one sentence requiring the canonical base64url spelling (RFC 4648
section 3.5) and requiring a verifier to reject a non-canonical one.
schema/trace-claim.json, its packaged copy atsrc/agentrust_trace/schema/trace-v0.2.json, and the v0.3 draft schema,which inherits
signaturefrom the canonical schema and picks up thesame change when regenerated): tightens the
signaturepatternso an86-character value's final character must be
A,Q,g, orw— thefour base64url characters whose low 4 bits are zero. No other pattern in
any of these files is touched.
src/agentrust_trace/models.py): the same tightened pattern onthe
signaturefield, plus an extended docstring.src/agentrust_trace/validate.py): the ECMA-to-Pythonpattern-translation registry updated to carry the new pattern string
(required — the registry is keyed by exact pattern text, so a schema
pattern change without a matching entry here would raise
KeyErroratvalidation time rather than silently doing the wrong thing).
docs/schema.md): thesignaturefield row gets the samecanonical-encoding note.
examples/signature-encoding/): three signed Trust Recordsand a re-runnable scan script; see below.
tests/test_signature_encoding.py,tests/test_adequacy_all_sets.py): recompute the vectors' claims fromcommitted bytes rather than trusting them, and register the new vector
set with the repo's vector-adequacy checks.
Scan: how many published signatures would stop passing
examples/signature-encoding/scan_published_signatures.pywalks any numberof given roots, finds every 86-character value at the key
signature, andreports how many are already canonically encoded. It is not scoped to this
repo — pass it other checkouts to extend the count.
Run against this repository's own
examples/, theagentrust-io/trace-testsconformance corpus pinned at
3af2b53, and a read-only clone ofagentrust-io/trace-registry:examples/(this repo)trace-tests@3af2b53trace-registryThe 2 non-canonical values are not pre-existing records: they are this
proposal's own two deliberately non-canonical example vectors (below),
counted because the script scans
examples/unconditionally. Excludingthose 3 new files (1 canonical reuse + 2 non-canonical), 231 signatures
predate this change, and all 231 are already canonical. Under this
proposal, none of them would stop passing.
The 216 I gave on #247 on 22 September for the first two roots was an
undercount: the same two roots hold 227 signatures, all canonical, so the
conclusion there stands.
Vectors
examples/signature-encoding/adds:01-canonical-signature.json— a signed Trust Record whose signature iscanonically spelled. Reuses the record, trusted key, and signature already
published in
examples/verifier-compatibility/01-known-version-verified.json,so the set introduces no new signing key.
02-non-canonical-respelling-low-bit.jsonand03-non-canonical-respelling-all-bits.json— the same 64 signature bytes,respelled with the final character's unused bits set to
0001and1111respectively. Two rejecting vectors rather than one, so the boundary isn't
covered by a single bad string a shortcut implementation could
special-case.
All three carry a
decoded_signature_hex;tests/test_signature_encoding.pydecodes each
record.signatureitself and asserts the raw bytes match acrossall three, rather than trusting the declared value. It also confirms the raw
Ed25519 bytes verify cryptographically under every spelling, isolating the
encoding as the rejection cause rather than a broken signature.
See
examples/signature-encoding/README.mdfor the full format and theadequacy rationale.
Current behavior of both implementations
Checked against
mainat the base this branch was cut from (includes #390):respelling leniently — the unused bits are discarded during decode, not
checked — and verifies it. Confirmed with a standalone reproducer against
the current
sign/verify_recordpath; fix: distinguish missing and malformed signatures #390 (missing vs. malformedsignature handling) doesn't touch this, since a non-canonical spelling is
neither missing nor malformed in the sense that PR addresses.
spelling, reporting
signature_malformed.So the two implementations already disagree, today, before this proposal.
Tightening the schema pattern (this PR) makes the reference library agree
with that refusal — through the schema-validation step both implementations
already run every record through first, rather than through a new check
specific to this one field.
Out of scope (follow-ups, not folded into this proposal)
cnf.jwk.xand other embedded public-key coordinates raise the samecanonical-encoding question and are not covered here.
sig.valueinschema/trace-revocation.json/schema/trace-revocation-bundle.jsoncarries the same base64url patternand the same question, independently noticed while scoping this change.
Its pattern is untouched by this PR.
Change classification
This PR doesn't set MAJOR/MINOR/PATCH for itself — that's the maintainer's
call. The scan above is meant to give that decision an evidence base: as far
as this scan reaches, no currently-published signature stops validating.
Type of change
Breaking or non-breaking is left to you; see "Change classification" above.
Spec section
§3.2.2 (Mandatory signature and freshness binding), the embedded-signature bullet.
Checklist
git commit -s)CHANGELOG.mdupdated<!-- CHANGED: #247 - embedded signature MUST be canonical base64url -->in spec textContext
Requested by @imran-siddique on #247, who asked for a small, focused
proposal rather than a bundle. Proposed by @chernistry.