Skip to content

fix: apply ECMA regex boundaries in Trust Record validation - #388

Merged
imran-siddique merged 2 commits into
mainfrom
fix/schema-regex-379
Sep 21, 2026
Merged

imran-siddique merged 2 commits into
mainfrom
fix/schema-regex-379

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

What this changes

The Python verifier accepts signed records whose subjects end with LF, CR, U+2028 or U+2029, although the published subject pattern rejects those values under ECMA-262. URI validation also accepts a final LF. This change rejects those records as schema-invalid.

Adapt the four pattern forms shipped in TRACE v0.2 to Python with a strict end anchor, the ECMA line-terminator exclusions for dot, and ASCII digits for \d. Keep the published schema and error paths unchanged. A test requires every shipped pattern to have an explicit adapter; this is not a general ECMA regex translator. The local URI checker rejects literal line terminators while accepting percent-encoded characters.

Closes #379. Thanks to @chernistry for the reproducer.

Type of change

Python verifier bug fix; no normative or schema change.

Spec section

TRACE v0.2 section 3.3, schema validation; JSON Schema 2020-12 section 6.3.3 and ECMA-262 regular expressions.

Validation

  • Initial regressions before the fix: 21 failed, 10 controls passed.
  • After the fix: 302 focused schema, URI and signing tests passed.
  • Independent Node 24 comparison: all 520 results agree across the four published pattern forms. The comparison test skips explicitly if Node is unavailable.
  • Full Windows run: 2,039 passed, 6 skipped, and the same five generator-fixture failures previously reproduced on unchanged ccbd5b9 (conformance, gap-disclosure, canonicalization-boundary, delegation-link, verifier-compatibility).
  • Ruff, mypy (13 source files), repository style check and git diff --check passed.

Tests cover signed records, middle and trailing line terminators, all eight URI fields, digest/signature end anchors, Unicode digit rejection in durations, valid Unicode subjects, schema inventory and isolation from jsonschema's global validators. The Windows environment uses pinned development requirements plus colorama==0.4.6.

The schema's negated SPIFFE authority class still permits line terminators there; a control preserves that published behavior. Tightening that rule would require a separate schema decision. This PR also shares the URI checker with #387, so the two guards must be retained when those branches are combined.

Checklist

  • DCO sign-off on the commit.
  • No normative change requiring a changelog entry or comment period.

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
lywinged
lywinged previously approved these changes Sep 21, 2026

@lywinged lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked at e77f498 against 3ddcbbf, merges clean, 302 tests pass, CI green. All four records
from #379 are rejected here and all four verify on 3ddcbbf.

I tried to make half of this unnecessary, because the four records all carry their terminator at
the end and an end anchor alone looks sufficient. It is not: keep $ to \Z and remove the dot
substitution, and two of the four still verify, because U+000D and U+2028 inside subject are
matched by .+ rather than by the anchor. Your own tests fail 16 on that variant, including
test_shipped_pattern_results_match_javascript. That is the counterfactual run from the other
side, and it passes.

Two things worth a line each, neither blocking.

An unadapted pattern is a bare KeyError out of the validator. _ecma_pattern does
_PYTHON_PATTERNS[pattern] with no fallback, so a schema pattern with no adapter gives
KeyError: "\'^[a-z]+$\'" rather than anything a caller can act on.
test_every_shipped_pattern_has_an_explicit_adapter means CI catches it before it ships, and the
inventory is currently exact. But the runtime shape is an exception where a verdict was promised,
which is the class your own 386 removes in this same batch. One raise RuntimeError(f"no ECMA adapter for schema pattern {pattern!r}") closes it.

The title is wider than the change. revocation.py's _bundle_validator() is a separate
plain Draft202012Validator, and trace-revocation-bundle.json carries ^[A-Za-z0-9_-]+$ at
/properties/sig/properties/value. On this branch a bundle whose sig.value ends in U+000A is
still accepted by that schema; U+000D is not, because the class has no .. One character, one
member, two schemas, and it lands on bundle_signature_invalid a moment later, so the severity is
low. #379 names only trace-claim.json, so the change is inside the issue; it is "ECMA regex
boundaries in TRACE validation" in the title that reaches one place further.

Combining it with 387

The two conflict in src/agentrust_trace/validate.py and in
tests/test_uri_formats_are_enforced.py, which is the pair your description warns about. Five
resolutions, run:

resolution #379 #380 tests
both bodies in one is_uri, raises=ValueError kept fixed fixed 296 pass
keep 388's is_uri, union the test files fixed back 34 fail
keep 387's is_uri, union the test files back, 4 of 4 fixed 35 fail
both def is_uri left in place, second shadowing the first 1 of 4 back fixed 9 fail
both bodies, raises=ValueError dropped fixed escapes as AddressValueError 34 fail

The catch is that those failure counts depend on resolving the test file by union. Resolve
both files the same way, which is what --ours or --theirs does, and the detector goes with the
guard: keeping 388 throughout is 129 passed and 0 failed with #380 restored, and keeping 387
throughout is 167 passed and 0 failed with #379 restored on all four records. Green, with the bug
back. That is the resolution I would guard against, not the shadowing one.

The one I ran and would suggest: a single is_uri with the line-terminator check first, then
base_uri_checker.conforms, then the bracket literal, decorated
@checker.checks("uri", raises=ValueError). Both issues closed, 296 passed across 387's URI
file, 388's URI file and the regex file.

Separately, across the ten pairs from 383, 386, 387, 388 and 390, two others conflict: 383 with
386 in CHANGELOG.md, and 383 with 390 in tests/test_sign.py. Both trivial. 390 merges clean
against 386, 387, 388 and against 389.



What I could not check

  • Windows. Every runs-on: in .github/workflows/ is ubuntu-latest and there is no Windows
    job, so my Linux run and CI are one instrument rather than two. Your own experiment on 383,
    that the same five generator failures reproduce with the production fix removed, is stronger
    than anything I can run here.
  • The published agentrust-trace wheel. Everything above is against repository trees.

Signed-off-by: Imran Siddique <imran.siddique@opaque.co>
@imran-siddique imran-siddique changed the title fix: apply ECMA regex boundaries in TRACE validation fix: apply ECMA regex boundaries in Trust Record validation Sep 21, 2026
@imran-siddique

Copy link
Copy Markdown
Member Author

@lywinged #387 is merged. I resolved #388 against main with one URI checker: line-terminator check first, then the base checker, then the IPv6 literal check, with raises=ValueError retained. Both test sets are preserved.

The combined URI and regex suites pass all 245 tests. Removing the IPv6 guard causes 34 failures, removing the line-terminator guard causes 9, and removing the exception conversion causes 34. Full Windows suite: 2,159 passed, 6 skipped and the same five known generator-fixture failures. Ruff, mypy and style checks pass. Fresh CI is running.

I narrowed the title to Trust Record validation, as you suggested. The unknown-pattern diagnostic remains a separate nonblocking follow-up; this merge does not change that behavior.

Could you confirm this resolution and #383 at 519cea1? On #383, the only manual conflict resolution keeps both changelog entries; the merged signing and revocation tests pass all 219 cases. GitHub dismissed the earlier approvals after these updates. Both PRs will wait for review and passing checks before merging.

@imran-siddique
imran-siddique enabled auto-merge (squash) September 21, 2026 20:35

@lywinged lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked both heads on Linux/Python 3.12.

#388 at f31e974: The single URI checker preserves both guards, their order, and raises=ValueError. Both test sets are intact. All 245 URI and regex tests pass. Removing the IPv6 guard, line-terminator guard, and exception conversion reproduces exactly 34, 9, and 34 failures respectively.

#383 at 519cea1: The only manual conflict resolution is in CHANGELOG.md, and both entries are preserved. The nonce implementation and signing tests are unchanged from 7897d82. All 219 signing and revocation tests pass.

CI was green on both checked heads. No blocking issues found. The unknown-pattern diagnostic can remain a separate follow-up.

@imran-siddique
imran-siddique merged commit 412f02c into main Sep 21, 2026
10 checks passed
@imran-siddique
imran-siddique deleted the fix/schema-regex-379 branch September 21, 2026 20:49
chernistry added a commit to chernistry/trace-spec that referenced this pull request Sep 22, 2026
…er has

agentrust-io#383, agentrust-io#386, agentrust-io#387 and agentrust-io#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 (agentrust-io#382), `subject`
and `appraisal.verifier` under ECMA-262 pattern semantics (agentrust-io#379), the
IPv4-in-IPv6 literal with a leading zero (agentrust-io#380), and the non-ASCII nonce (agentrust-io#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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants