Skip to content

Four surfaces state the rules and nothing says which wins: schema, reference model, normative spec and docs disagree #247

Open

Description

@imran-siddique

TRACE states its rules across four surfaces and nothing says which one wins when they disagree. Four reports in three days found four separate instances, all from producers who did the reasonable thing and validated against one of them.

The four surfaces

Surface Files
Normative spec spec/trace-v0.2.md
JSON Schema schema/trace-claim.json and its byte-identical copy src/agentrust_trace/schema/trace-v0.2.json
Reference model src/agentrust_trace/models.py
Prose docs docs/*.md, plus docs/rfcs/* which disclaim being normative

The four instances

Reported by @chernistry (four of them) and independently in the same window by @mohdibrahimaiml in #241, where the published 0.9.0 schema rejects §3.1.2 references on additionalProperties. Five, if that one is counted, and it is the same shape.

Each has been answered on its own merits. This issue is about the thing underneath them.

Why answering them individually is not enough

Every instance was found by a producer validating their records against one surface and discovering that a different surface disagreed. The schema check passing is not evidence the model will accept the record. The model accepting is not evidence the record is spec-conformant. A reader of the docs gets a fourth answer.

Two consequences worth stating plainly:

  1. Conformance is currently unfalsifiable. "Is this record TRACE-conformant" has no single answer, so a producer cannot be told they are wrong, and a verifier cannot be told it is being too strict.
  2. The failure is asymmetric and silent. The schema is the surface producers vendor and check against, and in three of the four instances it was the loose one. So the checks people actually run are the ones least likely to catch the divergence.

What this issue is for

Deciding the precedence rule and writing it somewhere a producer will find it. Not deciding the four individual answers, which are already ruled.

The obvious candidate: the normative spec wins; the schema and the reference model are derived artifacts and any divergence from the spec is a bug in the derived artifact. That is what the four rulings did in practice, and it is what §3.1.1 versus the schema description implies. But it needs to be true operationally, not just declared:

Not in scope here

The individual rulings, and the release-versus-main gap that #241 sits on. That one is real and separate: a fix on main is not a fix for anyone until it is tagged, which is why #241's reporter hit a schema that is correct in this repository and wrong in the package they installed.

Activity

  1. lywinged commented on Aug 30, 2026

    @lywinged
    Collaborator

    whether it covers the subject case is worth checking first

    Checked. It does not, and the reason is structural rather than an omission, which I think matters for the precedence question.

    MUTANTS in tests/test_the_schema_and_the_models_agree.py holds 24 values, 13 of them strings, and none begins with spiffe:// or did:. The old subject pattern was a prefix test, so every one of those failed it as well as failing the model's shape: both validators rejected, and they agreed by rejecting. Splitting them needs a value that passes the prefix and fails the shape, and the matrix had none, for any field. The sweep ran 672 records and was green, correctly.

    Four such values are added in the PR for #244. Measured there: with the schema reverted, four failures, all on subject; with the model loosened instead, the same four with the direction flipped. No other path moves, so DECLARED_DIVERGENCES gains no row.

    There is also a denominator for one leg of this issue. Resolving the model's aliases, DigestStr and retention's _DURATION_RE, the schema carries ten patterns and the model has a counterpart for each. Nine were already identical strings. subject was the only one that differed, and after the fix that set is empty. That is the schema-against-model leg only; it says nothing about spec-against-schema or docs-against-schema.

    The docs leg is worth a line, because it has a mechanism already. docs/schema.md described subject with the prefix reading, and had been stale since 84df8ef ("security: pre-launch hardening", #49, 2026-06-18) tightened the model and left the schema on the old string.

    tests/test_build_provenance_depth_doc.py parses the build_provenance table out of that same file and checks it against the schema, and tests/test_references_block.py does the same for the references table and for one row of the top-level field table. So that top-level table, the one that went stale, is parsed for exactly one row, on its type and required cells, and never on a description cell. Generalising that to the rest of the table is small, and it is the operational half you are asking for rather than a declaration.

    PR: #248.

  2. imran-siddique commented on Aug 31, 2026

    @imran-siddique
    MemberAuthor

    Narrowing this with a measurement rather than an opinion.

    #248 merged, closing #244, and @lywinged did more than the fix: they audited all ten of the schema's patterns against the model and report that subject was the only differing pair.

    So the schema-versus-model surface, which produced two of this issue's five instances (#244 here and the 100.0 case raised on #206), is now clean rather than merely fixed in one place.

    Two things from that PR belong on this issue:

    The parity test was structurally blind, and that is the reusable lesson. tests/test_the_schema_and_the_models_agree.py already existed and did not catch #244, because subject's schema pattern was a prefix test: every value in the differential's matrix failed both surfaces, so the two validators "agreed by both rejecting". A differential that reports agreement is worth nothing unless its inputs can separate the two implementations. Any future parity work here needs discriminating inputs generated from the looser surface, not a fixed matrix.

    The drift was dated. The two patterns parted at 84df8ef ("security: pre-launch hardening", #49, 2026-06-18), which tightened the model and left the schema on the old string. Ten weeks in which a producer validating against the published schema could emit a record the reference implementation refuses.

    What remains open on this issue, now narrower than when I filed it:

    1. Spec-versus-schema, which is schema: the software-only description states a narrower rule than §3.1.1 mandates #232 and trust-levels: the all-zero measurement convention is stated with two different scopes #240, both with PRs open.
    2. Spec coverage gaps, where no surface states the rule at all, which is test: independent-producer evidence for the parent-record canonicalization boundary #245's parent_record_hash.
    3. The release gap, 0.9.0 schema rejects §3.1.2 references (additionalProperties: false) #241: the schema in agentrust-trace 0.9.0 on PyPI is not the schema in this repository, so a producer can be conformant to the artifact they installed and refused by current main. That one is not fixed by any parity test.
    4. The precedence rule itself, still unwritten: which surface wins when they disagree.

    Item 3 is the one that keeps producing reports, and it is a release rather than a code change.

  3. chernistry commented on Aug 31, 2026

    @chernistry
    Contributor

    Four of the five instances are mine, so the producer's-eye view is the part I can
    add. Short version: I agree the normative spec wins, and I do not think saying so
    changes anything on its own.

    Why precedence alone does not close this. Every one of the four I filed was
    found the same way — validate against one surface, ship, discover another surface
    disagrees. Declaring which surface wins tells a producer who was right after
    the divergence exists. It does not tell them a divergence exists, and that is the
    part that costs the week. The rule is worth writing down, but it is the cheap
    half.

    @lywinged's finding is the reusable one, and it generalises past subject.
    A parity test whose inputs fail both validators proves only that the two agree
    on rejecting garbage. Agreement on a value neither accepts is not evidence of
    agreement. The property a parity matrix has to have is the opposite of the one
    this matrix had: for every constraint, at least one value that passes one
    validator and fails the other
    . If no such value can be constructed for a
    constraint, the two really do agree there and the test can say so; if it can, the
    test either catches the split or it is blind by construction. That reframing
    turns "the mutants happened not to cover subject" into a property the matrix
    can be checked against, which is what stops the next one.

    What I can contribute rather than just propose. Our vectors on #231 are
    emitted by a checked-in generator, never hand-written, which is what makes them
    reproducible. The same generator can emit a disagreement-seeking set: for each of
    the schema's ten patterns, values derived mechanically from the pattern that
    satisfy it and violate the model's shape, and the inverse where the model is the
    looser one. That is exactly the subject case and, I believe, the 100.0 case
    on #206. Say the word and I will send it as a PR against
    tests/test_the_schema_and_the_models_agree.py rather than as a suggestion.

    On #245, agreed and worth stating in the resolution. Precedence cannot
    resolve a surface that says nothing. Promoting the draft would make a document
    that disclaims binding into a binding one, which is the wrong move. If it helps,
    I can post the exact preimage our producer computes for parent_record_hash,
    with the byte layout, as input to the spec text — not as a proposal for what the
    spec should say, but so whoever writes it has one real implementation to check
    the wording against.

    One addition to the "not in scope" line. The release-versus-main gap is not
    only #241's problem; it makes a conformance claim ambiguous in the same way this
    issue's four surfaces do. A producer saying "conformant to TRACE v0.2" today
    cannot be checked unless they also say which tagged artifact they validated
    against, because main and the published package are different rules. Whatever
    precedence text lands, naming the tagged schema as the thing a claim is made
    against would close that with one sentence.

  4. lywinged commented on Aug 31, 2026

    @lywinged
    Collaborator

    @chernistry The word is said. Send the generator PR.

    Three steers so it lands on the first pass:

    1. What it replaces. The current matrix is a hand-written tuple of 28 values (MUTANTS) swept over every path in one base record, with floors of 20 mutants and 25 paths so a sweep that found nothing to mutate cannot pass silently. Your per-constraint property checks the thing those floors cannot, which is discrimination. Keep a floor of the same kind anyway: for each of the ten patterns, either at least one value that splits the two validators, or an explicit assertion that no such value exists for that constraint.

    2. The declaration stays two-sided. Today an undeclared disagreement fails one test and a stale declared entry fails the other (test_the_two_validators_agree_except_where_declared, test_no_declared_divergence_has_quietly_been_resolved). New splits your generator finds should enter DECLARED_DIVERGENCES with a reason string, not a new exemption list. One thing you will meet when you open the file: it already carries one environment-conditional divergence, format: uri on transparency and appraisal.verifier, handled by a computed exemption rather than a declared entry, because jsonschema enforces format only when a checker for it is installed. That pair sits outside your ten patterns, so your set does not need to cover it, but you will want to know why the exemption is computed before you touch the assertions it feeds.

    3. On the tagged artifact sentence: agreed, and it is the version of the precedence rule that is actually checkable. "Conformant to TRACE v0.2" is not a claim anyone can verify while main and the published package disagree. "Conformant to the schema at tag X" is. That sentence belongs in the precedence text.

    For the preimage bytes: yes, post them on #245. One real implementation to check the wording against is exactly what that issue is missing.

  5. chernistry commented on Aug 31, 2026

    @chernistry
    Contributor

    Sent: #270.

    Two things I did not expect to find, both worth reading before the diff:

    • Five of the ten pattern constraints were unreachable by the existing matrix. They sit on optional members BASE omits — model.weights_digest, delegation.parent_record_hash, references[].retention, references[].digest, signature — so nothing ever mutated them. That is the same failure shape as schema: the subject pattern accepts identities the reference model refuses #244 one level up: not a disagreement nobody resolved, a constraint nobody asked about.
    • models.py names this file as the thing that holds its pattern constants mirrored, and this file did not hold them. Two of the ten were pinned in test_references_block.py; the other eight were hand-maintained under that comment.

    On steer 1, I made the floor stronger than "at least one splitting value per pattern", because for a mirrored constraint no such value can exist — demanding one would be demanding something unconstructible. The test asks for the fact that makes it unconstructible (the schema's pattern string is one the model carries) and falls through to requiring a declared splitter wherever that is not true. Three outcomes per constraint, none of them silent.

    On steer 2: DECLARED_DIVERGENCES is reused, no new entries, and the third case gets its own two-sided set rather than an exemption list. I read the computed _URI_FORMAT_ENFORCED exemption before touching the assertions it feeds; format is not pattern, so it stays outside these ten.

    On steer 3: agreed on the tagged-artifact sentence. One thing already true that the issue lists as open — tests/test_validate.py:142 pins the two schema copies against each other, so the by-hand part of that bullet is the published package versus main, not the two files in the repository.

    Preimage bytes for #245 are on that issue: sha256: + hex SHA-256 of the JCS canonical form of the complete signed parent record, signature included, verified across two hops of a committed delegation chain.

  6. chernistry commented on Sep 1, 2026

    @chernistry
    Contributor

    A fifth instance, and it is the first one I have that sits downstream of the spec rather than inside it — which I think bears on the precedence sentence more than the four I filed here.

    This week our own exporter documentation told operators to verify records with --level 1 and called that the level to use in production. Our conformance test had always invoked --level 0. Both had been true-looking for weeks; nobody compared them, because they are different surfaces and nothing said which one a reader should believe. Measured against the four committed vectors, --level 1 fails TR-RTE-001 and TR-RTE-004 on every one — a software-only deployment cannot reach it, since both are properties of the deployment and of the verifier's call rather than of the record. Level 0 passes all four. The prose was the only thing that disagreed, and the prose is what a producer copies.

    The reason I am putting it here rather than filing it: it is the same shape as #232 and #240, but the disagreement had propagated out of the spec and into a consumer's own documentation. A producer reading our page would have published a Level 1 conformance claim in good faith, from a page that was wrong in the same way and for the same reason. That is the cost of the missing precedence sentence measured one hop further out than this issue currently measures it — and it argues that whatever ruling lands should be stated somewhere a downstream project can cite when writing its own docs, not only somewhere the spec's own four surfaces can be ranked against each other.

    Concretely, the two things that made it detectable on our side were the conformance suite pinning a level and the vectors being committed. Both are things TRACE already gives producers. A one-line normative statement of which surface wins would let a producer's own test assert the claim its docs make, which is the loop that closed this for us.

    Three of mine are still open and waiting on you, no rush intended: #234 (schema description vs §3.1.1), #242 (docs/trust-levels.md on the all-zero convention), and #270 (the pattern-constraint generator, approved). #234 and #242 are why #232 and #240 are still open — happy to rebase either if they have gone stale.

  7. imran-siddique commented on Sep 2, 2026

    @imran-siddique
    MemberAuthor

    This issue needs restating. I filed it as "four surfaces disagree and nothing says which wins". Two PRs merged today show the harder problem is that the tests meant to catch the disagreement were not able to detect it, and both were found by contributors auditing coverage rather than by anything failing.

    trace-spec#270 (@chernistry). test_the_schema_and_the_models_agree.py answers "do these two validators disagree about the values I tried", and I read it as answering "are these ten constraints consistent". Five of the ten patterns sit on optional fields absent from BASE, so they were never mutated and never reached their own boundary:

    • model.weights_digest
    • delegation.parent_record_hash
    • references[].retention
    • references[].digest
    • signature

    A sweep cannot disagree about a field it never sets, and a green run says the same thing either way.

    trace-tests#91 (@opento-suggestions). The digest-parity guard test_every_known_site_is_listed asserted len(SCHEMA_SITES) == 6 against another constant in the same module, so it compared the test's beliefs with the test's beliefs. Its docstring claimed a seventh digest field would fail it. @lywinged tested that claim by adding a seventh field to the schema with a drifted pattern and reported the suite stayed green.

    What this changes about this issue

    When I recorded #248's ten-pattern audit as narrowing this issue, I was wrong to treat it as settled. The audit was real and the fix was real; what it could not establish is that the remaining nine patterns are consistent, because five of them were never exercised.

    So the open work here is one item longer than it was:

    1. Spec-versus-schema, schema: the software-only description states a narrower rule than §3.1.1 mandates #232 and trust-levels: the all-zero measurement convention is stated with two different scopes #240, both with PRs.
    2. Spec coverage gaps, test: independent-producer evidence for the parent-record canonicalization boundary #245's parent_record_hash.
    3. The release gap, 0.9.0 schema rejects §3.1.2 references (additionalProperties: false) #241: the schema in the published package is not the schema in this repository.
    4. The precedence rule, still unwritten.
    5. New: any test asserting parity must be shown to discriminate. A parity test whose inputs cannot separate the two surfaces reports agreement and means nothing, and a guard comparing two constants in its own module cannot fail. Both merged today; the standing requirement is that a new drift test arrives with evidence that it fails when the drift is introduced.

    Item 5 is the one that would have prevented items 1 and 2 being found by producers in production rather than by CI.

    A note on how both were found. Neither came from a failing test, because neither test could fail. One came from a contributor asking what their own matrix actually reached; the other from a reviewer deliberately introducing the drift a guard claimed to catch. That is the technique worth generalising here, more than any particular fix.

  8. chernistry commented on Sep 2, 2026

    @chernistry
    Contributor

    Item 5 is the one worth being held to, so: the form that made it checkable on #270, and one leg where it still is not.

    On #270 the evidence was per-commit rather than per-PR. Each commit adds the guard together with a test that fails for the stated reason on its parent and passes on it, so the counterfactual is something a reviewer runs — revert the one commit, watch the new test go red — rather than something the author asserts. @lywinged did it from the other side on both #248 and #270, reintroducing the drift by hand, which is stronger. Either satisfies item 5; a green run and a docstring does not, which is what both of today's cases were.

    Where it is still not true: description text. Two tests parse the field tables in docs/schema.md — test_build_provenance_depth_doc.py and test_references_block.py — and both read the same three cells: name, type, required. Nothing under tests/ compares a description cell against the surface it describes. That is the class both #244 and #232 fall into: the rule stated in prose goes stale or narrow while every parsed cell around it stays correct, so nothing can go red. Ten weeks, for the first one.

    There is one precedent for holding description text, and it is small: test_references_block.py asserts every rel enum value is named in that field's schema description, so adding a value without documenting it fails. What has no counterpart is the reverse direction, and the doc side of either.

    The honest reading of item 5 here is that a text-equality guard is unconstructible. Two hand-maintained prose surfaces cannot be compared mechanically, and one written anyway would report agreement for the same structural reason the old matrix did. The constructible version is to stop maintaining both by hand — derive the doc's field table, description cell included, from the schema — after which there is something to compare. Same shape as the mirrored-constraint case on #270: ask for the fact that makes a splitter impossible, rather than demand a splitter that cannot exist.

    Scope is yours to set. Generating the whole table is a larger change to docs/schema.md than anything I have sent here. A narrower one is a fraction of it and closes the coverage half without touching any prose: every field the schema gives a description has a row, no row describes a field the schema does not carry, and the enum-naming check generalised past rel. Say which and I will send it.

  9. imran-siddique commented on Sep 7, 2026

    @imran-siddique
    MemberAuthor

    The next scope decision is the offered narrow schema/doc field-and-enum coverage guard versus generated field-table prose. Preserve the counterfactual requirement for drift tests. The preimage definition merged in #276. The published references-schema gap in #241 is now verified fixed in the 0.10.0 wheel: the same references-bearing record rejected by 0.9.0 passes its schema. The precedence and remaining coverage questions stay open.

  10. chernistry commented on Sep 7, 2026

    @chernistry
    Contributor

    On the scope decision, I would take the narrow schema/doc field-and-enum coverage guard over generated field-table prose, for one reason that outweighs the convenience of generation: the drift test stays real only under the guard.

    The counterfactual requirement is that introducing drift makes a test fail. Under a coverage guard that holds directly — remove a doc entry for a field or an enum value, the guard reds. Under generated prose it goes vacuous: generated text cannot drift from the source it was generated from, so the test ends up exercising the generator rather than the agreement between two surfaces. You would keep the requirement on paper and lose the thing it was protecting.

    Two qualifications, both of which argue for keeping the guard narrow:

    • Coverage is not precedence. Neither option answers the question in the title. A guard that proves every field and enum value appears in the docs still permits the four surfaces to describe the same field differently. The precedence rule needs its own normative sentence naming which surface wins; the guard cannot substitute for it and should not be scoped as though it could.
    • Narrow keeps it honest. Field-and-enum presence is mechanically decidable. Anything wider — prose agreement, example correctness — is not, and a guard that reaches past what it can decide starts producing failures a contributor cannot act on.

    So: guard for coverage, an explicit sentence for precedence, and generation only if it is ever wanted for readability, never as the drift mechanism.

  11. imran-siddique commented on Sep 11, 2026

    @imran-siddique
    MemberAuthor

    Ruling both, because you are right that one does not answer the other and leaving the second open would make the first unfalsifiable.

    1. Guard scope: narrow field-and-enum coverage. Adopted.

    Your counterfactual is the whole argument and it decides it. Generated prose cannot drift from the source it was generated from, so a drift test over generated text exercises the generator and not the agreement between two surfaces. The requirement survives on paper and the thing it protected is gone. Same shape as a conformance corpus that cannot fail in the direction it exists to test.

    So: every field and every enum value in the schema must appear in the docs, checked directly, and removing a doc entry reds the guard. No generation.

    2. Precedence. Ruled, and it is new normative text.

    There is no precedence statement in the repository today. I checked: the only thing close is one line in docs/verification.md calling §3.3 authoritative, which is a statement about one document rather than a rule.

    The order, from most authoritative to least:

    1. spec/trace-v0.2.md wins on meaning. It is the only surface that defines what a field means and when a verifier must refuse.
    2. schema/trace-claim.json is authoritative for what validates, and for nothing else. A producer may rely on it to know whether a record is well formed. It does not define meaning, and a schema description is not a normative statement.
    3. src/agentrust_trace/models.py is an implementation. It is never authoritative. A divergence there is a bug in the implementation.
    4. docs/*.md is explanatory and never authoritative. docs/rfcs/* already disclaims normativity and stays that way.

    The rule that matters more than the ordering. Where the schema and the spec disagree, the schema is defective, and the resolution is to fix the schema rather than to read the narrower of the two as the rule. This is the case all four of your instances share: #232, #240 and the rest were producers doing the reasonable thing and validating against a surface that under-stated the spec. Telling them they picked the wrong surface is not an answer, because there was no published order for them to consult.

    And the consequence, stated so it is not discovered later. A schema that is narrower than the spec rejects conformant records. That is a defect with a severity, not a documentation nit, and it should be filed as one. A schema broader than the spec accepts non-conformant records, which is worse.

    3. What this means for the guard

    The guard proves coverage. The precedence rule decides conflicts. Neither is a substitute and both land: the guard as a test, the precedence order as a short normative section near the top of spec/trace-v0.2.md, where a producer will actually meet it before they start validating.

    Happy for you to carry the guard. The precedence section I will write, since it is normative text and I answer for it.

  12. chernistry commented on Sep 16, 2026

    @chernistry
    Contributor

    The guard is up as #365, narrow as ruled: every property name and every enum value in schema/trace-claim.json must appear as an inline-code token under docs/ (rfcs excluded) and in docs/schema.md itself, checked directly, nothing generated. The counterfactual is run rather than asserted, in both directions.

    It found two gaps on arrival, both in docs/schema.md and both fixed in the same PR: cnf had no member table, so kty, crv, x, y, n, e and kid appeared only inside a fenced example, and the enforcement_mode row named neither advisory as a value nor declared at all. The precedence section stays yours, as ruled.

  13. 5 remaining items

  14. chernistry commented on Sep 21, 2026

    @chernistry
    Contributor

    Two specification questions from the differential run in #376, filed here rather than as bugs because the reference implementation is not wrong on either: the text leaves them open, and two implementations in two languages cannot agree until it settles them.

    1. Number spelling. §3.2.2 fixes the safe-integer range but not what a number's spelling means. iat written 1785000000.0 is an integer after JSON.parse (JavaScript has one number type) and a float after json.loads; the reference rejects it (record has no valid integer 'iat' for freshness check), a JavaScript verifier accepts it, and both readings are defensible. 1.0e+21 meets the same fork from the other side: a float in Python, which RFC 8785 renders as 1e+21; an integer beyond 2^53−1 in JavaScript, which §3.2.2 requires the verifier to reject. Proposed rule, either half works: (a) an integer-valued member MUST be spelled as a JSON integer, no fraction and no exponent, and a verifier MUST reject any other spelling; or (b) a verifier MUST decide by value after parsing, so 1785000000.0 is the integer 1785000000 everywhere. (a) is the smaller change and the one a language with one number type can implement without a custom parser.

    2. Canonical form of the signature member. §3.1.3 digests the parent record with its signature member present, so the spelling of the signature is inside the chain digest. RFC 4648 §3.5 makes the unused trailing bits of the last character canonical (zero); the schema pattern ^[A-Za-z0-9_-]+$ does not, and the reference decodes leniently. Reproducer on 0.10.0, base record from #379: re-spell the last character of the 86-character signature so its four unused bits are non-zero. The same 64 bytes decode, the record verifies under both spellings, and the two records have different chain digests (sha256:c071070d… against sha256:cf1f58fd…). Proposed rule: the signature member and every base64url JWK member (x) MUST be in RFC 4648 canonical form (no padding, unused bits zero), and a verifier MUST reject a non-canonical spelling before decoding. The chain digest then depends on the signature bytes rather than on one of their spellings. Backward compatibility: a correct base64url encoder already emits the canonical form; only a hand-edited record changes verdict.

    Neither touches the implementation fixes in #379–#382, which stand on the current text.

  15. chernistry commented on Sep 21, 2026

    @chernistry
    Contributor

    A third reading question from the same run, on the check-order contract rather than a rule; it is ledger entry 6 in #376, the one of the thirteen with nowhere else to go.

    A signature member present with a non-string value. Both implementations reject; the reference reports signature_missing for 0, false, [] and {}, and signature_malformed for 1, true, [0] and {"a": 1}, so its verdict on a present non-string is decided by the value's truthiness. The TypeScript reports signature_malformed for all eight. Reproducer on 0.10.0, base record from #379:

    for value in [0, False, [], {}, 1, True, [0], {"a": 1}]:
        rec = sign_record(json.loads(json.dumps(BASE)), KEY); rec["signature"] = value
        verify_record(rec, key_to_jwk(KEY), now=NOW)
    # 0, False, [], {}     -> ValueError: record has no 'signature' field
    # 1, True, [0], {"a":1} -> ValueError: signature must be a base64url string

    §3.2.2 says a record MUST carry a signature, and the schema types the member as a string; neither says whether a present non-string is absent or malformed. Proposed: a present member of the wrong type is malformed, and "missing" names an absent member only. A caller told "missing" for a value that is there goes looking for the wrong defect. Non-normative either way; a one-line change to the presence test in the reference if the reading is accepted.

  16. imran-siddique commented on Sep 21, 2026

    @imran-siddique
    MemberAuthor

    Alex, agreed. “Missing” should mean the signature field is absent. If it is present with the wrong type, report it as malformed. Both cases still reject the record; this makes the error accurate.

  17. imran-siddique commented on Sep 21, 2026

    @imran-siddique
    MemberAuthor

    Alex, I support requiring one canonical encoding. Please open a focused specification proposal with test examples and explain which existing records would stop passing. Keep this separate from the number-spelling question.

  18. imran-siddique commented on Sep 21, 2026

    @imran-siddique
    MemberAuthor

    Alex, I favor deciding by value: a whole number within the existing limits should count as an integer, including decimal or exponent spellings. Please bring this as a separate proposal with Python and JavaScript tests, including boundary cases. JavaScript’s normal parser loses the original spelling, so enforcing a spelling rule would require preserving that information.

  19. chernistry commented on Sep 22, 2026

    @chernistry
    Contributor

    Both proposals will come separately, as you asked. The signature-encoding one on Wednesday 24 September, the number one on Friday 26 September.

    On which existing records would stop passing, since that is the part worth having before the proposal rather than inside it: §3.2.2 already says the embedded signature is base64url with no padding, so the proposal is not a choice of encoding but a rule about the canonical form of the one already named — whether a spelling with non-zero unused trailing bits, which decodes to the same 64 bytes, is a different record. I scanned everything published for a signature that is not already canonical: 216 signatures across this repository's examples/ and the trace-tests corpus at 3af2b53, and 4 in trace-registry, all canonical. So the tightening invalidates nothing that exists today; what it changes is what a conformant verifier must do with a record someone mints tomorrow.

    It is still a change to what verifiers must reject, so I would rather you set the class than assume it. I will bring it with the reproducer in both implementations, the vectors, and the scan above restated so it can be re-run.

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions