Skip to content

Six verification properties live in sign.py and one in provenance.py, and each has arrived separately #165

Description

@lywinged

Spec section affected

None. This is about src/agentrust_trace/sign.py and src/agentrust_trace/provenance.py, measured at 5367970, and it proposes no normative text — no RFC 2119 keyword changes and nothing that binds an implementation. By GOVERNANCE.md that puts it in the no-sponsor-required set, alongside tooling and conformance tests.

Filed as a proposal rather than a PR because the useful outcome is an answer about direction, not a refactor landing.

Problem

Six verification properties have arrived one at a time, each landing on one side of a two-verifier codebase, and nothing structural stops the next one.

There are two signature verifiers here:

$ git grep -l "\.verify(" -- 'src/agentrust_trace/*.py'
src/agentrust_trace/provenance.py
src/agentrust_trace/sign.py

The registry anchor leaf hashes and verifies no signature; content_marking.verify_assertion verifies none either, by explicit design — "The C2PA manifest signature and the Trust Record signature are separate checks by separate keys, and this function performs neither." Neither is a third verifier.

The two that exist share _canonical_bytes, _pubkey_from_jwk and key_to_jwk. Every policy decision above those is implemented per format, or not implemented:

Property sign.py provenance.py
Key identified by RFC 7638 thumbprint #157 dict equality — #149 open
Revocation at verification time #113 absent — #164 open
Maximum record age pre-existing absent — #164 open
Future-dated record rejected #155 absent — #164 open
Schema enforced during verification #156 absent
Structural rules applied consumer-side #146, after #142

Most of those landed in the last week. #157 is the sharpest instance: it landed

compare_digest(jwk_thumbprint(embedded_jwk), jwk_thumbprint(trusted_jwk))

in sign.py on 2026-08-11, while provenance.py compared the same thing as Python dicts — and jwk_thumbprint was already exported from the package, covered by 16 tests, and the identifier a revocation store is keyed on. Not a knowledge gap. A sharing gap.

The argument #146 settled at function scope is the same one, in its own commit message:

two copies drift, and the copy that matters is the consumer's

That was about a rule enforced in build_record and not in verify_record. This is that sentence one level up, about a property enforced in one format and not the other.

Proposed change

No normative text, so nothing to paste. The shape:

An envelope layer implementing signature verification, key resolution and comparison, revocation, and freshness once, with each format declaring which of those it requires and supplying its own structural rules.

All the signed formats already sign JCS bytes over the record with the signature member removed, so the envelope exists implicitly. Making it explicit means a new format inherits the checks rather than re-deriving them, and a property added later propagates instead of having to be remembered twice.

Costs, which I would rather state than have found:

  • It couples the two formats' release cadences. A change to the envelope is a change to both.
  • content_marking would consume only part of it, since it verifies no signature — so the envelope cannot simply be "the thing every format uses".
  • It touches the two most security-sensitive functions in the library at once, which is the kind of change that wants to land alone rather than alongside anything else.

Backward compatibility

  • Non-breaking (new optional field, informative addition)
  • Breaking (removes or changes required field, changes wire format, changes algorithm set)

Non-breaking as scoped here: no wire format, schema, or conformance level changes, and no record that verifies today stops verifying. The properties provenance.verify_record is missing are being added as opt-in parameters in #164 with defaults chosen so nothing already passing begins to fail; the envelope would move where that code lives, not what it decides.

The one thing to watch is that a shared implementation makes it easier to change both formats at once by accident, which is a review question rather than a compatibility one.

Motivation

No conformant implementation produces or accepts an invalid record because of this, and once #149 and #164 land every individual gap in the table is closed. There is no live defect here.

The cost is that each gap has needed a separate report to notice, and each fix has needed a separate PR. That is a price paid per property rather than once, and it falls on whoever is reading both files closely enough to see that one of them is behind.

If the answer is "yes, but not before v1.0", that is a useful answer and I will stop sending these individually. If the answer is that the two formats should stay independent on purpose, I would like to know that too — it is a defensible position and it would change what I look for.

Related issues or PRs

#149 and #164 close three of the six rows. #146 settled the same argument at function scope after #142. #157, #155, #156 and #113 are the sign.py side of the table.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions