Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
__pycache__/
*.pyc
*.egg-info/
dist/
build/

**/target/
/*/tools/
/*/image
Expand Down
46 changes: 46 additions & 0 deletions sev_verify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# sev_verify

Host-side testing harness for SEV-SNP certification. Reads TOML manifests that declare which tests to run, imports per-test Python modules that define executable steps, and orchestrates execution across host and guest environments.

## Usage

```bash
# Run a specific certification level
python3 -m sev_verify /path/to/guest.efi -v 3.0

# Run multiple levels
python3 -m sev_verify /path/to/guest.efi -v 3.0 -v 3.1

# Run all certifications found in cert_tests/
python3 -m sev_verify /path/to/guest.efi
```

## How it works

1. Discover manifests at `cert_tests/*/manifest.toml`. Each manifest declares test entries (name, scope, module path).

2. For each test, import its Python module from the same `cert_tests/<level>/` directory and call `steps()` to get the ordered list of `Step` objects. Steps specify a shell command, where it runs (host or guest), what constitutes success, and a timeout.

3. Execute steps sequentially. Host steps run locally via subprocess. Guest steps are sent to the VM over a dedicated serial channel (`ttyS1`). For tests with `scope: guest` or `scope: mixed`, a QEMU SNP guest is launched before the first guest step and torn down after the last.

4. Write results to `results/`.
Comment on lines +20 to +26

## Layout

```
sev_verify/ Harness package
cli.py CLI arg parsing + entry point
models.py Step, TestDefinition, CertificationDefinition
cert_tests/ Certification levels
common/ Shared test modules
snphost_ok.py Test modules
...
cert_3_0/ Level 3.0
manifest.toml What to run
...
results/ Output (gitignored)
```

## Requirements

Python 3.11+ (uses `tomllib` from stdlib). No external packages.
1 change: 1 addition & 0 deletions sev_verify/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""sev-verify: SEV-SNP certification testing harness."""
7 changes: 7 additions & 0 deletions sev_verify/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""python3 -m sev_verify"""

import sys

from .cli import main

sys.exit(main())
Empty file.
Empty file.
2 changes: 2 additions & 0 deletions sev_verify/cert_tests/cert_3_0/manifest.toml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you/we planning to only maintain one certificate definition per generation?

Or what is your idea for the certificate definition structure?

I was looking at your command-line arguments and noticed that we would run certificates per CPU generation. I do like that approach.

My original intent was to go much more granular, something like:

-v 3.0.0-0 -v 3.1.2-0 -v 3.0.1-2

But I feel like that would require users to type too much if they want to run more than one test.

We’ll keep it as you proposed, but I’m curious how you envision the full manifest evolving as we add new versions across the different certificate generations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Or I guess, now that I read a little more, I think I would need an example on what a full manifest would look like.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My original intent was to go much more granular, something like:
-v 3.0.0-0 -v 3.1.2-0 -v 3.0.1-2

This would definitely be useful, maybe even to a single test level. Let me work on adding handling for that. And yeah I'll flesh out some examples - I had the vague idea that each test would be its own python module, but didn't think about the sub-groupings of certificates. So good questions, I need to work with @ajcaldelas to get the certificate structure thought through & how we can populate everything.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = "3.0"
description = "SEV 3.0 Tests (AMD EPYC 7003+) - Current Level 3.0.0-0"
Comment on lines +1 to +2
Empty file.
24 changes: 24 additions & 0 deletions sev_verify/cert_tests/common/snphost_ok.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""snphost-ok: Verify SNP is enabled and functional on the host."""

from sev_verify.models import Step


def steps() -> list[Step]:
return [
Step(
name="snphost-ok",
type="required",
runs_on="host",
command="snphost ok",
expected_result="exit_code:0",
timeout=30,
),
Step(
name="snphost-show-guests",
type="info",
runs_on="host",
command="snphost show guests",
expected_result="exit_code:0",
timeout=10,
),
]
105 changes: 105 additions & 0 deletions sev_verify/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""CLI arg parsing and entry point for sev_verify."""

from __future__ import annotations

import argparse
import sys
import tomllib
from pathlib import Path

from .models import CertificationDefinition, TestDefinition


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="sev_verify",
description="SEV-SNP certification testing harness",
)
parser.add_argument(
"path_to_guest",
help="Path to the guest image/UKI",
)
parser.add_argument(
"--version",
"-v",
dest="versions",
action="append",
default=[],
help="Certification version(s) to run (e.g. 3.0). Repeatable. "
"If omitted, all cert_tests/*/manifest.toml are used.",
)
return parser.parse_args(argv)


def load_manifest(toml_path: Path) -> CertificationDefinition:
"""Load a TOML certification manifest into a CertificationDefinition."""
with open(toml_path, "rb") as f:
data = tomllib.load(f)

try:
tests = [TestDefinition(**t) for t in data.get("tests", [])]
return CertificationDefinition(
version=data["version"],
description=data["description"],
tests=tests,
)
except (KeyError, TypeError) as exc:
raise ValueError(f"Invalid manifest {toml_path}: {exc}") from exc


def discover_manifests(cert_dir: Path, versions: list[str]) -> list[Path]:
"""Find all manifest.toml files in cert_tests/ subdirectories."""
if not cert_dir.is_dir():
return []

if not versions:
return sorted(cert_dir.glob("*/manifest.toml"))

manifest_paths = []
for version in versions:
subfolder = "cert_" + version.replace(".", "_")
mpath = cert_dir / subfolder / "manifest.toml"
if not mpath.exists():
print(f"Error: no manifest for version {version!r} "
f"(expected {mpath})", file=sys.stderr)
continue
manifest_paths.append(mpath)

return manifest_paths


def print_certification(cert: CertificationDefinition) -> None:
"""Print certification header."""
header = f" Certification {cert.version} "
print(f"──{header}{'─' * (60 - len(header))}")
print(f" {cert.description}")


def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
guest_path = Path(args.path_to_guest)

if not guest_path.exists():
print(f"Error: guest path does not exist: {guest_path}", file=sys.stderr)
return 1

cert_dir = Path(__file__).resolve().parent / "cert_tests"

manifest_paths = discover_manifests(cert_dir, args.versions)

if not manifest_paths:
print(
"Error: no manifest.toml found in cert_tests/*/",
file=sys.stderr,
)
Comment on lines +91 to +94
return 1

print(f" Guest: {guest_path}")
print()

for manifest_path in manifest_paths:
cert = load_manifest(manifest_path)
print_certification(cert)
print()

return 0
80 changes: 80 additions & 0 deletions sev_verify/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Data model dataclasses for sev_verify."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal


# ── Definition models (loaded from TOML manifests) ──────────────


@dataclass
class Step:
"""A single executable step within a test."""

name: str
type: Literal["setup", "required", "info"]
runs_on: Literal["host", "guest"]
command: str
expected_result: str # e.g. "exit_code:0", "stdout_contains:PASS"
timeout: int = 60


@dataclass
class TestDefinition:
"""A test declared in the TOML manifest."""

name: str
module: str # dotted module path, e.g. "cert_tests.common.snphost_ok"
scope: Literal["host", "guest", "mixed"]

@property
def requires_vm(self) -> bool:
return self.scope in ("guest", "mixed")


@dataclass
class CertificationDefinition:
"""Top-level certification suite loaded from a TOML manifest."""

version: str
description: str
tests: list[TestDefinition] = field(default_factory=list)


# ── Runtime result models (populated during execution) ──────────


@dataclass
class StepResult:
"""Result of executing a single Step."""

step: Step
result: Literal["pass", "fail", "error", "skip"]
exit_code: int | None = None
stdout: str | None = None
stderr: str | None = None
duration_ms: int | None = None


@dataclass
class TestResult:
"""Result of executing a TestDefinition."""

test: TestDefinition
result: Literal["pass", "fail", "error"]
step_results: list[StepResult] = field(default_factory=list)
started_at: str | None = None
completed_at: str | None = None


@dataclass
class CertificationResult:
"""Result of executing a CertificationDefinition."""

certification: CertificationDefinition
result: Literal["pass", "fail", "error"]
test_results: list[TestResult] = field(default_factory=list)
started_at: str | None = None
completed_at: str | None = None
Loading