Skip to content

security: require explicit trust roots across delegation verification - #132

Merged
imran-siddique merged 3 commits into
mainfrom
security/offline-verifier-root-trust
Aug 23, 2026
Merged

security: require explicit trust roots across delegation verification#132
imran-siddique merged 3 commits into
mainfrom
security/offline-verifier-root-trust

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Summary

Fixes the trust-root verification gap disclosed in GHSA-95qh-qjph-8622.

  • require callers to supply an explicit trusted root in offline API and CLI verification
  • make the core chain verifier fail closed when no trust root is configured
  • preserve structural and cryptographic diagnostics before enforcing root trust
  • explicitly scope Agent Manifest integration to signature-and-identity verification
  • require agent-manifest >=0.11.1 and release as ca2a-runtime 0.2.1

Evidence

  • 510 passed, 2 skipped
  • 89.74% coverage
  • Ruff lint and format clean
  • strict mypy clean
  • Bandit clean
  • wheel and sdist built successfully
  • Twine package validation passed

Security

This PR is intentionally public only after publication of GHSA-95qh-qjph-8622. The advisory contains impact and affected-version details.

@imran-siddique
imran-siddique requested a review from a team as a code owner August 23, 2026 03:15
@imran-siddique
imran-siddique enabled auto-merge (squash) August 23, 2026 03:15
zohebk8s
zohebk8s previously approved these changes Aug 23, 2026

@zohebk8s zohebk8s left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Requiring an explicit trust set and failing closed on an empty one is the right shape, and the core property holds.

One thing worth changing: the root check now runs after the full signature and structural loop rather than before it. I ran the same three chains through verify_chain on current main and on this branch, each time with a trust set that does not contain the chain root:

                             main                      this branch
well formed chain            UntrustedDelegationRoot   UntrustedDelegationRoot
scope escalation at hop 1    UntrustedDelegationRoot   InvalidCredential
forged signature at hop 1    UntrustedDelegationRoot   InvalidCredential

An untrusted caller can now tell "my forged chain is malformed" apart from "my chain is fine but you do not trust my root", which makes the verifier a debugging oracle for chains it will never accept. Counting verify_signature calls before rejection, for an untrusted root:

hops   main   this branch
1      0      1
4      0      4
8      0      8

Callers who benefit from the precise diagnostics already hold a trusted root, so moving the check back to the top costs them nothing. The only behaviour it changes is for an untrusted root.

Three smaller notes. verify-dag declares --trusted-root-issuer with default=[] while its help says it is required with --chain, and nothing enforces it: verify-chain without the flag is rejected by argparse, but verify-dag --dag ... --chain ... parses and then returns UNTRUSTED_DELEGATION_ROOT, reporting a trust decision for what is a missing argument. And the conformance profile passes trusted_root_issuers={chain[0].issuer}, so it never asserts that an untrusted root is refused, which seems worth pinning there given why this PR exists.

And AgentManifestBinding.verification_scope is only ever set to its default and only read by an assertion in test_agent_manifest_cose.py, so it records the signature-and-identity limit without enforcing it. Is something downstream meant to branch on it?

Suite is green on my checkout, 526 passed and 2 skipped.

Needs a rebase, GitHub reports merge conflicts against main. Also worth noting this cannot install until agent-manifest 0.11.1 is on PyPI, so the release is gated on agentrust-io/agent-manifest#331.

@imran-siddique
imran-siddique force-pushed the security/offline-verifier-root-trust branch from 65bddb6 to 6bd0319 Compare August 23, 2026 18:03
@imran-siddique
imran-siddique merged commit 23c980a into main Aug 23, 2026
24 of 32 checks passed
@imran-siddique
imran-siddique deleted the security/offline-verifier-root-trust branch August 23, 2026 18:46
imran-siddique added a commit that referenced this pull request Aug 25, 2026
…top it dying on cp1252 (#140)

Two bugs, both of which made `demo.py` fail before it reached its own point.

**The trust root was only half-threaded through.** #132 made `verify_chain` fail
closed when no trusted root issuer is supplied, which is right: a self-consistent
chain minted by an attacker passes every signature and attenuation check, so the
pinned anchor is what makes the call authorization rather than structure
checking. That change updated the two CLI invocations at the end of this demo to
pass `--trusted-root-issuer`, and missed all three in-process call sites. The
demo died at step 4 with `UntrustedDelegationRoot`, which reads like the example
is wrong about delegation when the example was simply not passing the root.

`effective_scope` and both `enforce_peer_call` sites now pass
`trusted_root_issuers=[chain[0].issuer]`, with a comment saying why the anchor is
load-bearing rather than ceremony.

**The demo crashed on a default Windows console.** It prints the set-intersection
sign when reporting scope n policy, and cp1252 cannot encode it, so a run on
stock Windows died with a UnicodeEncodeError partway through the checks. stdout
and stderr are reconfigured to UTF-8 rather than downgrading the notation, since
the intersection is the clearest way to write what that step does.

12/12 checks now pass on a default Windows console, including the sealed task,
the binary-swap rejection, and the offline provenance-DAG verification.

The regenerated chain.json and dag.json are deliberately not included: the demo
mints fresh keys each run, so committing them is pure churn.


Claude-Session: https://claude.ai/code/session_01P1p7BCjLPmLCw4VcBpcF8J

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
imran-siddique added a commit that referenced this pull request Aug 25, 2026
* fix(examples): pin the trusted root in the cross-operator demo, and stop it dying on cp1252

Two bugs, both of which made `demo.py` fail before it reached its own point.

**The trust root was only half-threaded through.** #132 made `verify_chain` fail
closed when no trusted root issuer is supplied, which is right: a self-consistent
chain minted by an attacker passes every signature and attenuation check, so the
pinned anchor is what makes the call authorization rather than structure
checking. That change updated the two CLI invocations at the end of this demo to
pass `--trusted-root-issuer`, and missed all three in-process call sites. The
demo died at step 4 with `UntrustedDelegationRoot`, which reads like the example
is wrong about delegation when the example was simply not passing the root.

`effective_scope` and both `enforce_peer_call` sites now pass
`trusted_root_issuers=[chain[0].issuer]`, with a comment saying why the anchor is
load-bearing rather than ceremony.

**The demo crashed on a default Windows console.** It prints the set-intersection
sign when reporting scope n policy, and cp1252 cannot encode it, so a run on
stock Windows died with a UnicodeEncodeError partway through the checks. stdout
and stderr are reconfigured to UTF-8 rather than downgrading the notation, since
the intersection is the clearest way to write what that step does.

12/12 checks now pass on a default Windows console, including the sealed task,
the binary-swap rejection, and the offline provenance-DAG verification.

The regenerated chain.json and dag.json are deliberately not included: the demo
mints fresh keys each run, so committing them is pure churn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1p7BCjLPmLCw4VcBpcF8J

* docs: record SEV-SNP and TDX collection on real silicon, and what the host's PLATFORM_INFO said

The collection table in hardware-validation.md recorded both configfs-TSM
collectors as never run on real silicon: exercised against a simulated configfs
tree and synthetic reports, so code that should work rather than a validated
capability. Both have now been run.

Intel TDX on a GCP c3-standard-4 (kernel 6.17, tdx: Guest detected) produced an
8000-byte DCAP v4 quote whose REPORTDATA matched the derived binding, and
verify_tdx_quote appraised it to the Intel SGX Root CA committed in the fixtures.

AMD SEV-SNP on a GCP n2d-standard-4 pinned to Milan produced a 1184-byte report,
with auxblob carrying the AMD certificate table: VCEK, SEV-Milan, and the
self-signed ARK-Milan. verify_sev_snp_report appraised it to that ARK and failed
closed on a flipped bit in the signed body.

Both quotes were produced by this codebase's own providers and appraised by its
own verifiers, which is the loop the "Not yet validated" entries asked for. The
captures are not committed: a report's 64-byte CHIP_ID is a per-CPU identifier.

The SNP capture was also the first chance to point the PLATFORM_INFO appraisal
added in #139 at a real cloud host. It reported 0x25: SMT enabled, ECC enabled,
DRAM alias check complete. So require_platform={"alias_check_complete"} is
accepted on GCP, and forbid_platform={"smt_enabled"} is rejected there. Neither
is a defect in cA2A or in GCP. The point is that before that appraisal existed
this report verified exactly as cleanly as one from a host with SMT off, because
a signature, a chain and a measurement say which workload ran and never what the
host was doing while it ran.

One reproduction detail worth writing down, because it cost a failed run:
cert_chain.pem must carry ASK then ARK only. The test prepends the VCEK itself,
so including it in the PEM duplicates the leaf and chain verification fails.

545 passed, 2 skipped. The 2 skips are the gated hardware tests, which pass when
pointed at the captures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1p7BCjLPmLCw4VcBpcF8J

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants