diff --git a/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/generate_sev_certificate.py b/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/generate_sev_certificate.py index 9515f321..b1339aae 100644 --- a/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/generate_sev_certificate.py +++ b/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/generate_sev_certificate.py @@ -1,23 +1,41 @@ -import sys -import subprocess import os import emoji as em -import textwrap -import re -import json -# Import user-defined modules located at sibling directory in the parent folder -from sev_certificate_version_3_0_0_0 import SEV_Certificate as sev_certificate_v_3_0_0_0 +from sev_certificate_version_3_0_0_0 import SEV_Certificate as SEV_Certificate_v3_0_0_0 +from sev_certificate_version import SEV_Certificate as SEV_Certificate_structured -sev_report = '' +FAIL_MARKER = em.emojize(':cross_mark:') -# Get SEV Certificate Version 3.0.0-0 -sev_report_v_3_0_0_0 = sev_certificate_v_3_0_0_0() -sev_report += sev_report_v_3_0_0_0.generate_sev_certificate() +# Certification levels in ascending order. +# v3.0.0-0 uses its own class (service-based status extraction). +# All subsequent levels use the structured JSON class, just with different version strings. +levels = [ + SEV_Certificate_v3_0_0_0(), + SEV_Certificate_structured("3.0.0-1"), +] -# Print SEV Certificate into the console -print(sev_report) +combined = '' +highest_passed = None -# Write certificate to file -sev_report_v_3_0_0_0.write_sev_certificate(sev_report, "~/sev_certificate_v3.0.0-0.txt") +for cert in levels: + content = cert.generate_sev_certificate() + combined += content + if FAIL_MARKER not in content: + highest_passed = cert.sev_version +print(combined) + +# Write one combined cert named after highest achieved level +if highest_passed: + output_file = os.path.expanduser(f"~/sev_certificate_v{highest_passed}.txt") +else: + output_file = os.path.expanduser("~/sev_certificate.txt") + +with open(output_file, "w") as f: + f.write(combined) + +print(f"Certificate saved to: {output_file}") +if highest_passed: + print(f"Highest achieved level: {highest_passed}") +else: + print("No certification level achieved") diff --git a/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/sev_certificate_version.py b/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/sev_certificate_version.py new file mode 100644 index 00000000..17dfe5d0 --- /dev/null +++ b/modules/report/host/sev-certificate-generator/mkosi.extra/usr/local/lib/scripts/generate_sev_certificate/sev_certificate/sev_certificate_version.py @@ -0,0 +1,92 @@ +import subprocess +import json +import emoji as em + +test_status_emojis = { + 'pass': em.emojize(':check_mark_button:'), + 'fail': em.emojize(':cross_mark:'), + 'skip': em.emojize(':fast_forward:', language='alias'), +} + + +class SEV_Certificate: + """Generic certificate generator for structured JSON test results. + + Parses step/summary JSON lines from journald, filtered by SEV_VERSION + and grouped by SEV_TEST_GROUP. Works for any certification level that + uses the emit_step/emit_summary JSON format. + """ + + def __init__(self, sev_version): + self.sev_version = sev_version + + def get_test_group_summary(self): + """Generate per-group test summaries from structured JSON results.""" + cmd = f"journalctl SEV_VERSION={self.sev_version} -o json" + result = subprocess.run(cmd, shell=True, text=True, capture_output=True, check=True) + + groups = {} + for line in result.stdout.strip().splitlines(): + try: + record = json.loads(line) + message = record.get("MESSAGE", "") + if not message.startswith("{"): + continue + entry = json.loads(message) + except (json.JSONDecodeError, ValueError): + continue + group = record.get("SEV_TEST_GROUP", "unknown") + if group not in groups: + groups[group] = {"steps": [], "summary": None} + if entry.get("type") == "step": + groups[group]["steps"].append(entry) + elif entry.get("type") == "summary": + groups[group]["summary"] = entry + + content = "" + + for group, data in groups.items(): + summary = data["summary"] + steps = data["steps"] + + if summary: + overall = summary.get("status", "?") + passed = summary.get("passed", 0) + failed = summary.get("failed", 0) + else: + passed = sum(1 for s in steps if s.get("status") == "pass") + failed = sum(1 for s in steps if s.get("status") == "fail") + overall = "fail" if failed > 0 else "pass" + + overall_emoji = test_status_emojis.get(overall, "?") + content += f"\n[ {overall_emoji} ] {group} ({passed} passed, {failed} failed)\n" + + for step in steps: + emoji = test_status_emojis.get(step.get("status", "?"), "?") + name = step.get("test", "?") + detail = step.get("detail", "") + line = f"\t{emoji} {name}" + if detail: + line += f" ({detail})" + content += line + "\n" + + return content.expandtabs(2) + + def get_sev_log(self): + """Get raw journal log for this certification level.""" + cmd = f"journalctl SEV_VERSION={self.sev_version} --no-hostname --utc" + result = subprocess.run(cmd, shell=True, text=True, capture_output=True, check=True) + return result.stdout + + def generate_sev_certificate(self): + """Generate the SEV Certificate content for this level.""" + content = "\n ====== SEV CERTIFICATE ====== \n" + content += f"\n SEV VERSION: {self.sev_version} \n" + + content += "\n=== SUMMARY ===\n" + content += self.get_test_group_summary() + + content += f"\n=== SEV VERSION {self.sev_version} LOG ===\n" + content += self.get_sev_log() + + return content.expandtabs(2) diff --git a/modules/stop/host/beacon-report/mkosi.extra/usr/local/lib/scripts/beacon-report.sh b/modules/stop/host/beacon-report/mkosi.extra/usr/local/lib/scripts/beacon-report.sh index 802fafde..485e6948 100755 --- a/modules/stop/host/beacon-report/mkosi.extra/usr/local/lib/scripts/beacon-report.sh +++ b/modules/stop/host/beacon-report/mkosi.extra/usr/local/lib/scripts/beacon-report.sh @@ -1,16 +1,15 @@ #!/usr/bin/bash set -euo pipefail -SEV_VERSIONS=("3.0.0-0") -SEV_CERT_FILE="" - -# Temporarily hardcode the milestone name -MILESTONE="c3.0.0-0" +# Maximum milestone version to report via beacon. +# Milestones above this version are omitted from the report even if achieved. +# Bump this when a new certification level is ready to be officially reported. +MAX_MILESTONE="3.0.0-0" # Determine OS name and version if [ -f /etc/os-release ]; then . /etc/os-release - OS_NAME="${ID}" + OS_NAME="${ID}" OS_VERSION="${VERSION_ID:-""}" # Initialize OS release with the OS VERSION_CODENAME if VERSION_ID is missing in /etc/os-release. @@ -28,32 +27,43 @@ fi # Fetch AMD processor model PROC_LABEL=$(/usr/bin/python3 /usr/local/lib/scripts/get_processor_model.py series) -# Loop over to generate beacon report for all SEV certificates -for sev_version in "${SEV_VERSIONS[@]}"; do - # Build title - if [ -n "$OS_VERSION" ]; then - SEV_TITLE="${OS_NAME} ${OS_VERSION} SEV version ${sev_version}" - else - SEV_TITLE="${OS_NAME} SEV version ${sev_version}" - fi +# Find the combined certificate file (generator names it after the highest achieved level) +SEV_CERT_FILE=$(compgen -G "${HOME:-/root}/sev_certificate_v3.0.*.txt" | sort -V | tail -1 || true) +if [ -z "$SEV_CERT_FILE" ]; then + # Fallback: no level achieved, generator writes unversioned file + SEV_CERT_FILE="${HOME:-/root}/sev_certificate.txt" +fi - # Obtain SEV Version Content - SEV_CERT_FILE="${HOME:-/root}/sev_certificate_v${sev_version}.txt" +# Extract achieved version from filename (e.g. sev_certificate_v3.0.0-1.txt -> 3.0.0-1) +ACHIEVED=$(basename "$SEV_CERT_FILE" | sed -n 's/sev_certificate_v\(.*\)\.txt/\1/p') + +# Cap achieved version to MAX_MILESTONE for reporting purposes +REPORTED="$ACHIEVED" +if [ -n "$ACHIEVED" ]; then + if [ "$(printf '%s\n' "$MAX_MILESTONE" "$ACHIEVED" | sort -V | head -1)" != "$ACHIEVED" ]; then + echo "Achieved milestone c${ACHIEVED} exceeds MAX_MILESTONE ${MAX_MILESTONE}, capping to c${MAX_MILESTONE}" + REPORTED="$MAX_MILESTONE" + fi +fi - # Set up parameters - PARAMS=() +# Build title +if [ -n "$OS_VERSION" ]; then + SEV_TITLE="${OS_NAME} ${OS_VERSION} SEV certification${REPORTED:+ v${REPORTED}}" +else + SEV_TITLE="${OS_NAME} SEV certification${REPORTED:+ v${REPORTED}}" +fi - # Add labels - PARAMS+=("--label" "certificate") - PARAMS+=("--label" "os-${OS_LABEL}") - PARAMS+=("--label" "proc-${PROC_LABEL}") +# Set up parameters +PARAMS=() +PARAMS+=("--label" "certificate") +PARAMS+=("--label" "os-${OS_LABEL}") +PARAMS+=("--label" "proc-${PROC_LABEL}") - # Add milestone for valid test - if [ -e "${SEV_CERT_FILE}" ] && [ -z "$(grep "❌" "${SEV_CERT_FILE}")" ]; then - PARAMS+=("--milestone" "$MILESTONE") - fi +# Add milestone if a level was achieved +if [ -n "$REPORTED" ]; then + PARAMS+=("--milestone" "c${REPORTED}") +fi - beacon report --title "$SEV_TITLE" --body "$SEV_CERT_FILE" "${PARAMS[@]}" +beacon report --title "$SEV_TITLE" --body "$SEV_CERT_FILE" "${PARAMS[@]}" - echo "Published SEV certificate via beacon with title: $SEV_TITLE" -done +echo "Published SEV certificate via beacon with title: $SEV_TITLE" diff --git a/modules/system/guest/snpguest-ok/mkosi.extra/usr/local/lib/systemd/system/snpguest-ok.service b/modules/system/guest/snpguest-ok/mkosi.extra/usr/local/lib/systemd/system/snpguest-ok.service index 387018f1..3eb933be 100644 --- a/modules/system/guest/snpguest-ok/mkosi.extra/usr/local/lib/systemd/system/snpguest-ok.service +++ b/modules/system/guest/snpguest-ok/mkosi.extra/usr/local/lib/systemd/system/snpguest-ok.service @@ -1,8 +1,8 @@ [Unit] -Description=Run snguest ok to verify SNP enablement on guest +Description=Run snpguest ok to verify SNP enablement on guest DefaultDependencies=no -After=boot-succesful.service -wants=boot-succesful.service +After=boot-successful.service systemd-modules-load.service +Wants=boot-successful.service systemd-modules-load.service [Service] Type=oneshot diff --git a/modules/test/host/mkosi.conf b/modules/test/host/mkosi.conf index 6d0961fe..da4d9ec6 100644 --- a/modules/test/host/mkosi.conf +++ b/modules/test/host/mkosi.conf @@ -1,2 +1,3 @@ [Include] +Include=./snphost-config-commit Include=./test-done diff --git a/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/scripts/snphost_config_commit.sh b/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/scripts/snphost_config_commit.sh new file mode 100755 index 00000000..ea0162c6 --- /dev/null +++ b/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/scripts/snphost_config_commit.sh @@ -0,0 +1,215 @@ +#!/bin/bash + +# snphost config/commit test script -- certification level 3.0.0-1 +# +# Exercises snphost config set, config reset, and commit subcommands. +# Runs all 8 test cases, continuing past failures to collect full results. +# +# Output: JSON lines to stdout (captured by journald via StandardOutput=journal+console). +# Each test emits a {"type":"step"} line; a {"type":"summary"} line is emitted at the end. +# The service unit tags output with LogExtraFields: +# SEV_VERSION=3.0.0-1 -- certification level +# SEV_TEST_GROUP=snphost-config-commit -- this test group's identifier + +PASSED=0 +FAILED=0 + +emit_step() { + local test_name="$1" status="$2" detail="${3:-}" + jq -nc --arg t "$test_name" --arg s "$status" --arg d "$detail" \ + '{type:"step",test:$t,status:$s} + (if $d!="" then {detail:$d} else {} end)' + case "$status" in + pass) PASSED=$((PASSED + 1)) ;; + fail) FAILED=$((FAILED + 1)) ;; + esac +} + +emit_summary() { + local overall="pass" + [[ "$FAILED" -gt 0 ]] && overall="fail" + jq -nc --arg s "$overall" --argjson p "$PASSED" --argjson f "$FAILED" \ + '{type:"summary",status:$s,passed:$p,failed:$f}' +} + +run_cmd() { + CMD_ERROR="" + local output + output=$("$@" 2>&1) + local rc=$? + if [[ $rc -ne 0 ]]; then + CMD_ERROR="${output}" + return 1 + fi + echo "${output}" +} + +# ─── TCB Parsing ──────────────────────────────────────────────────────────── + +parse_tcb_field() { + local tcb_output="$1" field_name="$2" section="$3" + echo "${tcb_output}" \ + | awk -v section="${section}" -v field="${field_name}" ' + BEGIN { in_section=0 } + $0 ~ section " TCB:" { in_section=1; next } + in_section && / TCB:/ { in_section=0 } + in_section && $0 ~ field ":" { + sub(/.*:[ \t]*/, "") + gsub(/[ \t\r\n]/, "") + print + exit + } + ' +} + +read_platform_tcb() { + local tcb_output="$1" + PLATFORM_BL=$(parse_tcb_field "${tcb_output}" "Boot Loader" "Platform") + PLATFORM_TEE=$(parse_tcb_field "${tcb_output}" "TEE" "Platform") + PLATFORM_SNP=$(parse_tcb_field "${tcb_output}" "SNP" "Platform") + PLATFORM_UCODE=$(parse_tcb_field "${tcb_output}" "Microcode" "Platform") + PLATFORM_FMC=$(parse_tcb_field "${tcb_output}" "FMC" "Platform") + + if [[ -n "${PLATFORM_FMC}" ]]; then + HAS_FMC=1 + [[ "${PLATFORM_FMC}" == "None" ]] && PLATFORM_FMC=0 + else + HAS_FMC=0 + fi +} + +build_config_set_args() { + local bl="$1" tee="$2" snp="$3" ucode="$4" mask="$5" + CONFIG_ARGS=("${bl}" "${tee}" "${snp}" "${ucode}") + [[ "${HAS_FMC}" -eq 1 ]] && CONFIG_ARGS+=("${PLATFORM_FMC}") + CONFIG_ARGS+=("${mask}") +} + +tcb_versions_match() { + local tcb_output="$1" + local field reported platform + for field in "Boot Loader" "TEE" "SNP" "Microcode"; do + reported=$(parse_tcb_field "${tcb_output}" "${field}" "Reported") + platform=$(parse_tcb_field "${tcb_output}" "${field}" "Platform") + [[ "${reported}" != "${platform}" ]] && return 1 + done + return 0 +} + +# ─── Tests ──────────────────────────────────────────────────────────────── + +test_read_tcb() { + local output + output=$(run_cmd snphost show tcb) || { emit_step "read_tcb" "fail" "${CMD_ERROR}"; return 1; } + read_platform_tcb "${output}" + local detail="bl=${PLATFORM_BL} tee=${PLATFORM_TEE} snp=${PLATFORM_SNP} ucode=${PLATFORM_UCODE}" + [[ "${HAS_FMC}" -eq 1 ]] && detail+=" fmc=${PLATFORM_FMC}" + emit_step "read_tcb" "pass" "${detail}" +} + +test_config_set_lower() { + local set_bl="${PLATFORM_BL}" set_tee="${PLATFORM_TEE}" + local set_snp="${PLATFORM_SNP}" set_ucode="${PLATFORM_UCODE}" + local decremented_field="" + + if [[ "${PLATFORM_BL}" -gt 0 ]]; then + set_bl=$((PLATFORM_BL - 1)); decremented_field="Boot Loader" + elif [[ "${PLATFORM_SNP}" -gt 0 ]]; then + set_snp=$((PLATFORM_SNP - 1)); decremented_field="SNP" + elif [[ "${PLATFORM_TEE}" -gt 0 ]]; then + set_tee=$((PLATFORM_TEE - 1)); decremented_field="TEE" + elif [[ "${PLATFORM_UCODE}" -gt 0 ]]; then + set_ucode=$((PLATFORM_UCODE - 1)); decremented_field="Microcode" + else + emit_step "config_set_lower" "skip" "all platform TCB fields are 0" + return 0 + fi + + build_config_set_args "${set_bl}" "${set_tee}" "${set_snp}" "${set_ucode}" 0 + run_cmd snphost config set "${CONFIG_ARGS[@]}" >/dev/null || { emit_step "config_set_lower" "fail" "${CMD_ERROR}"; return 1; } + + local verify_output + verify_output=$(run_cmd snphost show tcb) || { emit_step "config_set_lower" "fail" "${CMD_ERROR}"; return 1; } + + local reported platform + reported=$(parse_tcb_field "${verify_output}" "${decremented_field}" "Reported") + platform=$(parse_tcb_field "${verify_output}" "${decremented_field}" "Platform") + + if [[ "${reported}" != "${platform}" ]]; then + emit_step "config_set_lower" "pass" "${decremented_field}: ${platform}->${reported}" + else + emit_step "config_set_lower" "fail" "reported ${decremented_field} should differ from platform after config set" + return 1 + fi +} + +test_config_reset() { + run_cmd snphost config reset >/dev/null || { emit_step "config_reset" "fail" "${CMD_ERROR}"; return 1; } + local verify_output + verify_output=$(run_cmd snphost show tcb) || { emit_step "config_reset" "fail" "${CMD_ERROR}"; return 1; } + if tcb_versions_match "${verify_output}"; then + emit_step "config_reset" "pass" + else + emit_step "config_reset" "fail" "reported TCB should match platform TCB after reset" + return 1 + fi +} + +test_mask_chip_id() { + build_config_set_args "${PLATFORM_BL}" "${PLATFORM_TEE}" "${PLATFORM_SNP}" "${PLATFORM_UCODE}" 1 + run_cmd snphost config set "${CONFIG_ARGS[@]}" >/dev/null || { emit_step "mask_chip_id" "fail" "${CMD_ERROR}"; return 1; } + run_cmd snphost show tcb >/dev/null || { emit_step "mask_chip_id" "fail" "${CMD_ERROR}"; return 1; } + emit_step "mask_chip_id" "pass" +} + +test_mask_chip_key() { + build_config_set_args "${PLATFORM_BL}" "${PLATFORM_TEE}" "${PLATFORM_SNP}" "${PLATFORM_UCODE}" 2 + run_cmd snphost config set "${CONFIG_ARGS[@]}" >/dev/null || { emit_step "mask_chip_key" "fail" "${CMD_ERROR}"; return 1; } + run_cmd snphost show tcb >/dev/null || { emit_step "mask_chip_key" "fail" "${CMD_ERROR}"; return 1; } + emit_step "mask_chip_key" "pass" +} + +test_both_masks() { + build_config_set_args "${PLATFORM_BL}" "${PLATFORM_TEE}" "${PLATFORM_SNP}" "${PLATFORM_UCODE}" 3 + run_cmd snphost config set "${CONFIG_ARGS[@]}" >/dev/null || { emit_step "both_masks" "fail" "${CMD_ERROR}"; return 1; } + run_cmd snphost show tcb >/dev/null || { emit_step "both_masks" "fail" "${CMD_ERROR}"; return 1; } + emit_step "both_masks" "pass" +} + +test_reset_after_masks() { + run_cmd snphost config reset >/dev/null || { emit_step "reset_after_masks" "fail" "${CMD_ERROR}"; return 1; } + local verify_output + verify_output=$(run_cmd snphost show tcb) || { emit_step "reset_after_masks" "fail" "${CMD_ERROR}"; return 1; } + if tcb_versions_match "${verify_output}"; then + emit_step "reset_after_masks" "pass" + else + emit_step "reset_after_masks" "fail" "reported TCB should match platform TCB after mask reset" + return 1 + fi +} + +test_commit() { + run_cmd snphost commit >/dev/null || { emit_step "commit" "fail" "${CMD_ERROR}"; return 1; } + emit_step "commit" "pass" +} + +# ─── Main ─────────────────────────────────────────────────────────────────── + +main() { + if ! test_read_tcb; then + emit_summary + return 1 + fi + + test_config_set_lower + test_config_reset + test_mask_chip_id + test_mask_chip_key + test_both_masks + test_reset_after_masks + test_commit + + emit_summary + [[ "${FAILED}" -eq 0 ]] +} + +main diff --git a/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/systemd/system/snphost-config-commit.service b/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/systemd/system/snphost-config-commit.service new file mode 100644 index 00000000..a8132a07 --- /dev/null +++ b/modules/test/host/snphost-config-commit/mkosi.extra/usr/local/lib/systemd/system/snphost-config-commit.service @@ -0,0 +1,12 @@ +[Unit] +Description=Test snphost config and commit commands +DefaultDependencies=no +After=launch.target snphost-ok.service +Requires=launch.target snphost-ok.service + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/scripts/snphost_config_commit.sh +StandardOutput=journal+console +StandardError=journal+console +LogExtraFields="SEV_VERSION=3.0.0-1" "SEV_TEST_GROUP=snphost-config-commit" diff --git a/modules/test/host/test-done/mkosi.extra/usr/local/lib/systemd/system/test-done.service b/modules/test/host/test-done/mkosi.extra/usr/local/lib/systemd/system/test-done.service index d83d4210..aaff947e 100644 --- a/modules/test/host/test-done/mkosi.extra/usr/local/lib/systemd/system/test-done.service +++ b/modules/test/host/test-done/mkosi.extra/usr/local/lib/systemd/system/test-done.service @@ -2,6 +2,9 @@ Description=Barrier that triggers test services DefaultDependencies=no +Wants=snphost-config-commit.service +After=snphost-config-commit.service + [Service] Type=oneshot ExecStart=/usr/bin/true