diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d347fa3..fb3a010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: run: mypy src/ca2a_runtime/ src/ca2a_verify/ - name: Test - run: pytest tests/unit/ -v --tb=short --cov=src --cov-report=xml + run: pytest tests/unit/ tests/conformance/ -v --tb=short --cov=src --cov-report=xml - name: Upload coverage report uses: codecov/codecov-action@v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a16f5..5c332cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Peer-call enforcement decision core (Tier 2): `ca2a_runtime.policy.LocalPolicy` and `ca2a_runtime.peer` (`effective_scope`, `enforce_peer_call`). Effective permission is the delegated leaf scope intersected with the callee's local policy; a granted call emits a linked provenance record. New error `SCOPE_NOT_PERMITTED`. Claim C3 (scope-policy intersection) is now a validated experiment. Cedar-engine binding of the local policy and live A2A transport wiring remain open. - Sealed peer channel (Tier 2): `ca2a_runtime.channel` (`SealedChannel`, `generate_channel_keypair`, `open_sealed`). HPKE-style X25519 -> HKDF-SHA256 -> ChaCha20-Poly1305 sealing a payload to the peer's attested key; only the peer's private key opens it, and a wrong key or tampered ciphertext fails closed. Claim C4 (sealed-payload confidentiality) is now a validated experiment at the cryptographic layer. The enclave-binding of the private key (a hardware property) and live-path wiring remain open. - Cross-operator attestation (Claim C6) validated in software: a two-operator harness composing the SEV-SNP verifier, measurement pinning, and the sealed channel demonstrates independent keys, mutual attestation, confidential cross-operator delegation, and binary-swap detection. Synthetic report vectors (a genuine report needs SEV-SNP hardware); real hardware end to end remains open. **All six claims (C1-C6) are now validated experiments.** +- cA2A-compatible conformance suite: `tests/conformance/` with a normative README (stable MUST/SHOULD test IDs across delegation, scope-policy, attestation, sealed channel, provenance, and the inbound pipeline) and runnable checks that exercise every MUST-level requirement. Wired into CI and documented at `docs/spec/conformance.md`; ties to the CHARTER trademark language. - Intel TDX attestation backend: `ca2a_runtime.tee.tdx` (DCAP Quote v4 parsing, `TdxProvider`) and `ca2a_verify.tdx.verify_tdx_quote` (PCK chain to a trusted Intel root, QE report signature, attestation-key binding, quote signature, and MRTD/report-data binding), all fail-closed. Chain path validated against the genuine Intel SGX Root CA; multi-level signature path validated with a synthetic self-consistent quote. Quote generation requires a real TDX guest. - Transport-agnostic inbound peer request handler: `ca2a_runtime.peer.handle_peer_request` with `PeerRequest` / `PeerResult`. Composes the full pipeline (verify chain, intersect scope and enforce, open a sealed payload with the enclave key, emit a linked provenance record) fail-closed. A transport parses its wire format into a `PeerRequest`; cA2A does not define the transport (profile, not protocol). - RFC 8785 (JSON Canonicalization Scheme) canonicalization: `ca2a_runtime.canonical.canonicalize`. Credential and provenance bodies are now signed over the JCS encoding (UTF-16 key ordering, JCS string escaping, literal non-ASCII, shortest-decimal integers), so cA2A signatures are cross-verifiable with agent-manifest. ASCII credentials are byte-identical to the previous encoding, so existing signatures still verify. diff --git a/ROADMAP.md b/ROADMAP.md index 0a6a136..8057586 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -39,5 +39,5 @@ Real hardware attestation verification (SEV-SNP VCEK chain, Intel TDX quote via - Stable delegation credential and TRACE link schema with documented versioning guarantees - Full RATS/EAT conformance for peer attestation evidence -- Conformance suite for "cA2A-compatible" claims +- Conformance suite for "cA2A-compatible" claims: **landed** (`tests/conformance/`, normative README + runnable MUST-level checks, in CI). A production run on confidential-computing hardware is the remaining step for a hardware-attested claim. - OWASP liaison on the multi-agent threat mapping; ITI conversation on conformance diff --git a/docs/SPEC.md b/docs/SPEC.md index 5d3c01d..bbd9466 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -27,7 +27,7 @@ cA2A is a profile, not a transport. It does not define how tasks are moved betwe ## Conformance -An implementation may claim "cA2A-compatible" for a given version when it enforces, on an inbound peer call: delegation chain verification (signature, continuity, attenuation, anti-replay), peer attestation against an expected measurement, payload sealing to that measurement, and emission of a linked TRACE record. Conformance tests are on the roadmap for v1.0. +An implementation may claim "cA2A-compatible" for a given version when it enforces, on an inbound peer call: delegation chain verification (signature, continuity, attenuation, anti-replay), peer attestation against an expected measurement, payload sealing to that measurement, and emission of a linked TRACE record. These requirements are defined as a numbered, runnable conformance suite; see [conformance](spec/conformance.md). ## Relationship to sibling specs diff --git a/docs/spec/conformance.md b/docs/spec/conformance.md new file mode 100644 index 0000000..da21eb2 --- /dev/null +++ b/docs/spec/conformance.md @@ -0,0 +1,34 @@ +# Conformance + +An implementation may claim **cA2A-compatible** for a given version when it passes all MUST-level tests in the cA2A conformance suite for that version. This ties directly to the trademark language in [CHARTER.md](../../CHARTER.md): the mark asserts that a deployment satisfies the attestation, attenuation, sealing, and provenance requirements defined here. + +## The normative suite + +The suite is defined in [`tests/conformance/README.md`](https://github.com/agentrust-io/ca2a/blob/main/tests/conformance/README.md). It is a spec document expressed as stable, numbered test IDs grouped by area, each referencing the section it validates. The runnable checks in `tests/conformance/test_profile_conformance.py` exercise every MUST-level requirement against the reference implementation; a third-party implementation is expected to satisfy the same behaviors. + +```bash +pip install -e ".[dev]" +pytest tests/conformance/ -v +``` + +## Requirement groups + +| Group | Covers | Spec | +|---|---|---| +| Delegation (`DELEG-*`) | Signature, attenuation, continuity, depth, anti-replay | [delegation-chain.md](delegation-chain.md) | +| Scope-policy (`POLICY-*`) | Effective scope = delegated ∩ local policy | [cedar-policy.md](cedar-policy.md) | +| Attestation (`ATTEST-*`) | Fail-closed providers, measurement, chain, tamper, MRTD | [attestation.md](attestation.md) | +| Sealed channel (`SEAL-*`) | Seal to attested key, no plaintext, tamper fails closed | [sealed-channel.md](sealed-channel.md) | +| Provenance (`PROV-*`) | DAG integrity, tamper detection, bound to authority | [provenance-dag.md](provenance-dag.md) | +| Inbound pipeline (`PIPE-*`) | The handler grants, records, and fails closed correctly | [call-graph.md](call-graph.md) | + +## Levels + +- **MUST**: required for a cA2A-compatible claim. Partial conformance (MUST only) is sufficient. +- **SHOULD**: recommended; indicates a higher-quality implementation. + +Test IDs are stable: once assigned, an ID is never reused even if the test is removed. This lets a conformance report for one version be compared against another. + +## Scope note + +The attestation requirements are validated against synthetic report and quote vectors plus the genuine AMD and Intel roots, since producing a real report requires confidential-computing hardware. A production conformance run on hardware, and end-to-end validation against a real quote, are the remaining step before a hardware-attested cA2A-compatible claim; see [LIMITATIONS.md](../../LIMITATIONS.md). diff --git a/mkdocs.yml b/mkdocs.yml index ca338c5..6627a94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,148 +1,149 @@ -site_name: cA2A -site_description: Confidential agent-to-agent delegation, attested and attenuated, as a profile on A2A -site_url: https://ca2a.agentrust-io.com -repo_url: https://github.com/agentrust-io/ca2a -repo_name: agentrust-io/ca2a -edit_uri: edit/main/ -docs_dir: . -exclude_docs: | - .github/ - node_modules/ - benchmarks/ - src/ - tests/ - schemas/ - examples/ - LICENSE - NOTICE - ADOPTERS.md - MAINTAINERS.md - SECURITY.md - CHARTER.md - CODE_OF_CONDUCT.md - pyproject.toml - .gitignore - -theme: - name: material - logo: docs/assets/icon.svg - favicon: docs/assets/icon.svg - palette: - - scheme: slate - primary: custom - accent: custom - toggle: - icon: material/brightness-7 - name: Switch to light mode - - scheme: default - primary: custom - accent: custom - toggle: - icon: material/brightness-4 - name: Switch to dark mode - features: - - navigation.instant - - navigation.tracking - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.top - - navigation.path - - search.suggest - - search.highlight - - content.code.copy - - content.tabs.link - - toc.follow - - header.autohide - icon: - repo: fontawesome/brands/github - font: - text: Inter, system-ui, -apple-system, sans-serif - code: JetBrains Mono, Cascadia Code, monospace - -plugins: - - search - - minify: - minify_html: true - - mkdocstrings: - default_handler: python - handlers: - python: - paths: [src] - options: - docstring_style: google - show_source: false - show_root_heading: true - show_root_full_path: false - show_symbol_type_heading: true - show_symbol_type_toc: true - members_order: source - separate_signature: true - show_signature_annotations: true - unwrap_annotated: true - -markdown_extensions: - - admonition - - pymdownx.details - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.tabbed: - alternate_style: true - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - - attr_list - - md_in_html - - tables - - toc: - permalink: true - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/agentrust-io/ca2a - generator: false - -extra_css: - - docs/stylesheets/extra.css - -nav: - - Home: README.md - - Quick Start: docs/quickstart.md - - How It Works: docs/concepts.md - - Configuration: docs/configuration.md - - Tutorials: - - Verify a delegation chain: docs/tutorials/verify-a-delegation-chain.md - - Authoring a delegation credential: docs/tutorials/authoring-a-delegation-credential.md - - Emit and verify provenance: docs/tutorials/emit-and-verify-provenance.md - - Reproducing the claims: docs/tutorials/reproducing-the-claims.md - - Integrating with A2A: docs/tutorials/integrating-with-a2a.md - - Specification: - - Overview: docs/SPEC.md - - A2A Profile: docs/spec/profile.md - - Transport Binding: docs/spec/transport.md - - Component Model: docs/spec/component-model.md - - Inbound Peer-Call Decision: docs/spec/call-graph.md - - Delegation Chain: docs/spec/delegation-chain.md - - Provenance DAG: docs/spec/provenance-dag.md - - Sealed Peer Channel: docs/spec/sealed-channel.md - - Attestation: docs/spec/attestation.md - - Scope-Policy Intersection: docs/spec/cedar-policy.md - - TRACE A2A Profile: docs/spec/trace-a2a-profile.md - - Verification Library: docs/spec/verification-library.md - - Error Codes: docs/spec/error-codes.md - - Failure Modes: docs/spec/failure-modes.md - - Threat Model: docs/spec/threat-model.md - - Project: - - Limitations: LIMITATIONS.md - - Changelog: CHANGELOG.md - - Contributing: CONTRIBUTING.md - - Governance: GOVERNANCE.md - - Roadmap: ROADMAP.md +site_name: cA2A +site_description: Confidential agent-to-agent delegation, attested and attenuated, as a profile on A2A +site_url: https://ca2a.agentrust-io.com +repo_url: https://github.com/agentrust-io/ca2a +repo_name: agentrust-io/ca2a +edit_uri: edit/main/ +docs_dir: . +exclude_docs: | + .github/ + node_modules/ + benchmarks/ + src/ + tests/ + schemas/ + examples/ + LICENSE + NOTICE + ADOPTERS.md + MAINTAINERS.md + SECURITY.md + CHARTER.md + CODE_OF_CONDUCT.md + pyproject.toml + .gitignore + +theme: + name: material + logo: docs/assets/icon.svg + favicon: docs/assets/icon.svg + palette: + - scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Switch to light mode + - scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Switch to dark mode + features: + - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.top + - navigation.path + - search.suggest + - search.highlight + - content.code.copy + - content.tabs.link + - toc.follow + - header.autohide + icon: + repo: fontawesome/brands/github + font: + text: Inter, system-ui, -apple-system, sans-serif + code: JetBrains Mono, Cascadia Code, monospace + +plugins: + - search + - minify: + minify_html: true + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [src] + options: + docstring_style: google + show_source: false + show_root_heading: true + show_root_full_path: false + show_symbol_type_heading: true + show_symbol_type_toc: true + members_order: source + separate_signature: true + show_signature_annotations: true + unwrap_annotated: true + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - attr_list + - md_in_html + - tables + - toc: + permalink: true + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/agentrust-io/ca2a + generator: false + +extra_css: + - docs/stylesheets/extra.css + +nav: + - Home: README.md + - Quick Start: docs/quickstart.md + - How It Works: docs/concepts.md + - Configuration: docs/configuration.md + - Tutorials: + - Verify a delegation chain: docs/tutorials/verify-a-delegation-chain.md + - Authoring a delegation credential: docs/tutorials/authoring-a-delegation-credential.md + - Emit and verify provenance: docs/tutorials/emit-and-verify-provenance.md + - Reproducing the claims: docs/tutorials/reproducing-the-claims.md + - Integrating with A2A: docs/tutorials/integrating-with-a2a.md + - Specification: + - Overview: docs/SPEC.md + - A2A Profile: docs/spec/profile.md + - Transport Binding: docs/spec/transport.md + - Component Model: docs/spec/component-model.md + - Inbound Peer-Call Decision: docs/spec/call-graph.md + - Delegation Chain: docs/spec/delegation-chain.md + - Provenance DAG: docs/spec/provenance-dag.md + - Sealed Peer Channel: docs/spec/sealed-channel.md + - Attestation: docs/spec/attestation.md + - Scope-Policy Intersection: docs/spec/cedar-policy.md + - TRACE A2A Profile: docs/spec/trace-a2a-profile.md + - Verification Library: docs/spec/verification-library.md + - Conformance: docs/spec/conformance.md + - Error Codes: docs/spec/error-codes.md + - Failure Modes: docs/spec/failure-modes.md + - Threat Model: docs/spec/threat-model.md + - Project: + - Limitations: LIMITATIONS.md + - Changelog: CHANGELOG.md + - Contributing: CONTRIBUTING.md + - Governance: GOVERNANCE.md + - Roadmap: ROADMAP.md diff --git a/tests/conformance/README.md b/tests/conformance/README.md new file mode 100644 index 0000000..77d98b2 --- /dev/null +++ b/tests/conformance/README.md @@ -0,0 +1,89 @@ +# cA2A Conformance Suite + +## Overview + +A conforming cA2A implementation passes all MUST-level tests in this suite. +Implementations SHOULD also pass all SHOULD-level tests. Passing the MUST-level +tests for a given version is the bar for claiming **cA2A-compatible** for that +version, per the trademark language in [CHARTER.md](../../CHARTER.md). + +Each test references the spec section it validates. Test IDs are stable: once +assigned, an ID is never reused even if the test is removed. This document is the +normative definition of what a conforming implementation must do; the runnable +checks in `test_profile_conformance.py` exercise these behaviors against the +reference implementation, and a third-party implementation is expected to satisfy +the same behaviors. + +Run the suite: + +```bash +pip install -e ".[dev]" +pytest tests/conformance/ -v +``` + +--- + +## Group 1: Delegation + +Spec: [delegation-chain.md](../../docs/spec/delegation-chain.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| DELEG-001 | MUST | An unsigned or signature-tampered credential is rejected. | `INVALID_CREDENTIAL`. | +| DELEG-002 | MUST | A child scope that is not a subset of its parent is rejected. | `SCOPE_ESCALATION`. | +| DELEG-003 | MUST | A hop whose parent link or issuer breaks continuity, or whose depth is not previous + 1, is rejected. | `BROKEN_DELEGATION_LINK`. | +| DELEG-004 | MUST | A chain deeper than the configured maximum is rejected. | `DELEGATION_DEPTH_EXCEEDED`. | +| DELEG-005 | MUST | A `credential_id` that repeats within a chain is rejected. | `CREDENTIAL_REPLAY`. | +| DELEG-006 | MUST | A well-formed, strictly narrowing chain is accepted. | Verification succeeds. | + +## Group 2: Scope-policy intersection + +Spec: [cedar-policy.md](../../docs/spec/cedar-policy.md), [call-graph.md](../../docs/spec/call-graph.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| POLICY-001 | MUST | The effective scope is the delegated leaf scope intersected with the local policy, never wider than either. | Effective set equals the intersection. | +| POLICY-002 | MUST | A capability delegated but not locally allowed is denied. | `SCOPE_NOT_PERMITTED`. | +| POLICY-003 | MUST | A capability locally allowed but not delegated is denied. | `SCOPE_NOT_PERMITTED`. | + +## Group 3: Attestation + +Spec: [attestation.md](../../docs/spec/attestation.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| ATTEST-001 | MUST | Hardware providers without a backend are never auto-selected. | `detect()` returns False; generating a report fails closed with `ATTESTATION_UNSUPPORTED`. | +| ATTEST-002 | MUST | An attestation report whose measurement differs from the expected value is rejected. | `ATTESTATION_FAILED`. | +| ATTEST-003 | MUST | A report whose certificate chain does not reach a trusted root is rejected. | `ATTESTATION_FAILED`. | +| ATTEST-004 | MUST | A report with a tampered body or signature is rejected. | `ATTESTATION_FAILED`. | +| ATTEST-005 | MUST | A TDX quote whose MRTD differs from the expected value is rejected. | `ATTESTATION_FAILED`. | + +## Group 4: Sealed channel + +Spec: [sealed-channel.md](../../docs/spec/sealed-channel.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| SEAL-001 | MUST | A payload sealed to a peer's attested key opens only with that peer's private key. | Peer recovers the payload; any other key fails. | +| SEAL-002 | MUST | The sealed blob does not contain the plaintext. | Plaintext bytes absent from the sealed output. | +| SEAL-003 | MUST | A tampered sealed payload fails closed rather than returning plaintext. | `SEALED_CHANNEL_ERROR`. | + +## Group 5: Provenance + +Spec: [provenance-dag.md](../../docs/spec/provenance-dag.md), [trace-a2a-profile.md](../../docs/spec/trace-a2a-profile.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| PROV-001 | MUST | A well-formed linked-record DAG verifies. | Verification succeeds. | +| PROV-002 | MUST | A tampered or reparented record is detected. | `PROVENANCE_LINK_BROKEN`. | +| PROV-003 | MUST | Provenance is bound to authority: record i must match credential i. | Mismatch raises `PROVENANCE_LINK_BROKEN`. | + +## Group 6: Inbound pipeline + +Spec: [call-graph.md](../../docs/spec/call-graph.md) + +| ID | Level | Requirement | Expected outcome | +|---|---|---|---| +| PIPE-001 | MUST | The handler grants only a capability in the effective scope and emits a linked provenance record. | Accepted call returns a verifiable record. | +| PIPE-002 | MUST | A sealed payload with no enclave key available fails closed. | `SEALED_CHANNEL_ERROR`; no payload returned. | +| PIPE-003 | MUST | An invalid delegation chain is rejected before any authorization or payload step. | The chain error is raised; no payload returned. | diff --git a/tests/conformance/conftest.py b/tests/conformance/conftest.py new file mode 100644 index 0000000..7479446 --- /dev/null +++ b/tests/conformance/conftest.py @@ -0,0 +1,10 @@ +"""Conformance suite: ensure this branch's src/ resolves before any installed package.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_src = str(Path(__file__).parent.parent.parent / "src") +if _src not in sys.path: + sys.path.insert(0, _src) diff --git a/tests/conformance/test_profile_conformance.py b/tests/conformance/test_profile_conformance.py new file mode 100644 index 0000000..7d8ce76 --- /dev/null +++ b/tests/conformance/test_profile_conformance.py @@ -0,0 +1,234 @@ +"""Runnable cA2A conformance checks. Each test maps to a MUST-level ID in +README.md and exercises the reference implementation. A third-party +implementation is expected to satisfy the same behaviors. +""" + +from __future__ import annotations + +import pytest +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + +from ca2a_runtime.channel import SealedChannel, generate_channel_keypair, open_sealed +from ca2a_runtime.delegation import DelegationCredential, new_keypair, verify_chain +from ca2a_runtime.errors import ( + AttestationFailed, + AttestationUnsupported, + BrokenDelegationLink, + CredentialReplay, + DelegationDepthExceeded, + InvalidCredential, + ProvenanceLinkBroken, + ScopeEscalation, + ScopeNotPermitted, + SealedChannelError, +) +from ca2a_runtime.peer import PeerRequest, effective_scope, handle_peer_request +from ca2a_runtime.policy import LocalPolicy +from ca2a_runtime.provenance import cross_check_chain, record_for, verify_dag +from ca2a_runtime.tee.sev_snp import SevSnpProvider +from ca2a_runtime.tee.tdx import TdxProvider +from ca2a_verify.sev_snp import verify_sev_snp_report +from ca2a_verify.tdx import verify_tdx_quote +from tests.unit.conftest import build_chain, make_ec_cert, make_sev_snp_report +from tests.unit.test_tdx import build_quote + + +def _narrowing(): + return build_chain([frozenset({"read", "write", "admin"}), frozenset({"read", "write"})]) + + +def _deep3(): + return build_chain([frozenset({"a", "b", "c"}), frozenset({"a", "b"}), frozenset({"a"})]) + + +def _records(chain): + recs, ph = [], None + for i, cred in enumerate(chain): + rec = record_for(cred, record_id=f"r{i}", parent_record_hash=ph) + recs.append(rec) + ph = rec.record_hash() + return recs + + +# --- Group 1: Delegation --- + +def test_deleg_001_signature() -> None: + _, pub = new_keypair() + _, sub = new_keypair() + with pytest.raises(InvalidCredential): + DelegationCredential("c0", pub, sub, frozenset({"a"}), 0).verify_signature() + + +def test_deleg_002_attenuation() -> None: + with pytest.raises(ScopeEscalation): + verify_chain(build_chain([frozenset({"a"}), frozenset({"a", "b"})])) + + +def test_deleg_003_continuity() -> None: + rp, rpub = new_keypair() + mp, mpub = new_keypair() + _, leaf = new_keypair() + root = DelegationCredential("c0", rpub, mpub, frozenset({"a"}), 0).sign(rp) + child = DelegationCredential("c1", mpub, leaf, frozenset({"a"}), 1, parent_id="wrong").sign(mp) + with pytest.raises(BrokenDelegationLink): + verify_chain([root, child]) + + +def test_deleg_004_depth() -> None: + with pytest.raises(DelegationDepthExceeded): + verify_chain(_deep3(), max_depth=1) # leaf is depth 2 > 1 + + +def test_deleg_005_replay() -> None: + chain = build_chain([frozenset({"a"}), frozenset({"a"})]) + dup = DelegationCredential(chain[0].credential_id, chain[0].issuer, chain[0].subject, + chain[0].scope, chain[0].depth, chain[0].parent_id, chain[0].signature) + with pytest.raises(CredentialReplay): + verify_chain([chain[0], dup]) + + +def test_deleg_006_valid_chain_accepted() -> None: + verify_chain(_narrowing()) + + +# --- Group 2: Scope-policy intersection --- + +def test_policy_001_intersection() -> None: + assert effective_scope(_narrowing(), LocalPolicy.of(["read", "audit"])) == frozenset({"read"}) + + +def test_policy_002_delegated_not_allowed_denied() -> None: + req = PeerRequest(chain=_narrowing(), requested_capability="write", record_id="r0") + with pytest.raises(ScopeNotPermitted): + handle_peer_request(req, policy=LocalPolicy.of(["read"])) + + +def test_policy_003_allowed_not_delegated_denied() -> None: + req = PeerRequest(chain=_narrowing(), requested_capability="audit", record_id="r0") + with pytest.raises(ScopeNotPermitted): + handle_peer_request(req, policy=LocalPolicy.of(["read", "audit"])) + + +# --- Group 3: Attestation --- + +def test_attest_001_providers_fail_closed() -> None: + assert SevSnpProvider.detect() is False + assert TdxProvider.detect() is False + with pytest.raises(AttestationUnsupported): + SevSnpProvider().attest("deadbeef", "n") + with pytest.raises(AttestationUnsupported): + TdxProvider().attest("deadbeef", "n") + + +def _sev_setup(): + root_key = ec.generate_private_key(ec.SECP384R1()) + root = make_ec_cert("root", "root", root_key, root_key) + vcek_key = ec.generate_private_key(ec.SECP384R1()) + vcek = make_ec_cert("vcek", "root", vcek_key, root_key) + report = make_sev_snp_report(vcek_key, measurement=b"\x11" * 48, report_data=b"\x22" * 64) + return report, [vcek, root], root + + +def test_attest_002_wrong_measurement() -> None: + report, chain, root = _sev_setup() + with pytest.raises(AttestationFailed): + verify_sev_snp_report(report, chain, trusted_roots=[root], expected_measurement=b"\x99" * 48) + + +def test_attest_003_untrusted_root() -> None: + report, chain, _ = _sev_setup() + stranger = make_ec_cert("s", "s", ec.generate_private_key(ec.SECP384R1()), + ec.generate_private_key(ec.SECP384R1())) + with pytest.raises(AttestationFailed): + verify_sev_snp_report(report, chain, trusted_roots=[stranger]) + + +def test_attest_004_tampered_report() -> None: + report, chain, root = _sev_setup() + bad = bytearray(report) + bad[0x90] ^= 0xFF + with pytest.raises(AttestationFailed): + verify_sev_snp_report(bytes(bad), chain, trusted_roots=[root]) + + +def test_attest_005_tdx_wrong_mrtd() -> None: + root_key = ec.generate_private_key(ec.SECP256R1()) + quote, root = build_quote(b"\x11" * 48, b"\x22" * 64, root_key=root_key) + with pytest.raises(AttestationFailed): + verify_tdx_quote(quote, trusted_roots=[root], expected_mrtd=b"\x99" * 48) + + +# --- Group 4: Sealed channel --- + +def test_seal_001_only_peer_key_opens() -> None: + priv, pub = generate_channel_keypair() + sealed = SealedChannel(pub).seal(b"secret") + assert open_sealed(sealed, priv) == b"secret" + with pytest.raises(SealedChannelError): + open_sealed(sealed, X25519PrivateKey.generate()) + + +def test_seal_002_no_plaintext_in_blob() -> None: + _, pub = generate_channel_keypair() + assert b"secret" not in SealedChannel(pub).seal(b"secret") + + +def test_seal_003_tamper_fails_closed() -> None: + priv, pub = generate_channel_keypair() + sealed = bytearray(SealedChannel(pub).seal(b"secret")) + sealed[-1] ^= 0xFF + with pytest.raises(SealedChannelError): + open_sealed(bytes(sealed), priv) + + +# --- Group 5: Provenance --- + +def test_prov_001_dag_verifies() -> None: + recs = _records(_deep3()) + assert verify_dag(recs) == recs + + +def test_prov_002_tamper_detected() -> None: + chain = _deep3() + recs = _records(chain) + from ca2a_runtime.provenance import DelegationRecord + # Tamper the middle record: the leaf's parent link no longer matches. + recs[1] = DelegationRecord(recs[1].record_id, recs[1].credential_id, recs[1].subject, + frozenset({"a", "injected"}), recs[1].parent_record_hash) + with pytest.raises(ProvenanceLinkBroken): + verify_dag(recs) + + +def test_prov_003_bound_to_authority() -> None: + chain = _deep3() + recs = _records(chain) + cross_check_chain(recs, chain) # aligned: passes + from ca2a_runtime.provenance import DelegationRecord + recs[0] = DelegationRecord(recs[0].record_id, "WRONG", recs[0].subject, recs[0].scope, None) + with pytest.raises(ProvenanceLinkBroken): + cross_check_chain(recs, chain) + + +# --- Group 6: Inbound pipeline --- + +def test_pipe_001_grants_and_records() -> None: + req = PeerRequest(chain=_narrowing(), requested_capability="read", record_id="r0") + result = handle_peer_request(req, policy=LocalPolicy.of(["read", "audit"])) + assert result.granted_capability == "read" + assert verify_dag([result.record]) == [result.record] + + +def test_pipe_002_sealed_without_key_fails_closed() -> None: + _, pub = generate_channel_keypair() + req = PeerRequest(chain=_narrowing(), requested_capability="read", record_id="r0", + sealed_payload=SealedChannel(pub).seal(b"x")) + with pytest.raises(SealedChannelError): + handle_peer_request(req, policy=LocalPolicy.of(["read"])) + + +def test_pipe_003_invalid_chain_rejected_first() -> None: + bad = build_chain([frozenset({"read"}), frozenset({"read", "write"})]) + req = PeerRequest(chain=bad, requested_capability="read", record_id="r0") + with pytest.raises(ScopeEscalation): + handle_peer_request(req, policy=LocalPolicy.of(["read", "write"]))