Skip to content

feat: add key derivation tests to sev_verify harness, with cross-CVM check - #285

Closed
markg-github wants to merge 7 commits into
AMDEPYC:mainfrom
markg-github:dev/cross-cvm-keys-v2
Closed

feat: add key derivation tests to sev_verify harness, with cross-CVM check#285
markg-github wants to merge 7 commits into
AMDEPYC:mainfrom
markg-github:dev/cross-cvm-keys-v2

Conversation

@markg-github

Copy link
Copy Markdown
Contributor

Adapts the SNP key derivation tests to run within the sev_verify harness, with all test logic on the host using vsock to issue snpguest key commands on the guest. No guest-side Python script is needed — snpguest is already available in the generic guest image.

What's tested

  • Determinism: same params → same key
  • VMPL isolation: different VMPL → different keys
  • Root key difference: VCEK vs VMRK → different keys
  • SVN bound enforcement: values above the active bound are rejected (tested with committed+1..+3, always exercised even without an ID block)
  • SVN sensitivity: all valid SVN values produce distinct keys (N/A without ID block)
  • TCB bound enforcement: committed+1..+3 per component are rejected
  • TCB sensitivity: distinct valid TCB values produce distinct keys
  • GFS sensitivity: GFS=0x01 and GFS=0x02 produce different keys
  • GFS field mixing: bits 0-5 each produce a key distinct from GFS=0 baseline
  • Cross-CVM determinism: same key derived in two independent CVMs matches, proving platform-bound derivation

Approach

The attestation report is fetched and pulled to the host first (while the VM is running), then callable steps loop over snpguest key via vsock with dynamic bounds from the report. SVN and TCB sweeps are fully runtime-driven — no static assumptions about platform values.

The cross-CVM check saves a key from CVM 1, stops it, launches CVM 2, rederives the same key, and compares on the host.

Tested on AMD EPYC 9004 hardware. All 17 steps pass.

Closes #269

markg-github and others added 7 commits July 30, 2026 15:43
Co-Authored-By: Claude <noreply@anthropic.com>
Always verify that values above the active bound are rejected, even
when the bound is 0 (no ID block). Tests 3 values above the bound
for both guest SVN and each TCB component.

Co-Authored-By: Claude <noreply@anthropic.com>
Import calculate_measurement from attestation_test instead of
duplicating it. Conditionally add generate_id_block step when
sev_verify.id_block is available.

Co-Authored-By: Claude <noreply@anthropic.com>
Same fix as pr/id-block — snphost ok can report failures on systems
that are otherwise fully functional for SNP guest testing.

Co-Authored-By: Claude <noreply@anthropic.com>
…mands

Replace the guest-side Python script approach with individual snpguest
key commands over vsock, pulling derived keys to the host for comparison.
All test logic (determinism, VMPL isolation, root key difference, SVN/TCB
bound enforcement, GFS sensitivity and field mixing) now lives in the
sev_verify test module as callable steps.

Use callable steps that loop over snpguest key via
run_guest_command/fetch_guest_file_bytes while the VM is running. SVN
and TCB sweeps are fully dynamic based on the runtime attestation report.

TCB above-bound tests use committed+1..committed+3 per component from
the runtime report, capped at 0xFF to avoid 8-bit overflow.

Remove key-derivation guest module. The guest-side Python script and systemd
service are no longer needed and snpguest is available in the generic guest
image.

Co-Authored-By: Claude <noreply@anthropic.com>
Derives a key at the end of CVM 1, stops it, launches CVM 2, derives
the same key with identical parameters, and compares. Proves the key
is bound to platform identity rather than transient VM state.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 14:05
@markg-github
markg-github marked this pull request as draft August 1, 2026 14:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new SEV-SNP key-derivation certification test to the sev_verify harness, including a cross-CVM (two independent guest launches) determinism check driven entirely from the host via vsock-issued snpguest key commands.

Changes:

  • Introduces a new c3_0_0_2 test module that parses the attestation report on-host and runs multiple derived-key property checks (determinism, VMPL/root key differences, SVN/TCB bounds & sensitivity, GFS mixing, cross-CVM match).
  • Registers the new test at certification level 3.0.0-2 in the c3_0 manifest.
  • Updates prerequisites behavior (snp_ok) and guest image build dependencies (adds python3 package).

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sev_verify/cert_tests/common/snp_ok.py Changes severity of snphost ok step within prereq gating test.
sev_verify/cert_tests/c3_0/manifest.toml Registers new key-derivation test at level 3.0.0-2.
sev_verify/cert_tests/c3_0/c3_0_0_2/key_derivation_test.py New host-driven vsock key-derivation test suite + cross-CVM check.
sev_verify/cert_tests/c3_0/c3_0_0_2/init.py Adds package init for the new test module directory.
modules/build/guest/mkosi.conf Adds python3 to guest build packages.
Suppressed comments (4)

sev_verify/cert_tests/c3_0/c3_0_0_2/key_derivation_test.py:222

  • If either VMPL key read fails (returns None), k0 != k1 can evaluate to True and incorrectly pass the isolation check. Fail explicitly when key files cannot be read.
    k0 = _read_key(ctx.artifact_dir / "vmpl0_key.bin")
    k1 = _read_key(ctx.artifact_dir / "vmpl1_key.bin")
    if k0 != k1:
        return StepHandlerResult(exit_code=0, stdout="VMPL0 and VMPL1 keys differ (proper isolation)")
    return StepHandlerResult(exit_code=1, stderr="VMPL0 and VMPL1 keys are identical")

sev_verify/cert_tests/c3_0/c3_0_0_2/key_derivation_test.py:234

  • If either key read fails (returns None), kv != km can incorrectly pass and claim the root keys differ. Explicitly fail if either key cannot be read.
    kv = _read_key(ctx.artifact_dir / "vcek_key.bin")
    km = _read_key(ctx.artifact_dir / "vmrk_key.bin")
    if kv != km:
        return StepHandlerResult(exit_code=0, stdout="VCEK and VMRK keys differ")
    return StepHandlerResult(exit_code=1, stderr="VCEK and VMRK keys are identical")

sev_verify/cert_tests/c3_0/c3_0_0_2/key_derivation_test.py:355

  • If either derived key read fails (returns None), k1 != k2 can incorrectly pass and claim GFS sensitivity. Fail explicitly when key files cannot be read.
    k1 = _read_key(ctx.artifact_dir / "gfs1_key.bin")
    k2 = _read_key(ctx.artifact_dir / "gfs2_key.bin")
    if k1 != k2:
        return StepHandlerResult(exit_code=0, stdout="GFS=0x01 and GFS=0x02 keys differ")
    return StepHandlerResult(exit_code=1, stderr="GFS=0x01 and GFS=0x02 keys are identical")

sev_verify/cert_tests/c3_0/c3_0_0_2/key_derivation_test.py:384

  • baseline may be None if the key file can't be read; in that case k != baseline can incorrectly pass for all bits. Treat missing baseline/bit keys as a hard failure to avoid false positives.
    ok, err = _derive_key(ctx, "gfs_baseline.bin", gfs=0)
    if not ok:
        return StepHandlerResult(exit_code=1, stderr=f"Baseline derivation failed: {err}")
    baseline = _read_key(ctx.artifact_dir / "gfs_baseline.bin")

    bits = [
        (0, "Image ID"),
        (1, "Family ID"),
        (2, "Measurement"),
        (3, "Guest SVN Policy"),
        (4, "Guest SVN"),
        (5, "TCB Version"),
    ]
    failed = []
    lines = []
    for bit, label in bits:
        ok, err = _derive_key(ctx, f"gfs_bit{bit}.bin", gfs=1 << bit)
        if not ok:
            failed.append(f"GFS bit {bit} ({label}): derivation failed: {err}")
            continue
        k = _read_key(ctx.artifact_dir / f"gfs_bit{bit}.bin")
        if k != baseline:
            lines.append(f"GFS bit {bit} ({label}): differs from baseline")
        else:
            failed.append(f"GFS bit {bit} ({label}): same as baseline")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 48 to 52
Step.for_host(
name="snphost ok",
type="required",
type="info",
command="snphost ok",
),
Comment on lines +201 to +205
k1 = _read_key(ctx.artifact_dir / "det_key1.bin")
k2 = _read_key(ctx.artifact_dir / "det_key2.bin")
if k1 == k2:
return StepHandlerResult(exit_code=0, stdout="Keys match (deterministic)")
return StepHandlerResult(exit_code=1, stderr="Keys differ — derivation is not deterministic")
Comment on lines +72 to +80
class ReportInfo:
version: Optional[int] = None
guest_svn: int = 0
committed_tcb: TcbVersion = None

def __post_init__(self):
if self.committed_tcb is None:
self.committed_tcb = TcbVersion()

Comment on lines +156 to +158
info = _parse_report_info(result.stdout)
c = info.committed_tcb
(ctx.artifact_dir / "report_info.txt").write_text(
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.

2 participants