From f5d82d9681d793eb6d6d70c16f0184fec4653f6b Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Tue, 21 Jul 2026 16:47:39 -0700 Subject: [PATCH] refactor(verify): delegate shared attestation crypto to agent-manifest 0.5 Removes cMCP's duplicated cryptographic verification in favor of the shared agent-manifest library (single source of truth across the org, consumed via PyPI). SNP report-signature -> agent_manifest.verify_snp_signature; VCEK and PCK certificate-chain verification -> agent_manifest.verify_cert_chain (the generic, algorithm-agnostic verifier). cMCP keeps its DCAP quote parser, *VerificationResult shapes, TR semantics, and all binding checks; behavior is unchanged. TPM stays local (Phase-1 parse-only; no AK sig/chain to share). Dep: agent-manifest>=0.5. 814 tests pass unchanged; ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++ pyproject.toml | 2 +- src/cmcp_verify/sev_snp.py | 82 ++++++++++---------------------------- src/cmcp_verify/tdx.py | 28 ++++--------- 4 files changed, 35 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a047c7..9a30bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Attestation crypto now delegates to agent-manifest's shared verification library (`agent-manifest>=0.5`) instead of cMCP's own copies: the SEV-SNP report signature (`verify_snp_signature`) and the VCEK/PCK certificate-chain verification (the generic `verify_cert_chain`) are shared across the org rather than duplicated. cMCP keeps its own DCAP quote parser, `*VerificationResult` shapes, TR-field semantics, and report_data/qualifying-data bindings; behavior is unchanged (all tests pass unchanged). cMCP's TPM verifier stays local (Phase-1 parse-only: no AK signature/chain to share). + ### Added - Azure confidential-VM attestation (`cmcp_runtime.tee.azure_cvm.AzureCVMProvider` + `cmcp_verify.azure_cvm`), **hardware-validated on live Azure SEV-SNP silicon**. Azure runs SNP behind a Hyper-V paravisor with no `/dev/sev-guest`; the SNP report is read from the vTPM NV index `0x01400001` and the guest cannot control `REPORT_DATA` (the paravisor binds the vTPM AK there). cMCP's nonce (`jwk_thumbprint || audit-root`) is therefore committed into an AK-signed TPM quote's qualifying data, with the AK rooted in silicon via the SNP report (`REPORT_DATA == sha256(runtime_data)`) and the VCEK→ASK→ARK chain (reusing `cmcp_verify.sev_snp`). Auto-detected first (before TPM/SEV-SNP) since Azure exposes no `/dev/sev-guest`. Carries its own `runtime.platform` value `azure-cvm-sev-snp` (requires `agentrust-trace>=0.4`) so a consumer keying on `runtime.platform` knows the root of trust is vTPM-rooted, not a guest-controlled SNP `report_data`. diff --git a/pyproject.toml b/pyproject.toml index d9098d5..b5f5f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ requires-python = ">=3.11" dependencies = [ "agentrust-trace>=0.4", - "agent-manifest>=0.1.1", + "agent-manifest>=0.5", "cryptography>=42.0", "pyyaml>=6.0", "httpx>=0.27", diff --git a/src/cmcp_verify/sev_snp.py b/src/cmcp_verify/sev_snp.py index 1ee3b07..240a4dc 100644 --- a/src/cmcp_verify/sev_snp.py +++ b/src/cmcp_verify/sev_snp.py @@ -20,8 +20,7 @@ from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import ec, rsa -from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature -from cryptography.hazmat.primitives.hashes import SHA384 +from cryptography.hazmat.primitives.serialization import Encoding # The SNP report is signed over its leading bytes; the 512-byte signature field # occupies the tail. sizeof(report) == 0x4A0, signature == 0x200, so the signed @@ -92,27 +91,18 @@ class SNPVerificationResult: details: dict[str, str] = field(default_factory=dict) -def _snp_report_signature_der(raw_report: bytes) -> bytes: - """Extract the ECDSA-P384 signature from an SNP report and DER-encode it. - - R and S are stored as little-endian 72-byte fields; P-384 uses 48 bytes each. - """ - sig_field = raw_report[_SNP_SIG_OFFSET : _SNP_SIG_OFFSET + 2 * _SNP_SIG_COMPONENT_LEN] - r_le = sig_field[:_SNP_SIG_COMPONENT_LEN] - s_le = sig_field[_SNP_SIG_COMPONENT_LEN : 2 * _SNP_SIG_COMPONENT_LEN] - r = int.from_bytes(r_le[:_P384_COMPONENT_LEN], "little") - s = int.from_bytes(s_le[:_P384_COMPONENT_LEN], "little") - return encode_dss_signature(r, s) - - def verify_snp_report_signature( raw_report: bytes, vcek_cert: x509.Certificate ) -> tuple[bool, str | None]: """Verify the SNP report is signed by the VCEK (ECDSA P-384 / SHA-384). - Returns (True, None) on a valid signature, (False, reason) otherwise. Fails - closed on any parse or verification error. + The cryptographic check is delegated to agent-manifest's shared verifier + (`agent_manifest.verify_snp_signature`) so the org shares one implementation; + the format pre-checks below keep cmcp's specific failure reasons. Returns + (True, None) on a valid signature, (False, reason) otherwise. Fails closed. """ + from agent_manifest import parse_snp_report, verify_snp_signature + if len(raw_report) < _SNP_SIG_OFFSET + 2 * _SNP_SIG_COMPONENT_LEN: return False, "report too short to contain a signature" try: @@ -126,44 +116,15 @@ def verify_snp_report_signature( if not isinstance(pub, ec.EllipticCurvePublicKey) or pub.curve.name != "secp384r1": return False, "VCEK public key is not EC P-384" - signed_region = raw_report[:_SNP_SIGNED_LEN] try: - der_sig = _snp_report_signature_der(raw_report) - pub.verify(der_sig, signed_region, ec.ECDSA(SHA384())) - except Exception: # noqa: BLE001 (InvalidSignature and malformed r/s both fail closed) + ok = verify_snp_signature(parse_snp_report(raw_report), vcek_cert.public_bytes(Encoding.DER)) + except Exception: # noqa: BLE001 (fail closed on any parse/verify error) + return False, "SNP report signature does not verify against the VCEK" + if not ok: return False, "SNP report signature does not verify against the VCEK" return True, None -def _cert_signed_by(child: x509.Certificate, issuer: x509.Certificate) -> bool: - """True iff `child` carries a valid signature from `issuer`'s public key. - - Handles both AMD's RSA-PSS cert signatures (ARK/ASK) and EC signatures by - reading the signature parameters off the certificate, so we do not hand-roll - fragile PSS parameters. - """ - issuer_pub = issuer.public_key() - try: - if isinstance(issuer_pub, rsa.RSAPublicKey): - issuer_pub.verify( - child.signature, - child.tbs_certificate_bytes, - child.signature_algorithm_parameters, - child.signature_hash_algorithm, - ) - elif isinstance(issuer_pub, ec.EllipticCurvePublicKey): - issuer_pub.verify( - child.signature, - child.tbs_certificate_bytes, - ec.ECDSA(child.signature_hash_algorithm), - ) - else: - return False - return True - except Exception: # noqa: BLE001 - return False - - def load_snp_cert_chain( pem_bundle: bytes, ) -> tuple[x509.Certificate, x509.Certificate, x509.Certificate]: @@ -193,17 +154,18 @@ def verify_vcek_chain( ) -> tuple[bool, str | None]: """Verify VCEK -> ASK -> ARK, with ARK pinned to a caller-trusted AMD root. - Returns (True, None) or (False, reason). Fails closed. + Returns (True, None) or (False, reason). Fails closed. Delegates to + agent-manifest's generic, algorithm-agnostic chain verifier (shared across + the org) which honors each certificate's own signature algorithm and pins + the root by fingerprint. """ - if ark.fingerprint(SHA384()) != trusted_ark.fingerprint(SHA384()): - return False, "chain ARK does not match the pinned trusted AMD ARK" - if not _cert_signed_by(ark, ark): - return False, "ARK is not validly self-signed" - if not _cert_signed_by(ask, ark): - return False, "ASK is not signed by the ARK" - if not _cert_signed_by(vcek, ask): - return False, "VCEK is not signed by the ASK" - return True, None + from agent_manifest import verify_cert_chain + + try: + verify_cert_chain([vcek, ask, ark], [trusted_ark]) + return True, None + except Exception as exc: # noqa: BLE001 (CertChainError + any parse error → fail closed) + return False, str(exc) def verify_sev_snp_measurement( diff --git a/src/cmcp_verify/tdx.py b/src/cmcp_verify/tdx.py index 6ecbc26..d251001 100644 --- a/src/cmcp_verify/tdx.py +++ b/src/cmcp_verify/tdx.py @@ -234,19 +234,6 @@ def _verify_p256(pub: ec.EllipticCurvePublicKey, sig64: bytes, data: bytes) -> b return False -def _cert_signed_by(child: x509.Certificate, issuer: x509.Certificate) -> bool: - # PCK chain certs are ECDSA P-256. - pub = issuer.public_key() - if not isinstance(pub, ec.EllipticCurvePublicKey): - return False - try: - pub.verify(child.signature, child.tbs_certificate_bytes, - ec.ECDSA(child.signature_hash_algorithm)) - return True - except (InvalidSignature, ValueError): - return False - - @dataclass class _ParsedQuote: signed_region: bytes @@ -350,13 +337,14 @@ def fail(reason: str) -> TDXVerificationResult: root = x509.load_pem_x509_certificate(trusted_intel_root_pem) except ValueError as exc: return fail(f"intel_root_parse_error: {exc}") - if not _cert_signed_by(root, root): - return fail("intel_root_not_self_signed") - # Verify each cert is signed by the next in the chain, with the last PCK cert - # signed by the pinned root (which was checked self-signed above). - for child, issuer in zip(certs, [*certs[1:], root], strict=True): - if not _cert_signed_by(child, issuer): - return fail("pck_chain_invalid") + # PCK chain (leaf..intermediates) up to the pinned Intel root, via the shared + # generic cert-chain verifier (agent-manifest) instead of a local copy. + from agent_manifest import verify_cert_chain + + try: + verify_cert_chain([*certs, root], [root]) + except Exception: # noqa: BLE001 (CertChainError etc. -> fail closed) + return fail("pck_chain_invalid") result = TDXVerificationResult(verified=True) result.verified_fields.extend(["dcap_quote_signature", "pck_chain"])