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