Skip to content

fix(verify): enforce key revocation at verification time (#76) - #113

Open
zohebk8s wants to merge 1 commit into
agentrust-io:mainfrom
zohebk8s:fix/76-enforce-revocation-at-verify-time
Open

fix(verify): enforce key revocation at verification time (#76)#113
zohebk8s wants to merge 1 commit into
agentrust-io:mainfrom
zohebk8s:fix/76-enforce-revocation-at-verify-time

Conversation

@zohebk8s

@zohebk8s zohebk8s commented Jul 30, 2026

Copy link
Copy Markdown

What this changes

Closes #76.

verify_record() checked signature and freshness but never consulted revocation or transparency state, so a record signed by a revoked or compromised key kept verifying. This is a gap against the spec's own normative text, not a new requirement: §3.2.1 already says "Verifiers MUST consult current revocation status at verification time".

A signature stays cryptographically valid forever and nothing inside a record can retract the key that made it, so the verifier is the only place withdrawal of trust can be applied.

The revocation store

verify_record(..., revocation=...) accepts either form:

# A revocation list the caller already holds.
verify_record(record, trusted_jwk, revocation={"kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k"})

# Or a live CRL / status endpoint / SCITT lookup.
verify_record(record, trusted_jwk, revocation=lambda key_id: crl.is_revoked(key_id))
Outcome Result
Key listed as revoked Rejected (ValueError)
Store raises (endpoint down, timeout) Rejected; an unavailable source is not evidence a key is unrevoked
Key absent from the store Verification continues
No revocation passed Check skipped; offline verification, unchanged

Failing closed on an unreachable store is the part that matters: if a lookup failure fell through to "verified", an attacker holding a revoked key would only need to make the status endpoint unreachable.

Two design points worth review

The check reads the trusted key, not record["cnf"]["jwk"]. The embedded key is attacker-controlled until the signature verifies, so keying the lookup on it would let a revoked issuer present an unlisted thumbprint and pass. There is a test pinning this.

Identifiers are the RFC 7638 JWK Thumbprint or kid. Thumbprints work when an issuer publishes no kid, and are stable across JWKs differing in optional members. jwk_thumbprint() is exported so a caller can key a list on the same identifier the verifier derives; it is pinned against the RFC 8037 Appendix A.3 known-answer vector.

Scope

Deliberately not in this PR:

Documented rather than left implied: docs/verification.md opens with "independently verifiable offline", which is true and is exactly why non-revocation is the one thing it cannot establish. That page and LIMITATIONS.md now say so directly.

Type of change

  • Editorial (typo, link fix, clarification — no normative effect)
  • Non-breaking spec change (new optional field, new platform profile, informative addition)
  • Breaking spec change (requires 14-day comment period and Project Lead sign-off)
  • Schema change
  • Example addition

None of the listed categories fit, so I have left them unticked rather than claim one inaccurately. This is a library and docs change that implements an existing normative MUST. No spec text, schema, or record field is modified. Happy to tick whichever box you consider correct.

Spec section

3.2.1 (Signing and key management), implemented rather than modified. No spec text changed.

Checklist

  • DCO sign-off on all commits (git commit -s)
  • CHANGELOG.md updated
  • Breaking changes marked with <!-- CHANGED: #NNN — description --> in spec text (not applicable, no breaking change)
  • Backward compatibility statement included: additive keyword argument defaulting to None, so offline verification is byte-for-byte unchanged

Verification

  • 105 tests pass, 14 new. Every new line covered (sign.py misses are pre-existing).
  • ruff check src tests and mypy src/agentrust_trace (strict) clean.
  • Backward compatibility checked against a downstream consumer: the cMCP suite passes unchanged with the patched package installed, 893 passed and 8 skipped, before and after.

…o#76)

Spec 3.2.1 requires that "Verifiers MUST consult current revocation status
at verification time", but verify_record() checked only the signature and
freshness. A record signed by a revoked or compromised key kept verifying,
because a signature stays cryptographically valid forever and nothing inside
a record can retract the key that made it. The verifier is the only place
that withdrawal of trust can be applied.

Add a revocation store to the verify path. It accepts either a container of
revoked key identifiers, for a list the caller already holds, or a callable
performing a live CRL, status-endpoint, or SCITT lookup. A listed key is
rejected, and so is a store that cannot answer: an unavailable revocation
source is not evidence that a key is unrevoked, and treating a lookup
failure as a pass would mean an attacker holding a revoked key only has to
make the endpoint unreachable.

Keys are identified by RFC 7638 JWK Thumbprint or kid. The check reads the
trusted key rather than record["cnf"]["jwk"], which is attacker-controlled
until the signature verifies; keying the lookup on the embedded key would
let a revoked issuer present an unlisted thumbprint and pass.

Also exports jwk_thumbprint(), so a caller can key a revocation list on the
same identifier the verifier derives. Pinned against the RFC 8037 A.3
known-answer vector.

Additive and backward compatible: revocation defaults to None, which leaves
verification purely offline and byte-for-byte unchanged. That mode cannot
prove non-revocation, which LIMITATIONS.md and docs/verification.md now say
plainly rather than leaving implied by "independently verifiable offline".
No normative text, schema, or record field changed.

Verified: 105 tests pass (14 new), ruff and mypy clean, all new lines
covered. Downstream cmcp suite passes unchanged against the patched
package, 893 passed before and after.

Refs agentrust-io#76

Signed-off-by: zohebk8s <zoheb.shaik7@gmail.com>
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential NONE
Overall MEDIUM

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label Jul 30, 2026
@zohebk8s

Copy link
Copy Markdown
Author

Fork CI is green on this exact commit (283383d), both matrix legs:
https://github.com/zohebk8s/trace-spec/actions/runs/30584031843

test (3.11) and test (3.12): lint, type check, and tests all pass.

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

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: enforce revocation/transparency at verify time

2 participants