feat(delegation): add credential validity windows (not_before/not_after) - #110
Conversation
imran-siddique
left a comment
There was a problem hiding this comment.
Please rebase after #107 and trim its residual-risk sentence from 'a credential carries no validity window and there is no revocation path' to the remaining revocation gap, as already agreed on #107. Also validate the public �t_time argument as a non-negative integer excluding bool, with tests for bool/float/negative inputs. Credential bounds are deliberately strict JSON integers; accepting True, 1.5, or -1 as the verifier's evaluation time undermines that deterministic boundary for library callers even though argparse protects the CLI.
Optional not_before / not_after fields (Unix epoch seconds, inclusive) on DelegationCredential, enforced per hop by verify_chain at a caller-supplied at_time defaulting to the current time, and threaded through ca2a_verify.verify_delegation_chain, verify_chain_file, and the verify-chain / verify-dag CLI as --at-time. An absent bound is omitted from the signed body rather than encoded as null, so every previously signed credential keeps its exact signed bytes; a present bound is signed, so it cannot be stripped without failing verification. The a2a-sdk bridge restores the bounds' integer-ness across the protobuf Struct round trip exactly as it already did for depth. New error codes CREDENTIAL_EXPIRED and CREDENTIAL_NOT_YET_VALID. Conformance DELEG-007..009 and ACTION-012/013 cover the expired and not-yet-valid cases from the agentrust-io#36 residual checklist. Refs agentrust-io#36 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Murtaza Munaim <manizzle.msf@gmail.com>
…idity windows Review follow-up to agentrust-io#110: - verify_chain now rejects a non-integer at_time (bool, float, negative) with ValueError before any credential is examined. The bounds on the wire are strict JSON integers; the evaluation time they are compared against holds the same line for library callers. The CLI was already argparse-typed. - The residual-risk entry agentrust-io#107 added to the threat model now reflects that a credential can carry a validity window: the remaining gap is revocation inside a still-valid window, as agreed on agentrust-io#107. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Murtaza Munaim <manizzle.msf@gmail.com>
20fe95d to
8d6ba7f
Compare
|
Both addressed in
Full suite on the rebased branch: 489 passed, 3 skipped; ruff, ruff format, mypy, bandit all clean. |
imran-siddique
left a comment
There was a problem hiding this comment.
The prior rebase, threat-model, and at_time findings are addressed. One blocking direct-construction bypass remains in the new validity fields.
from_dict enforces non-negative integers excluding bool, but DelegationCredential is also a public dataclass constructor and both sign() and verify_chain() trust its in-memory bounds. I reproduced signed credentials with not_before=True and with not_before=-5; both signatures verified and verify_chain(..., at_time=1) accepted them. That creates signed wire bodies outside the documented format and causes cross-implementation divergence (a serialized credential produced by this API is rejected by this implementation's own from_dict).
Please centralize credential model validation and invoke it before signing and before/within verification, or add equivalent __post_init__ enforcement. Add direct-construction tests for bool, negative, float/string/null-equivalent misuse on both bounds, plus the inverted case, proving invalid objects cannot be signed or accepted. The existing from_dict tests should remain.
Focused reproduction on head 8d6ba7f:
DelegationCredential(..., not_before=True, not_after=2).sign(key) followed by verify_chain([cred], at_time=1) -> ACCEPTED
DelegationCredential(..., not_before=-5, not_after=2).sign(key) followed by verify_chain([cred], at_time=1) -> ACCEPTED
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… the wire Review follow-up to agentrust-io#110, round 2. DelegationCredential is a public constructor as well as a wire format, and sign() would put a bound outside the documented format into a signed body that this implementation's own from_dict rejects. __post_init__ now enforces the same non-negative-integer rule and window ordering from_dict applies, so an invalid credential cannot be constructed, signed, or verified; replace() re-runs it, so sign() is covered. The now-unreachable inverted-window check in verify_chain is removed. Direct-construction tests cover bool, negative, float, and string misuse on both bounds, plus the inverted window; the from_dict tests are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Murtaza Munaim <manizzle.msf@gmail.com>
|
Good catch — reproduced both of your examples on Direct-construction tests cover Full suite: 497 passed, 3 skipped; ruff, format, mypy, bandit clean. |
imran-siddique
left a comment
There was a problem hiding this comment.
The rebase and at_time validation are addressed, and the direct-construction bypass is closed in post_init with tests for bool, negative, float/string misuse, null-equivalent bounds, and inverted windows. Focused verification: 52 unit tests passed, 1 skipped; Ruff and mypy pass. Approving pending hosted conformance CI.
What
Adds an optional validity window to
DelegationCredential:not_before/not_after(Unix epoch seconds, inclusive), enforced per hop byverify_chainat a caller-suppliedat_timedefaulting to the current time. New error codesCREDENTIAL_EXPIREDandCREDENTIAL_NOT_YET_VALID; conformance casesDELEG-007–DELEG-009andACTION-012/ACTION-013.Why
The #36 residual checklist asks for expired and not-yet-valid action-evidence cases, blocked on "define credential validity-window fields before adding expired and not-yet-valid cases.
DelegationCredentialcurrently has no time bounds." This PR defines those fields and adds the cases.Design choices, so they can be reviewed as decisions rather than accidents:
at_time=Nonemeans "now", not "skip". Every existing call site (runtime authorization, CLI, offline verifier) starts enforcing windows immediately, fail-closed. An auditor replaying recorded evidence passes the decision time viaat_time/--at-time, because a window that has lapsed by audit time says nothing about validity at decision time.Structround trip exactly as it already does fordepth(the bounds are part of the signed body when present, so this is load-bearing; round-trip tested against the real SDK).Refs #36 (checklist item: validity-window fields + expired / not-yet-valid cases).
On the checklist's first item (exercise the public
agent-manifestdelegation verifier):agent_manifest.DelegationHopis a different wire format fromDelegationCredential(manifest-bound pre-image, indexed hops,scope_grantobjects), so a cA2A chain cannot be fed to it. The conformance suite already routes through the publicca2a_verify.verify_delegation_chain; that acceptance criterion likely wants revising to name that API, per the alternative the checklist itself offers. Happy to do that as a follow-up if the maintainers agree.Security impact
Touches
src/ca2a_runtime/delegation/andsrc/ca2a_verify/(two-approval paths).at_timeparameter exists so offline audit replays at decision time. A skewed verifier clock can reject valid chains (availability), not accept expired ones beyond the skew itself.sign()now usesdataclasses.replaceso a future model field cannot be silently dropped from signed credentials.Test plan
pytestpasses (487 passed, 3 skipped; 20 new tests)ruff checkandruff format --checkpassmypypassesbanditpassesca2a verify-chain --chain <windowed chain> --at-timeinside/before/after the window returns verified /CREDENTIAL_NOT_YET_VALID/CREDENTIAL_EXPIREDDCO sign-off
Developer Certificate of Origin (https://developercertificate.org).
🤖 Generated with Claude Code