feat(provenance): make a refusal evidence, and demo it - #63
Merged
Conversation
Removes ca2a's last duplicated attestation crypto. ca2a_verify.verify_cert_chain (shared by the SEV-SNP VCEK, TDX PCK, and TPM AK chains) now delegates to agent_manifest.verify_cert_chain, the org's shared generic algorithm-agnostic verifier, re-raising CertChainError as AttestationFailed to preserve ca2a's contract. ca2a keeps its own parsers, per-format signature checks, appraisal shapes, trust-root pinning, and verify_offer semantics. Dep: adds agent-manifest>=0.5. 201 tests pass unchanged; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdware TdxQuote.parse threw on every genuine DCAP v4 quote. Real quotes nest the Quoting Enclave material: the bytes after the attestation key are a certification-data header of type 6 (QE_REPORT_CERTIFICATION_DATA) wrapping the QE report, its PCK signature, the auth data and the type-5 PCK chain. The parser read the QE report directly after the attestation key, six bytes early, so a real GCP C3 quote failed with a bogus cert-length read. The synthetic fixture emitted the same flat layout, so the tests validated the defect. Failure was closed, so this is a false negative rather than an unsound accept, but the TDX path had never worked against real evidence. Both verifiers now run against genuine hardware evidence, env-gated because the captures embed per-CPU identifiers: a real Azure CVM SEV-SNP report (CA2A_SNP_FIXTURE_DIR) and a real GCP C3 TDX quote (CA2A_TDX_QUOTE), each also asserted to fail closed on a tampered copy. docs/hardware-validation.md records what is now verified and, as importantly, what still is not: this validates the verifier, not a running attested peer. The live handshake is still software mode, so cA2A is still not attested across trust domains, and LIMITATIONS and ROADMAP say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enforce_peer_call raised on an over-scoped call and emitted nothing, so a denial left no artifact: an auditor walking the DAG saw a gap where a hop should be, and the only account of why the call stopped was the operator's log. It now builds a linked denial record carrying the requested capability, the effective scope it fell outside, and the reason, attached to ScopeNotPermitted.record. The call still fails closed; the record is evidence of the refusal, not a way to continue. verify_dag treats a denial as terminal so no trail can claim work proceeded past it, cross_check_chain matches only hop records positionally while still requiring a denial to name a credential that is in the chain, and ca2a verify-dag surfaces outcome: denied. Denial fields are omitted from an allow record's hashed body, so existing allow-record hashes are unchanged. examples/rejection-with-proof is the front door for it: an agent asks for authority nobody delegated, is refused, and the refusal verifies offline through the shipped CLI against the committed chain and DAG. The callee's own policy permits the capability, so the refusal turns on delegation rather than on the callee not supporting it. The demo runs in CI so the README cannot promise something the code stopped doing, and it states plainly that it makes no attestation claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cred` was already bound as DelegationCredential by the positional loop above, so reassigning the Optional from by_id.get shadowed it with an incompatible type. Renamed to `under`, which also reads better: the credential the refusal was issued under. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
enforce_peer_callraised on an over-scoped call and emitted nothing. A denial left no artifact, so an auditor walking the DAG saw a gap where a hop should be, and the only account of why the call stopped was the operator's log. For a project whose pitch is "a third party can check this without trusting you", the refusal was the one thing you had to take on trust.What changed
A refusal now emits a linked provenance record carrying the requested capability, the effective scope it fell outside, and the reason, attached to
ScopeNotPermitted.record. The call still fails closed; the record is evidence, not a way to continue.verify_dagtreats a denial as terminal: nothing may be chained onto a refused hop, so a trail cannot claim work proceeded.cross_check_chainmatches only hop records positionally against the chain, and still requires a denial record to name a credential that is actually in it, so a refusal cannot be attributed to authority never granted.ca2a verify-dagreportsoutcome: deniedwith the requested capability and effective scope, instead of printingverified: trueand burying the interesting part.The demo
examples/rejection-with-proof/. An agent asks for authority nobody delegated to it, is refused, and the refusal verifies offline through the shipped CLI against the committed chain and DAG.{"verified": true, "records": 4, "outcome": "denied", "requested_capability": "tool:purchase", "effective_scope": ["tool:search"], "cross_checked": true}The callee's local policy permits
tool:purchaseon purpose. It is refused anyway because the delegated scope does not carry it, which is the distinction transport-layer auth cannot express. A demo where the callee simply did not support purchasing would prove nothing.The demo runs in CI (
tests/unit/test_example_rejection_with_proof.py) so the README cannot promise something the code stopped doing, and both the README and the module docstring state that it makes no attestation claim: no sealing, no peer attestation, nothing about where the code ran.Test plan
pytest tests/— 205 passed, 2 skipped; 6 pre-existingtest_cedar.pyfailures unrelated to this change (they fail identically on a clean tree and pass in CI)ruff checkclean🤖 Generated with Claude Code