§3.2.3 requires a verifier to report absence of revocation evidence as absence. verify_record() promotes it to a pass. The same question gets two answers in one specification, and the code path is the permissive one.
Raised from the APS integration side in agentrust-io/integrations#140 by @aeoess, who noticed it as a divergence between APS and TRACE. It is not: it is a divergence between TRACE and TRACE.
What the spec says
docs/verification.md, on the bundle path, quoting §3.2.3:
a verifier with no bundle "MUST report that it performed no revocation check"
and an expired bundle "MUST report the record as unverified for revocation rather than as verified".
That is the right rule and the reasoning is stated well: offline is a state you report, not a check you skip.
What the code does
At 25013c59d607860a4a4d2608476280286f688243, src/agentrust_trace/sign.py:
if revocation is not None:
_check_not_revoked(trusted_jwk, revocation)
- No store passed at all (
revocation=None): the check is skipped and the record verifies. Nothing in the result records that no revocation check was performed.
- An empty store passed:
identifier in revocation is False, so the key reads as not revoked, and the record verifies. An empty container and a container that has affirmatively been checked are indistinguishable in the result.
Neither outcome reports "no revocation check performed". Both report a pass.
Why this matters more than it looks
The store path is not a lesser surface. docs/verification.md states it is §3.2.3's own fallback for records with no usable inclusion entry ID:
That fallback is what the current verify_record() store implements, and it is the correct behaviour for deployments carrying no receipts.
So the deployments most likely to pass no store, or an empty one, are precisely the ones the fallback exists for. The bundle path states the absence rule and the fallback path silently ignores it.
The failure is quiet by construction. A relying party wires up revocation, misconfigures the store so it is empty, and every record verifies exactly as it would with a correctly populated one. There is no observable difference between "checked against a real list" and "checked against nothing".
What is not wrong
The fail-closed behaviour that exists is good, and #236 improved it today: a store that raises is a rejection, and as of #236 a callable returning a non-bool is too, because truthiness is unrelated to revocation status. Both are the right direction. This issue is about the third case, which is not an error at all: the store answers, honestly, that it knows nothing.
Suggested direction, not a decision
The result needs somewhere to say what was checked, in the same shape §3.2.3 already requires of the bundle path. Something like a revocation-status field distinguishing:
checked against a store that was present and non-empty
not-performed where no store was supplied
indeterminate where a store was supplied but carries no usable evidence
Raising on an empty store would be wrong: an empty revocation list is a legitimate state, and the record is not less trustworthy for it. What is wrong is that the verifier cannot tell anyone which of the three happened.
This interacts with #226 and with the appraisal-status work in trace-tests, since a verifier that reports none for an appraisal it did not perform is the same discipline applied one field over.
§3.2.3 requires a verifier to report absence of revocation evidence as absence.
verify_record()promotes it to a pass. The same question gets two answers in one specification, and the code path is the permissive one.Raised from the APS integration side in agentrust-io/integrations#140 by @aeoess, who noticed it as a divergence between APS and TRACE. It is not: it is a divergence between TRACE and TRACE.
What the spec says
docs/verification.md, on the bundle path, quoting §3.2.3:and an expired bundle "MUST report the record as unverified for revocation rather than as verified".
That is the right rule and the reasoning is stated well: offline is a state you report, not a check you skip.
What the code does
At
25013c59d607860a4a4d2608476280286f688243,src/agentrust_trace/sign.py:revocation=None): the check is skipped and the record verifies. Nothing in the result records that no revocation check was performed.identifier in revocationisFalse, so the key reads as not revoked, and the record verifies. An empty container and a container that has affirmatively been checked are indistinguishable in the result.Neither outcome reports "no revocation check performed". Both report a pass.
Why this matters more than it looks
The store path is not a lesser surface.
docs/verification.mdstates it is §3.2.3's own fallback for records with no usable inclusion entry ID:So the deployments most likely to pass no store, or an empty one, are precisely the ones the fallback exists for. The bundle path states the absence rule and the fallback path silently ignores it.
The failure is quiet by construction. A relying party wires up revocation, misconfigures the store so it is empty, and every record verifies exactly as it would with a correctly populated one. There is no observable difference between "checked against a real list" and "checked against nothing".
What is not wrong
The fail-closed behaviour that exists is good, and #236 improved it today: a store that raises is a rejection, and as of #236 a callable returning a non-bool is too, because truthiness is unrelated to revocation status. Both are the right direction. This issue is about the third case, which is not an error at all: the store answers, honestly, that it knows nothing.
Suggested direction, not a decision
The result needs somewhere to say what was checked, in the same shape §3.2.3 already requires of the bundle path. Something like a revocation-status field distinguishing:
checkedagainst a store that was present and non-emptynot-performedwhere no store was suppliedindeterminatewhere a store was supplied but carries no usable evidenceRaising on an empty store would be wrong: an empty revocation list is a legitimate state, and the record is not less trustworthy for it. What is wrong is that the verifier cannot tell anyone which of the three happened.
This interacts with #226 and with the appraisal-status work in trace-tests, since a verifier that reports
nonefor an appraisal it did not perform is the same discipline applied one field over.