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 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.
Spec section affected
None. This is about
src/agentrust_trace/sign.pyandsrc/agentrust_trace/provenance.py, measured at5367970, and it proposes no normative text — no RFC 2119 keyword changes and nothing that binds an implementation. ByGOVERNANCE.mdthat 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:
The registry anchor leaf hashes and verifies no signature;
content_marking.verify_assertionverifies 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_jwkandkey_to_jwk. Every policy decision above those is implemented per format, or not implemented:sign.pyprovenance.pyMost of those landed in the last week. #157 is the sharpest instance: it landed
in
sign.pyon 2026-08-11, whileprovenance.pycompared the same thing as Python dicts — andjwk_thumbprintwas 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:
That was about a rule enforced in
build_recordand not inverify_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:
content_markingwould consume only part of it, since it verifies no signature — so the envelope cannot simply be "the thing every format uses".Backward compatibility
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_recordis 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.pyside of the table.