From 2fbe5feb72df362e6bb04300a99cd72efc96ea50 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Sat, 1 Aug 2026 21:05:17 -0700 Subject: [PATCH] feat(tee): export the SEV-SNP ABI offset table Downstreams kept a ctypes mirror of the SNP layout partly to parse and partly to read offsets off it. cmcp uses it as an offset oracle across seven test files, so sharing parse_snp_report without sharing the table would have moved the duplication into test scaffolding rather than removing it, and offsets that disagree silently are the failure this consolidation exists to prevent. SNP_OFFSETS and SNP_REPORT_LEN are checked against the genuine Azure capture rather than against themselves. Cut as 0.10.0 in the same change: purely additive, no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 ++++++++ python/pyproject.toml | 2 +- python/src/agent_manifest/__init__.py | 4 ++-- python/src/agent_manifest/_snp_verify.py | 19 +++++++++++++++++++ python/tests/test_snp_verify.py | 15 +++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d468fd..d89f696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to Agent Manifest are documented here. Format follows [Keep ## [Unreleased] +## [0.10.0] — 2026-08-01 + +Exports the SEV-SNP ABI offset table so downstreams can delete the ctypes mirrors they kept solely to read offsets from. Completes what 0.9.0 started: cmcp used its struct as an offset oracle across seven test files, so sharing the parse without sharing the table would only have moved the duplication into test scaffolding, where it would drift silently. + +### Added + +**[SDK]** **`SNP_OFFSETS` and `SNP_REPORT_LEN` are public.** The offsets are the contract consumers build and appraise reports against, and they are now checked against the genuine Azure capture rather than against themselves. + ## [0.9.0] — 2026-08-01 Shares the SEV-SNP report union so cmcp and ca2a can delete four copies of the layout between them, and restores a check that existed only in the copies being deleted: the report's declared `sig_algo` is now verified before the signature is checked under it. Phase A2 of consolidating TEE verification into this package. diff --git a/python/pyproject.toml b/python/pyproject.toml index c494871..f66301d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agent-manifest" -version = "0.9.0" +version = "0.10.0" description = "Agent Manifest SDK — cryptographically anchor all 10 artifacts defining an AI agent at deployment" readme = "README.md" requires-python = ">=3.11" diff --git a/python/src/agent_manifest/__init__.py b/python/src/agent_manifest/__init__.py index 17f32b5..bba8530 100644 --- a/python/src/agent_manifest/__init__.py +++ b/python/src/agent_manifest/__init__.py @@ -35,7 +35,7 @@ verify_attestation_chain, ChainVerificationResult, SignatureStatus, ) from ._snp_verify import ( - SIG_ALGO_ECDSA_P384_SHA384, + SIG_ALGO_ECDSA_P384_SHA384, SNP_OFFSETS, SNP_REPORT_LEN, SnpReport, SnpVerificationError, parse_snp_report, parse_hcl_report, load_snp_cert_chain, verify_snp_signature, verify_vcek_chain, verify_runtime_data_binding, @@ -90,7 +90,7 @@ "AttestationReport", "AttestationUnavailableError", "RuntimeAttestationReport", "TPMProvider", "AzureCVMProvider", "SEVSNPProvider", "TDXProvider", "OPAQUEProvider", "verify_attestation_chain", "ChainVerificationResult", "SignatureStatus", - "SIG_ALGO_ECDSA_P384_SHA384", + "SIG_ALGO_ECDSA_P384_SHA384", "SNP_OFFSETS", "SNP_REPORT_LEN", "SnpReport", "SnpVerificationError", "parse_snp_report", "parse_hcl_report", "load_snp_cert_chain", "verify_snp_signature", "verify_vcek_chain", "verify_runtime_data_binding", diff --git a/python/src/agent_manifest/_snp_verify.py b/python/src/agent_manifest/_snp_verify.py index e2ab1a8..c48cac3 100644 --- a/python/src/agent_manifest/_snp_verify.py +++ b/python/src/agent_manifest/_snp_verify.py @@ -64,6 +64,25 @@ # distinguished, which is exactly why it must be checked rather than assumed. SIG_ALGO_ECDSA_P384_SHA384 = 1 +# The ABI offsets, public because they are the contract consumers build and +# appraise reports against. Downstreams previously kept their own ctypes mirror +# of this layout purely to read offsets off it; four copies of one table is four +# chances for them to disagree, so the table is exported instead. +SNP_REPORT_LEN = _SNP_REPORT_LEN +SNP_OFFSETS: dict[str, int] = { + "version": _OFF_VERSION, + "guest_svn": _OFF_GUEST_SVN, + "policy": _OFF_POLICY, + "vmpl": _OFF_VMPL, + "sig_algo": _OFF_SIG_ALGO, + "report_data": _OFF_REPORT_DATA, + "measurement": _OFF_MEASUREMENT, + "host_data": _OFF_HOST_DATA, + "reported_tcb": _OFF_REPORTED_TCB, + "chip_id": _OFF_CHIP_ID, + "signature": _OFF_SIGNATURE, +} + _HCL_MAGIC = b"HCLA" _HCL_SNP_REPORT_OFFSET = 0x20 diff --git a/python/tests/test_snp_verify.py b/python/tests/test_snp_verify.py index 4f1f8da..4c8faa6 100644 --- a/python/tests/test_snp_verify.py +++ b/python/tests/test_snp_verify.py @@ -346,3 +346,18 @@ def test_load_snp_cert_chain_rejects_unparseable_input(): with pytest.raises(SnpVerificationError, match="could not parse"): load_snp_cert_chain(b"-----BEGIN CERTIFICATE-----\nnot a cert\n-----END CERTIFICATE-----\n") + + +def test_public_offsets_match_the_genuine_capture(): + """The exported table is the contract downstreams build reports against, so + it is checked against real silicon rather than against itself.""" + from agent_manifest import SNP_OFFSETS, SNP_REPORT_LEN + + raw = SNP.read_bytes() + rep = parse_snp_report(raw) + + assert SNP_REPORT_LEN == len(rep.raw) == 0x4A0 + assert raw[SNP_OFFSETS["measurement"]:SNP_OFFSETS["measurement"] + 48] == rep.measurement + assert raw[SNP_OFFSETS["report_data"]:SNP_OFFSETS["report_data"] + 64] == rep.report_data + assert raw[SNP_OFFSETS["chip_id"]:SNP_OFFSETS["chip_id"] + 64] == rep.chip_id + assert int.from_bytes(raw[SNP_OFFSETS["sig_algo"]:SNP_OFFSETS["sig_algo"] + 4], "little") == 1