Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions python/src/agent_manifest/_attestation.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,18 +233,36 @@ def verify_attestation_chain(
cannot pass, because an unverified report proves nothing.
"""
reasons: list[str] = []
platform = getattr(report, "platform", "") or ""

# Step 3: manifest-hash binding (software-checkable).
expected_digest = expected_manifest_hash.split(":", 1)[-1].lower()
actual_hex = _report_data_hex(report)
if actual_hex is None:
report_data_matched = False
reasons.append("report has no 'report_data' field to check the manifest binding against")
#
# Does not apply on Azure: the guest never controls REPORT_DATA there (the
# paravisor sets it to sha256(runtime_data) to bind the vTPM AK, not the
# manifest hash - see the module docstring and LIMITATIONS.md). Manifest
# binding on Azure is checked separately, via
# AzureCVMProvider.verify_manifest_in_report(). Treating this field as
# authoritative there would mean `passed` could never be True for a
# genuine Azure report, no matter how correct everything else is.
azure_paravisor = platform == "azure-cvm-sev-snp"
if azure_paravisor:
report_data_matched = True
reasons.append(
"report_data binding not applicable on Azure (REPORT_DATA is "
"sha256(runtime_data), not the manifest hash); manifest binding "
"is checked via AzureCVMProvider.verify_manifest_in_report()"
)
else:
# The first 32 bytes (64 hex chars) of REPORT_DATA carry the digest.
report_data_matched = hmac.compare_digest(actual_hex[:64].lower(), expected_digest)
if not report_data_matched:
reasons.append("manifest hash does not match the report_data binding")
expected_digest = expected_manifest_hash.split(":", 1)[-1].lower()
actual_hex = _report_data_hex(report)
if actual_hex is None:
report_data_matched = False
reasons.append("report has no 'report_data' field to check the manifest binding against")
else:
# The first 32 bytes (64 hex chars) of REPORT_DATA carry the digest.
report_data_matched = hmac.compare_digest(actual_hex[:64].lower(), expected_digest)
if not report_data_matched:
reasons.append("manifest hash does not match the report_data binding")

# Step 2: launch-measurement allow-list (software-checkable, optional).
measurement_matched: Optional[bool]
Expand All @@ -266,7 +284,6 @@ def verify_attestation_chain(
# VCEK material). Intel TDX verifies the self-contained DCAP quote + PCK chain
# to the pinned Intel SGX Root CA. Either way, without a verifiable signature
# the result cannot pass.
platform = getattr(report, "platform", "") or ""
if platform == "intel-tdx":
signature = _verify_tdx_signature_step(report, reasons, trusted_tdx_root_pem)
elif platform in ("tpm", "aws-nitro"):
Expand Down
51 changes: 51 additions & 0 deletions python/tests/test_attestation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,54 @@ def test_full_chain_reads_snp_bytes_from_report_quote():
)
assert result.signature is SignatureStatus.VERIFIED
assert result.passed is True


# ---------------------------------------------------------------------------
# Azure paravisor SNP: REPORT_DATA is sha256(runtime_data), never the manifest
# hash (the guest does not control it - see LIMITATIONS.md). Manifest binding
# is checked separately via AzureCVMProvider.verify_manifest_in_report(), so
# this function must not gate `passed` on report_data for azure-cvm-sev-snp.
# ---------------------------------------------------------------------------


def test_azure_report_passes_despite_report_data_not_carrying_manifest_hash():
# report_data here is sha256(runtime_data) - by construction NOT equal to
# the manifest digest. A genuine Azure report always looks like this.
runtime_data_hash = hashlib.sha256(b"azure-runtime-data").hexdigest()
snp, vcek_der, chain = _synthetic_snp_with_chain(runtime_data_hash, MEASUREMENT)
report = AttestationReport(
platform="azure-cvm-sev-snp",
manifest_hash=MANIFEST_HASH,
quote=snp,
raw={"report_data": runtime_data_hash + "00" * 32, "measurement": MEASUREMENT},
)
result = verify_attestation_chain(
report,
expected_manifest_hash=MANIFEST_HASH,
vcek_cert_der=vcek_der,
cert_chain_pem=chain,
)
assert result.signature is SignatureStatus.VERIFIED
assert result.report_data_matched is True
assert result.passed is True
assert any("not applicable on Azure" in r for r in result.reasons)


def test_non_azure_snp_still_requires_report_data_to_match():
# Confirms the fix is scoped to azure-cvm-sev-snp: a direct-silicon SNP
# report (amd-sev-snp) must still bind REPORT_DATA to the manifest hash.
snp, vcek_der, chain = _synthetic_snp_with_chain(MEASUREMENT, MEASUREMENT) # wrong digest
report = AttestationReport(
platform="amd-sev-snp",
manifest_hash=MANIFEST_HASH,
quote=snp,
raw={"report_data": MEASUREMENT + "00" * 16, "measurement": MEASUREMENT},
)
result = verify_attestation_chain(
report,
expected_manifest_hash=MANIFEST_HASH,
vcek_cert_der=vcek_der,
cert_chain_pem=chain,
)
assert result.report_data_matched is False
assert result.passed is False
Loading