Repository navigation
fix(security): strict signature decoding, documented refusals, ClusterFuzzLite - #418
Merged
Merged
Conversation
…rFuzzLite - sign._b64url_decode accepts only canonical unpadded base64url, so one signature has one spelling (Trust Record, JWK x, provenance, bridge and revocation-bundle signatures). - content_marking refuses served record bytes with duplicate members and keeps deep nesting and oversized ports on ContentMarkingError. - RecursionError from deeply nested cnf.jwk, tool calls and provenance records is reported on each module's documented error type. - tool_catalog_hash raises ProvenanceError (still an UnanchorableValue) for a catalog with no anchor form. - ClusterFuzzLite targets over the record, bundle, provenance, bridge, content-marking and canonicalization paths (Scorecard FuzzingID #42). - runtime-evidence.yml installs from hash-pinned locks (Scorecard #113, #114); requirements/dev.txt recompiled --universal with the same pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
imran-siddique
requested review from
a team,
lywinged and
rajnisht7
as code owners
September 25, 2026 21:04
imran-siddique
added a commit
that referenced
this pull request
Sep 25, 2026
Bump pyproject.toml to 0.11.0 and close the changelog. Add entries for sixteen merged changes that had none, and move the #239 entry out of the 0.10.0 section, which it joined after 0.10.0 was tagged. Add an entry for #418, which merged without one, alongside the breaking #419 entry. Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
4 of 9 tasks
5 of 9 tasks
chernistry
added a commit
to chernistry/trace-spec
that referenced
this pull request
Sep 30, 2026
…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>
chernistry
added a commit
to chernistry/trace-spec
that referenced
this pull request
Sep 30, 2026
….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>
chernistry
added a commit
to chernistry/trace-spec
that referenced
this pull request
Sep 30, 2026
…io#247) main gained agentrust-io#418, which wraps rfc8785.dumps in _canonical_bytes so that a value nested past the interpreter stack is reported as a CanonicalizationError. The whole-float range walk recurses over the same containers. Ahead of that guard its own RecursionError came out first, and two of agentrust-io#418's tests failed; inside it the merged tree passes. CHANGELOG.md: both sides kept, this entry first under Unreleased. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
chernistry
added a commit
to chernistry/trace-spec
that referenced
this pull request
Sep 30, 2026
main gained the strict base64url decoder (agentrust-io#418, released in 0.11.0), the shared pattern helper (agentrust-io#412) and provenance v2 (agentrust-io#409). No conflicts. The differential harness is re-baselined against the 0.11.0 reference in the commits that follow. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
chernistry
added a commit
to chernistry/trace-spec
that referenced
this pull request
Sep 30, 2026
…reference; decode the signature where it does Against main the differential no longer held. The reference's decoder refuses base64url that is not canonical since agentrust-io#418 (0.11.0), with two messages the oracle did not classify, and it reports a padded signature or one in the standard alphabet at its decoding step, which comes before its schema check. This implementation consulted the schema first. The run read 1644 cases, 1429 identical, 57 known, 158 unexpected, 4 ledger entries unreached. - oracle.py classifies "signature is not canonical base64url" and "JWK 'x' is not canonical base64url". - verifyRecord decodes the signature ahead of the schema, which is the order the module header already promised. A padded signature, one in the standard alphabet, one with a trailing newline and one of an impossible length are signature_malformed on both sides; the empty string decodes to no bytes and is still refused by the schema's pattern on both sides. A test pins all five. - Five ledger entries that no case reaches are retired: non-zero unused bits in a signature, a truncated signature, a non-canonical x in cnf.jwk or in the caller's key (the reference refuses them now), and the trailing newline (the order here moved). Now: 1644 cases, 1611 identical, 33 known, 0 unexpected, 2 ledger entries, none unreached, 12 published vectors. README and CHANGELOG carry those figures, and the CHANGELOG entry is back under Unreleased, where the merge with main had left it inside the 0.11.0 section. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
imran-siddique
pushed a commit
that referenced
this pull request
Sep 30, 2026
…ature` (#247) (#401) * Require canonical base64url encoding for the embedded signature (#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> * Add signature-encoding vectors, tests, and a published-signature scanner (#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> * docs(signature-encoding): classify the tightening, list the rejected spellings, say how existing records are handled, scope the scan (#247) Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * docs,test(signature-encoding): account for the strict decoder of 0.11.0; hold the schema and the model to one answer (#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 (#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> --------- Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com>
Merged
2 of 5 tasks
imran-siddique
added a commit
that referenced
this pull request
Oct 13, 2026
…ed against the Python one (#376) * feat(trace-verify-ts): add a TypeScript verifier for TRACE v0.2 records A specification with one implementation says whatever that implementation does. This is a second one, written from spec/trace-v0.2.md and schema/, and it runs on WebCrypto alone: no network, no filesystem, no platform module and no runtime dependency, so a record can be checked where there is no Python runtime. What it implements, with the rule each check comes from: - the profile cutover of section 2, with the superseded v0.1 identifier rejected under its own code rather than as an unknown profile; - RFC 8785 canonicalization, including section 3.2.2's requirement to reject an integer outside the JCS safe range and the I-JSON rule that a lone surrogate has no canonical form; - Ed25519 over the pre-image of section 3.2.2, the record with its signature member absent; - canonical unpadded base64url for the signature and for JWK members (RFC 4648 sections 5 and 3.5), so one byte string has one spelling; - RFC 7638 thumbprints, and the confirmation-key binding of section 3.2.2; - freshness and nonce, with the bounds section 3.2.2 states; - the three revocation outcomes of section 3.2.3 against a bundle the caller passes in, with a bundle defect reported as an outcome rather than thrown; - the chain digest of section 3.1.3, over the complete parent record with its signature present. The schemas are compiled ahead of time with ajv, so nothing is evaluated or fetched at run time, `pattern` is read as ECMA-262 as JSON Schema 2020-12 requires, and `format: uri` is asserted rather than annotated. The generator records the SHA-256 of every schema file it read. The order of the checks is the Python implementation's, so both report the same first failure for the same record. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * test(trace-verify-ts): cover the units, this repository's vectors and the build Three files. The unit tests pin the canonical forms and the refusals that have a published answer: RFC 8785 member ordering by UTF-16 code unit, the number and escape forms, the values that have no canonical form, the base64url alphabet and pad-bit rules, and the RFC 7638 section 3.1 example thumbprint. The vector tests run the canonicalization-boundary and revocation-bundle corpora through the verifier and check the outcome, the cause and every evidence member each vector states, plus the chain digest of every link in the delegation corpus. The third file tests the build rather than the logic: the compiled output imports nothing, so a platform module or a network call cannot arrive unnoticed; the schema digests the validators were generated from still match schema/; and every exported function refuses junk with the package's own error type rather than a TypeError, which is what a caller written against the documented contract catches. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * test(trace-verify-ts): score the two implementations against each other A second implementation is only evidence if something compares them. The harness generates a corpus, runs both sides over it and reports every case where they disagree. The corpus holds each record as text, not as a parsed object, because several cases exist to probe a difference that only survives in text: an iat written 1785000000.0, a lone surrogate in a string, a signature spelled with padding. Both runners parse the same bytes with their own JSON parser, which is part of what is compared. It covers a signed record under 24 verifier configurations with 59 mutations each, an RFC 8785 corpus, RFC 7638 thumbprints, the chain digest of every record in the delegation corpus, this repository's own vectors, and the published conformance vectors from a pinned checkout whose files are recorded by SHA-256 rather than vendored. The Python side reports a rejection as an exception, and two distinct checks raise the same message from the same helper called twice, so the classifier reads the traceback to ask which call site it came from, and looks the line numbers up in the installed source rather than writing them down. An exception it does not recognise is reported, not guessed. 1644 cases: 1280 agree exactly, including every published vector, every thumbprint and every chain digest. The other 364 are 13 classes argued in differential/known-divergences.json, which matches on both the case and the pair of reported reasons, so a new disagreement inside a listed family is reported rather than absorbed. Nothing was adjusted to match the other side. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * ci(trace-verify-ts): run the unit tests and the differential harness Two jobs. The unit job builds and tests on Node 20, 22 and 24: the floor is what the package declares and the ceiling is current, so a runtime leaving that matrix is a supported-versions decision rather than a CI adjustment. The differential job installs the Python implementation from this tree, checks out the published conformance vectors at a pinned commit, runs both sides over the generated corpus and fails on any disagreement that is not in the ledger. The vector checkout is allowed to fail: the corpus without it is this repository's own vectors plus the generated matrix, which is the larger part, and a verifier's CI should not go red because another repository is unreachable. Actions are pinned by commit, as the other workflows here are. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * docs(changelog): record the TypeScript verifier and what the differential run found Signed-off-by: chernistry <sanderchernitsky@gmail.com> * fix(trace-verify-ts): accept uppercase IPvFuture version flag Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * differential: assert the published-vector count and hold the ledger in both directions The workflow tested for the trace-tests checkout at a path relative to trace-verify-ts, so the published-vector group never ran in CI: the one run logged 1632 cases where 1644 were expected, and nothing failed. The checkout is now required, generate_cases.py refuses a root it cannot read or that yields no record, and compare.py --expect-external N fails unless exactly N cases came from it. The manifest of vector digests is always written, printed with the count, and uploaded with the report. The ledger failed in one direction only: a declared divergence that stopped being reached lowered the number the run leads with and exited 0. Each entry now declares how many cases land on it, and an entry, a pair or a count the run does not bear out fails the run. Six pairs that attributed a signature or schema divergence to the bundle or trusted-key context move to the entries whose fix alone resolves them; the 364 is unchanged, its split is now 20/14/23/23/24/96/30/69/23/23/2/16/1. Setting the RFC 4648 unused-bits mask to zero, which one unit test already caught, now also fails the differential: entries 2, 3, 4 and 12 unreached, one pair of entry 1 never observed. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * differential: judge a published vector at a valid now generate_cases.py set now from the vector's own iat whenever that was an int, so a vector whose point is an iat outside the safe-integer range made the TypeScript verifier refuse the caller's argument before it read the record. now is taken from iat only inside [0, 2^53-1]. Against trace-tests cf30e9b the corpus goes from 1660/1295/364/1 to 1660/1296/364/0; the pinned corpus is unchanged. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * differential: a text either runner cannot parse is a verdict, not a crash oracle.py parsed record_json, revocation_bundle_json and value_json outside any try, so a text json.loads refuses, or one nested past the interpreter's stack, took the process down with the output file however far it got, where run.mjs reports parse_error and exits 0. Every runner on the Python side now reports parse_error for such a text, a RecursionError inside rfc8785 classifies as canonicalization_failed, which is what the other side reports for the same input, and run.mjs gains the same parse arm on its thumbprint and chain-digest runners. Latent in the committed corpus, since all 1644 texts parse; live for the external one, which is arbitrary JSON from another repository. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * differential: classify the accepted-profiles refusal main now raises Since #116 the reference refuses a record whose eat_profile is outside the verifier's accepted set with a new message. The oracle mapped only the previous wording, so after merging main the 25 m16-profile-unknown cases reported unclassified against the TypeScript profile_unsupported. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * docs(trace-verify-ts): four acceptances rather than five, and what two sentences hold The changelog entry and the description said five of the 13 divergence classes were records the reference accepts. Four are (entries 3, 8, 9, 10); the other two are rejections of a different shape; and the "patterned field" clause covered signature, where the reference rejects. The clause now names subject (U+000A, U+000D, U+2028) and appraisal.verifier (format: uri, U+000A). The schema-digest test is described by what it holds, that a build came from exactly the schema bytes it names, rather than by a rebuild case no invocation in the repository reaches. The canonicalization divergence is named as its class, magnitude 2^53 and above, of which 1.0e+21 is the corpus's instance. A README paragraph says what the differential cannot observe: a constant-time property, and a length function the schema does not exercise. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * refactor(trust-record-verify): rename the package to @agentrust-io/trust-record-verify The package, its directory, the workflow and its path filters, the README and the changelog entry take the new name. The corpus seed in differential/generate_cases.py keeps its original spelling, as the named constant CORPUS_SEED_DOMAIN: every key in the 1644-case corpus derives from it, so changing it reissues the corpus and moves outcomes that known-divergences.json pins by case and by count. Differential against agentrust-trace 0.10.0 after the rename: 1644 cases, 1280 identical, 364 known, 0 unexpected; ledger 13 entries, 71 pairs, 0 unreached; 12 published vectors, manifest sha256:869550bf03073ba3. Unit tests 74/74 on Node 25. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * test(differential): retire the five divergences the reference no longer has #383, #386, #387 and #388 fixed the four defects this harness reported, so the cases that recorded them stopped diverging and the ledger failed closed on five entries no case reached: the lone-surrogate revocation bundle (#382), `subject` and `appraisal.verifier` under ECMA-262 pattern semantics (#379), the IPv4-in-IPv6 literal with a leading zero (#380), and the non-ASCII nonce (#381). They leave the ledger rather than stay as claims nothing checks. Four counts move with them. The `c23-bundle-lone-surrogate` context used to diverge on every mutation and matched the first entry; now that it verifies like any other context, its cases fall through to the mutation families: m03 23 to 24, m04 23 to 24, m20/m21 30 to 32, m46/m47 16 to 17. The ledger's `reference` and the README's table no longer say 0.10.0 without qualification, because the behaviour under test is the tree's and the four fixes are not in that release. 1644 cases, 1412 identical, 232 known, 0 unexpected; ledger 8 entries, 33 pairs, 0 unreached; 12 published vectors, manifest sha256:869550bf03073ba3. Signed-off-by: chernistry <sanderchernitsky@gmail.com> * fix(trust-record-verify): tighten three input guards encodeBase64url takes a Uint8Array and nothing else that views a buffer: a Uint16Array iterates 16-bit values, which the byte loop encoded as the wrong bytes, and a DataView is not iterable, which reached a native TypeError instead of the package's own error. canonicalJson refuses an object with a symbol-keyed member instead of dropping it: Object.keys never listed the symbol, so a value the module documents as unsupported canonicalized to a form missing a member. verifyDelegationLink holds the same plain-object contract for its options as verifyRecord; an array, a Date or a class instance was accepted before. A test for each, with the same-shaped value that is accepted beside it. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * differential: the schema location and the key source are part of the verdict; duplicate ids fail the run compare.py treated two schema_invalid verdicts as agreeing whatever member each side faulted, and only listed the difference as commentary. The check order is shared, so the first member faulted must be the same one: the location is now part of the summary a case has to match, and a difference there is a divergence to be argued in the ledger like any other. At this head every one of the 600 schema failures agrees on the member. VerificationResult.trustedKeySource was outside the comparison, so a regression in it would pass as long as the thumbprint matched, which it does whenever the caller passes the key the record embeds. run.mjs now reports it; oracle.py states the same fact the way verify_record resolves it, since the Python result carries no such member; compare.py matches the two. A unit test pins the case the thumbprint cannot tell apart. Both runners key verdicts by case id, so a second case under one id replaced the first without a trace, and the external ids fold "/" into "-", which is one way two files can collide. generate_cases.py fails on a duplicate id and compare.py fails on a verdict file that repeats one. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * ci(trust-record-verify): run the differential on spec changes too A normative change under spec/ can move the verdict a case is expected to produce without touching the schemas, the examples or either implementation. The path filter listed all of those and not the specification itself, so such a change skipped the differential. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * docs(trust-record-verify): align the README and the changelog with the ledger at this head The README said the 136 remaining cases were seven classes each appearing once per verifier configuration; three do (24 each), the others land on 32, 17 and 14 cases, and one is in the RFC 8785 corpus, not the mutation matrix. The retired entries were listed as "six" in five items, with two folded into one. The table now carries each entry with its pinned count, and the six retired entries are six items. The changelog entry still gave the first run's figures (1280 identical, 364 in 13 classes); it now gives this head's (1508, 136 in 7) and says what left and what remains. Both also say that the differential compares the location of a schema failure and the source of the verifying key. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * docs(trust-record-verify): state the section 3.2.3 subset this package implements The revocation consumer checks the bundle signature and applies a statement naming the trusted key as the section's fallback for a record without a usable receipt. It does not verify a statement's own signature against the section 3.2.1 hierarchy, and it takes no inclusion entry ID, so the last_valid_entry_id rule is not applied. The module header said so; the README's limits and the option's own documentation now say it too, and name it as the same subset the Python implementation documents. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * fix(trust-record-verify): re-baseline the differential on the 0.11.0 reference; decode the signature where it does Against main the differential no longer held. The reference's decoder refuses base64url that is not canonical since #418 (0.11.0), with two messages the oracle did not classify, and it reports a padded signature or one in the standard alphabet at its decoding step, which comes before its schema check. This implementation consulted the schema first. The run read 1644 cases, 1429 identical, 57 known, 158 unexpected, 4 ledger entries unreached. - oracle.py classifies "signature is not canonical base64url" and "JWK 'x' is not canonical base64url". - verifyRecord decodes the signature ahead of the schema, which is the order the module header already promised. A padded signature, one in the standard alphabet, one with a trailing newline and one of an impossible length are signature_malformed on both sides; the empty string decodes to no bytes and is still refused by the schema's pattern on both sides. A test pins all five. - Five ledger entries that no case reaches are retired: non-zero unused bits in a signature, a truncated signature, a non-canonical x in cnf.jwk or in the caller's key (the reference refuses them now), and the trailing newline (the order here moved). Now: 1644 cases, 1611 identical, 33 known, 0 unexpected, 2 ledger entries, none unreached, 12 published vectors. README and CHANGELOG carry those figures, and the CHANGELOG entry is back under Unreleased, where the merge with main had left it inside the 0.11.0 section. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * fix(trust-record-verify): type tests that do not depend on the realm; run the differential on dependency changes Review points on #376. encodeBase64url tested `instanceof Uint8Array`, which is false for a Uint8Array made in another frame or worker. It now reads the typed array's [[TypedArrayName]] through the %TypedArray% toStringTag getter: true for a Uint8Array of any realm, Buffer included, and false for a Uint16Array, a DataView, an ArrayBuffer, an array, and an object that only carries its own Symbol.toStringTag, which a test on Object.prototype.toString would accept. isPlainObject compared the prototype with this realm's Object.prototype, so a plain object from another realm was refused: the options of verifyDelegationLink and verifyRecord, and equally a record parsed by another frame's JSON.parse, a JWK, and any object reaching the canonicalizer. It now accepts an object with no prototype or with a prototype that has none of its own. Arrays, Dates, class instances and Object.create({}) are refused as before, from either realm. The revocation store was tested with `instanceof Set`, the same fault; it now asks the Set size getter, which answers for a Set of any realm and throws for anything else. Tests build each value in a second realm with node:vm: a record, its options and a revocation set from there verify as the same values do here. The workflow's path filters gain requirements/** and pyproject.toml, which the differential job installs the reference from. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * fix(trust-record-verify): own-member reads, a stricter plain-object test, profile on the result, symbol keys on arrays Four points from review of 0655596. isPlainObject accepted Object.create(Object.create(null)): a prototype with a null prototype is not Object.prototype. It now accepts a null prototype or a realm's Object.prototype, recognised by its own constructor being that realm's Object, and nothing else; cross-realm {} and Object.create(null) stay accepted. verifyRecord read cnf, iat and runtime through the prototype chain while the signature pre-image is built from own members; every read is now own(), and an inherited cnf or iat stands for nothing. Tests cover the one-hop prototype in both realms, and a record from a realm whose Object.prototype carries a non-enumerable iat or cnf, which the schema sees and verification does not. VerificationResult reports profile and the complete acceptedProfiles set, as section 3.3 requires; ACCEPTED_PROFILES is exported. Both runners of the differential emit the two fields and compare.py folds them into the verified equivalence. canonicalJson refused a symbol-keyed member on an object and not on an array; the array branch now refuses it too, with a test. The differential against current main: 1643 of 1644 identical. The ledger entry for iat written as a float or with an exponent is unreached since #404 and leaves the ledger; README, CHANGELOG and the ledger note carry the figures. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> * fix(trust-record-verify): compile the schema validators with ownProperties Review of e4300b7: the signature pre-image is built from a record's own members, while ajv's required and properties tests and its for-in walks also saw inherited ones. cnf, iat and runtime were read with own(); the other required members (subject, model, policy and the rest) were not held to the same rule by the schema. The validators are now generated with ownProperties: true, so required and properties test own properties and every object walk uses Object.keys. A regression test moves each member of a signed record onto a cross-realm Object.prototype, enumerable and not: a required member is schema_invalid, eat_profile is profile_missing, an optional member falls outside the pre-image and is signature_invalid, and cnf.jwk one level down is schema_invalid at cnf. Without the option 11 of those cases fail. Differential against current main: 1643 of 1644 identical, unchanged. Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> --------- Signed-off-by: chernistry <sanderchernitsky@gmail.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Signed-off-by: Alex Chernysh <73943355+chernistry@users.noreply.github.com> Co-authored-by: Imran Siddique <imran.siddique@opaque.co>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security and quality sweep of the reference implementation. No normative spec text, schema or licence file changes.
Signature spelling.
sign._b64url_decodewent throughbase64.urlsafe_b64decode, which takes+and/,=padding and stray characters, and ignores the unused bits of the last character. One signature had several spellings that all verified, and a revocation bundle'sbundle_digestcoverssig.value, so one bundle had several identities, each reportedverified. The decoder now accepts canonical unpadded base64url only, for the Trust Record signature, JWKx, and the provenance, bridge and bundle signatures. Overlap with #401: that PR makes the canonicalsignaturespelling a record-level rule in the schema and spec. This PR changes only the decoder and does not touch its schema pattern or text. Every committed fixture still verifies. Tests:tests/test_signature_spelling_is_canonical.py(20).Duplicate members.
content_marking.verify_assertionandbuild_assertionparsed served record bytes with plainjson.loads, which keeps the last of twosubjectmembers, while a first-wins parser reads the other one out of the same hashed bytes. Both now refuse a duplicate member. That is the only place insrc/that parses untrusted JSON;sign.verify_recordtakes an already-parsed dict.Documented error types. Found by the new fuzz targets, each confirmed with a failing test first. All were already refusals; none was an acceptance.
sign.verify_record:cnf.jwknested 200 levels (about 400 bytes) raisedRecursionErrorfrom jsonschema.validate_jsonanditer_errorsnow report it as aValidationError.intent_bridge.verify_bridge,provenance.verify_record,sign.anchor_bytes: nesting past the stack in a tool call or record member raisedRecursionError.provenance.check_tool_catalog: amaximum: 1.5in an offered tool'sinput_schemaraisedUnanchorableValue, not the documentedProvenanceError. The new error is a subclass of both, sotests/test_safe_integer_range.pyand existing callers still catch it.content_marking: a 4,400 digit URL port raised Python's integer-stringValueError.Tests:
tests/test_untrusted_input_stays_on_the_documented_error.py(15).Scorecard.
.clusterfuzzlite/with six Atheris targets (Trust Record verifier, revocation bundle, provenance, PIC/TRACE bridge with successor observation, content marking, JCS and anchor round trip), seeds fromexamples/,cflite_pr.yml(300 s, code-change) andcflite_batch.yml(nightly, 1800 s). Action SHAs match agent-manifest.runtime-evidence.ymlinstalls fromrequirements/dev.txtand a newrequirements/runtime-evidence.txt(cbor2 6.1.4, inside the verifier's>=5.6,<7) with--require-hashes, thenpip install --no-deps -e ..requirements/dev.txtrecompiled with--universalas its header says. Same pins, but it now carriescoloramafor Windows andtomlifor 3.11, so--require-hashesinstalls on Windows, where it failed on the missingcolorama.Verification. Fresh venv from the locks, Python 3.12 on Windows.
pytest: 2382 passed, 6 skipped, 5 failed. Baseline on main was 2347 passed with the same 5 failures, all intest_generators_reproduce_fixtures.py: the generators write CRLF on Windows, and the output is byte-identical to the fixtures after stripping CR.ruff,mypyandtools/check_dashes.pypass. atheris does not build on this machine, so each target'sTestOneInputran in plain Python over its seeds plus JSON-aware mutations, 65,601 inputs on the final code, with no finding left. The cbor2 wheel is blocked by local application control, soexamples/runtime-evidence/test_appraisal.pydid not run here; the hashed install of both locks did.Generated with Claude Code