Skip to content

feat(attestation): appraise SEV-SNP PLATFORM_INFO against an explicit policy - #139

Merged
imran-siddique merged 1 commit into
mainfrom
feat/platform-info-appraisal
Aug 24, 2026
Merged

feat(attestation): appraise SEV-SNP PLATFORM_INFO against an explicit policy#139
imran-siddique merged 1 commit into
mainfrom
feat/platform-info-appraisal

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

What

Closes the Platform state is not appraised gap in LIMITATIONS.md on the SEV-SNP path.

SevSnpReport.platform_info decodes the bitfield at offset 0x40, re-using the shared SNP_OFFSETS table rather than hardcoding the offset. verify_sev_snp_report gains require_platform, forbid_platform and reject_unrecognized_platform_bits, delegating to agent_manifest.appraise_platform_info so the semantics live in one place instead of two.

Why

The four existing checks establish that a report is authentic and which workload it describes. None of them ask what the host was doing while it ran. A report from a machine with SMT enabled and the firmware DRAM alias check never completed verified exactly as cleanly as one from a machine with neither condition. agent-manifest has been able to appraise these fields since 2026-08-20; cA2A simply never called it.

The signature is the point

The direction lives in the argument name, never the field name:

verify_sev_snp_report(
    report, chain, trusted_roots=[ark],
    require_platform={"alias_check_complete", "ecc_enabled"},
    forbid_platform={"smt_enabled"},
)

forbid_platform={"smt_enabled"} demands SMT be off and cannot be misread as demanding it be on. That is the failure mode google/go-sev-guest#195 describes in the reference verifier, whose single SnpPlatformInfo struct is documented as "the maximum of acceptable PLATFORM_INFO data" while enforcing four of its seven fields as minimums.

A rejection raises AttestationFailed carrying the raw platform_info value, so it says which host state caused it.

Compatibility

Opt in, and appraises nothing by default. No existing caller changes behaviour. The attested-peer and sealed-channel paths deliberately do not set a policy: cA2A supplies the mechanism and the vocabulary, the deployment picks the policy. LIMITATIONS.md now says exactly that rather than saying we cannot do it at all.

Tests

Five new cases: decode from the report, the no-policy default still admitting an SMT-enabled host, a missing required bit rejected, a present forbidden bit rejected, and a satisfied policy passing.

545 passed, 2 skipped
ruff check: All checks passed
ruff format --check: 86 files already formatted

The 2 skips are the hardware-capture tests (CA2A_SNP_FIXTURE_DIR, CA2A_TDX_QUOTE), unchanged by this PR.

… policy

The four checks in verify_sev_snp_report establish that a report is authentic
and which workload it describes: signature algorithm, VCEK-to-ARK chain, report
signature, and measurement binding. None of them ask what kind of machine the
report came from, so a report from a host with SMT enabled and the firmware DRAM
alias check never completed verified exactly as cleanly as one from a host with
neither condition. LIMITATIONS.md said so and pointed at agent-manifest, which
has been able to appraise these fields since 2026-08-20. cA2A just never called
it.

SevSnpReport.platform_info decodes the bitfield at offset 0x40, re-using the
shared SNP_OFFSETS table rather than hardcoding it. verify_sev_snp_report takes
require_platform, forbid_platform and reject_unrecognized_platform_bits and
delegates to agent_manifest.appraise_platform_info, so the semantics live in one
place instead of two. A failure raises AttestationFailed carrying the raw
platform_info value, so a rejection says which host state caused it.

The direction lives in the argument name rather than the field name. That is
the whole point of the signature: forbid_platform={"smt_enabled"} demands SMT be
off and cannot be misread as demanding it be on, which is the failure mode
google/go-sev-guest#195 describes in the reference verifier's single-struct
"maximum of acceptable PLATFORM_INFO" policy that enforces four of its seven
fields as minimums.

Appraisal is opt in and appraises nothing when no policy is passed, so existing
callers are unaffected. Five tests cover decode, the no-policy default, a
missing required bit, a present forbidden bit, and a satisfied policy.

545 passed, 2 skipped. ruff and ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1p7BCjLPmLCw4VcBpcF8J
@imran-siddique
imran-siddique requested a review from a team as a code owner August 24, 2026 23:45
@imran-siddique
imran-siddique merged commit 5d6e8ba into main Aug 24, 2026
15 checks passed
@imran-siddique
imran-siddique deleted the feat/platform-info-appraisal branch August 24, 2026 23:49
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.

1 participant