Skip to content

verify_record reports a pass when no revocation check was performed, where §3.2.3 requires it to report that none was performed #246

Description

@imran-siddique

§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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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