Skip to content

feat(cli): let ca2a start demand caller attestation from config - #161

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
Susanpdl:feat/start-caller-attestation
Sep 8, 2026
Merged

feat(cli): let ca2a start demand caller attestation from config#161
imran-siddique merged 1 commit into
agentrust-io:mainfrom
Susanpdl:feat/start-caller-attestation

Conversation

@Susanpdl

@Susanpdl Susanpdl commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What

ca2a start can now turn on mutual attestation. The attestation block of the config accepts require_caller_attestation (none | any | hardware), caller_verifier (platform plus a trusted_roots_path PEM bundle), and challenge_ttl_seconds, and bootstrap.build_peer_node passes them to the PeerNode arguments that already existed.

attestation:
  provider: tpm
  require_caller_attestation: hardware
  caller_verifier:
    platform: tpm
    trusted_roots_path: vtpm-roots.pem
  challenge_ttl_seconds: 60

Why

Closes #160. The knobs landed on PeerNode in #89 and #90 but were never threaded through the config, so a callee run from the CLI could not appraise its callers and could not be told to. LIMITATIONS calls mutual attestation "off by default"; through ca2a start it was off with no switch. This is a gap in #52, which I wrote.

Everything fails closed:

  • an unknown rung is a CONFIG_ERROR
  • hardware with no caller_verifier is a CONFIG_ERROR at load, naming the field to add (PeerNode refuses this too, but the config layer can say which line)
  • a missing or empty roots file is a CONFIG_ERROR
  • caller_verifier.platform: sev-snp or tdx is refused at startup with the reason. verify_sev_snp_report and verify_tdx_quote exist but take raw evidence plus a certificate chain, and the SNP collector drops the auxblob rather than guess at its format, so neither can be built from a report and a roots file yet. Refusing is better than a config that looks like it appraises and does not. Only tpm can be built today, via ca2a_verify.tpm.tpm_verifier.

Two small CLI changes came with it. The startup line prints the rung alongside the provider, and a node with no verifier is told that hardware offers will be refused as unappraisable (at every rung, per the spec), so the first hardware-attested caller does not look like a bug. ConfigError.detail is now printed on the start path; before, the part of the message that says what to change was dropped.

require_holder_proof is deliberately not exposed. It is on for every node ca2a start builds; turning it off is a downgrade with no attestation rationale.

Security impact

Positive: a config-driven callee can now demand caller attestation, which it previously could not. No change to the wire format, the record hash, or what PeerNode accepts; the new config paths only add refusals. The SNP and TDX report-level verifiers are follow-up work and are named as such in the error.

Test plan

  • pytest passes (69 in the touched modules, full suite 614 passed, 2 skipped; the one local failure was a venv issue on my machine, test_docs_quickstart passes with the path set)
  • ruff check and ruff format --check pass
  • mypy passes
  • bandit clean
  • Manual: ca2a start with require_caller_attestation: any and no verifier prints the rung and the unappraisable note and binds; with hardware and no verifier exits 1 naming attestation.caller_verifier; with platform: sev-snp exits 1 with the VCEK reason; with platform: tpm and the fixture root binds with require_caller_attestation=hardware on the startup line

New tests: config validation for each field and each rejection; build_caller_verifier builds a real TPM verifier that fails closed on a bare report, refuses missing and empty roots, refuses sev-snp/tdx with a detail; build_peer_node carries the knobs; a node built from a config with require_caller_attestation: any refuses a caller offering nothing (ATTESTATION_FAILED) and accepts one attesting with the software provider (caller_attestation: software-only) on a live HTTP call; CLI tests for the startup line, the two refusals, and that the detail reaches stderr.

DCO sign-off

PeerNode has carried require_caller_attestation, caller_verifier and
challenge_ttl_seconds since the mutual-attestation work, but none of them
were reachable from a config file, so a callee run through ca2a start could
never appraise its callers.

The attestation block now accepts all three. caller_verifier names a
platform and a PEM roots file; only tpm can be built today (via
ca2a_verify.tpm.tpm_verifier), and sev-snp or tdx fail at startup with the
reason rather than appraising nothing. A hardware rung with no verifier is a
CONFIG_ERROR naming the field. The startup line prints the rung, a node with
no verifier is told hardware offers will be refused as unappraisable, and
ConfigError.detail is now printed on the start path so the reason reaches
the operator.

Closes agentrust-io#160

Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
@Susanpdl
Susanpdl requested review from a team, carloshvp and zohebk8s as code owners September 8, 2026 19:27
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 94.00000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ca2a_runtime/bootstrap.py 86.95% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving. The gap is real and you found it in work you wrote yourself, which is the part I would keep doing: the knobs landed on PeerNode in #89 and #90, LIMITATIONS said mutual attestation was off by default, and through ca2a start it was off with no switch at all. Those are different statements and only one of them was true.

What makes this safe to take is the shape of the refusals rather than the plumbing.

The default stays REQUIRE_NONE, so nothing changes for an existing config and the LIMITATIONS wording remains accurate. An unrecognised require_caller_attestation raises ConfigError at load rather than falling through to the permissive value, which is the failure mode that matters here: a typo in a security knob must not read as "off".

The caller_verifier split is the better decision in this PR. The config vocabulary knows tpm, sev-snp and tdx, and only tpm can actually be built, so naming either of the others is a startup error carrying the specific reason: the SNP collector drops the auxblob rather than guess at its format, and TDX has no AttestationReport-level wrapper. The alternative, accepting the name and appraising nothing, produces a callee that looks like it verifies its callers and does not, and nothing downstream could tell the difference. Refusing at startup with the reason attached is the same principle as provider selection failing closed rather than downgrading to software-only, and it keeps an unavailable capability from reading as a satisfied one.

Green on all twelve real checks across the six matrix rows, vouch-gate included. The red gate in the rollup is a stale entry with a later success beside it. Tests in test_config.py, test_bootstrap.py and test_cli_start.py cover the three layers separately.

Closes #160.

@imran-siddique
imran-siddique merged commit 04170fb into agentrust-io:main Sep 8, 2026
14 of 15 checks passed
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.

ca2a start cannot turn on mutual attestation or a caller verifier

3 participants