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
2 changes: 2 additions & 0 deletions modules/build/guest/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Include=../../stop/guest

[Content]
KernelCommandLine=console=ttyS0
Packages=
python3
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ TIMEOUT=60
INTERVAL=1
ELAPSED=0

units=("snpguest-ok.service" "attestation-workflow.service")
units=("snpguest-ok.service" "attestation-workflow.service" "key-derivation.service")

args=()
for unit in "${units[@]}"; do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,46 @@ def get_snp_guest_attestation_summary(self):

return snpguest_attestation_summary

def get_key_derivation_summary(self):
"""Generate SNP Guest Key Derivation summary from the key-derivation service.

Returns:
Tuple of (formatted_summary_str, inferred_status_str).
inferred_status is "passed", "failed", or None if no JSON data found.
"""

key_derivation_service = "key-derivation.service"
key_derivation_cmd = f"journalctl -D {self.guest_logs_path} -u {key_derivation_service} -o cat"
result = subprocess.run(key_derivation_cmd, shell=True, text=True, capture_output=True)

# Extract and parse JSON objects (format: {"test_name": "0"/"1"})
json_objects = re.findall(r'\{[^}]+\}', result.stdout)

key_derivation_data = {}
for obj in json_objects:
try:
key_derivation_data.update(json.loads(obj))
except (json.JSONDecodeError, ValueError):
pass

if not key_derivation_data:
return '', None

# Convert status codes to human-readable form (0=passed, non-zero=failed)
for step, status_code in key_derivation_data.items():
key_derivation_data[step] = "passed" if int(status_code) == 0 else "failed"

Comment on lines +138 to +140
# Infer overall status: failed if any step failed
inferred_status = "failed" if any(s == "failed" for s in key_derivation_data.values()) else "passed"

# Format output with test emojis
summary = ''
for step, step_status in key_derivation_data.items():
emoji = test_status_emojis.get(step_status.lower(), '?')
summary += "\t\t\t " + f"{emoji} {step}" + "\n"

return summary, inferred_status

def get_snp_guest_summary(self):
"""Generate all SNP Guest tests summary."""

Expand All @@ -118,6 +158,11 @@ def get_snp_guest_summary(self):
snpguest_services = command.stdout
snpguest_services_list = snpguest_services.splitlines()

# key-derivation.service may finish after other services and miss the journal
# upload window, so ensure it is always included even if discovery missed it.
if "key-derivation.service" not in snpguest_services_list:
snpguest_services_list.append("key-derivation.service")

# Map SNP Guest test service name with its status
snpguest_services_status ={}

Expand All @@ -130,17 +175,28 @@ def get_snp_guest_summary(self):
snpguest_emoji = ''

guest_attestation_summary = self.get_snp_guest_attestation_summary() + "\n"
key_derivation_summary, key_derivation_status = self.get_key_derivation_summary()
key_derivation_summary = (key_derivation_summary or '') + "\n"

for service, service_status in snpguest_services_status.items():
# For key-derivation.service, use JSON-inferred status when systemd lifecycle
# message is absent (shows as '?') due to journal-upload timing
if "key-derivation.service" in service.lower() and service_status == '?' and key_derivation_status:
service_status = key_derivation_status

emoji = test_status_emojis.get(service_status.lower(),'?')
content += "\t" + f"{emoji} {service} :"
service_description = self.sev_service.get_service_description(service, "guest")
content += " " + service_description + "\n"

# Add step-by-step summary status of the guest attestation workflow
if "attestation-workflow.service" in service.lower() :
if "attestation-workflow.service" in service.lower():
content += guest_attestation_summary

# Add step-by-step summary status of the key derivation tests
if "key-derivation.service" in service.lower():
content += key_derivation_summary

# Set "snpguest_emoji" status based on the single failed/skipped SNP test
if service_status.lower() == 'failed':
snpguest_emoji = 'failed'
Expand Down
190 changes: 190 additions & 0 deletions modules/test/guest/key-derivation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# SNP Guest Key Derivation Tests

This guest-image module includes a Python-based systemd service that executes comprehensive key derivation tests on SNP-enabled guests using the [snpguest tool](https://github.com/virtee/snpguest.git).

## Test Coverage

The test suite validates the following key derivation properties:

1. **Determinism**: Same parameters produce the same key
2. **VMPL Isolation**: Different VMPL values produce different keys (cryptographic isolation)
3. **Root Key Difference**: VCEK and VMRK root key selections produce different keys
4. **Guest SVN Sensitivity**: Different guest SVN values produce different keys
5. **TCB Sensitivity**: Different TCB versions produce different keys
6. **Guest Field Select Sensitivity**: Different guest field select values produce different keys

## Key Derivation Security Properties Tested

### VMPL-Based Key Isolation

The tests verify that the firmware correctly implements VMPL-based cryptographic isolation:
- Code at VMPL0 can derive keys tagged vmpl=0,1,2,3
- Code at VMPL1 can derive keys tagged vmpl=1,2,3 (but NOT vmpl=0)
- Keys derived with different VMPL values are cryptographically distinct

This prevents privilege escalation where compromised guest OS at VMPL1 attempts to access SVSM secrets at VMPL0.

### Root Key Selection

Tests validate that different root keys produce different derived keys:
- **VCEK (Versioned Chip Endorsement Key)**: Selected via `RootKeySelect=0` in `SNP_DERIVE_KEY`
- **VMRK (VM Root Key)**: VM-specific key for migration scenarios; selected via `RootKeySelect=1`

AMD uses the name VCEK for two distinct roles: the asymmetric key that signs attestation reports,
and as the name for `RootKeySelect=0` in `SNP_DERIVE_KEY`. These are different uses of the same
underlying key material. The `snpguest` CLI follows AMD's naming directly.

The output of `SNP_DERIVE_KEY` is a symmetric secret returned to the guest. Whether to call it
a "key" or a "seed" is somewhat in the eye of the beholder: the firmware does not use it
internally for encryption, decryption, or authentication — it is simply derived and handed to
the guest, which then uses it as a key for its own purposes. AMD and `snpguest` call it a derived
key, reflecting its intended use.

VMRK is used in live migration to protect VM state across hosts, meaning the firmware may use it
internally for encryption or authentication — which places it more firmly in the "key" category.

## Test Architecture

The test suite is implemented in Python and follows the same structure as the attestation tests:

```
key-derivation/
├── README.md
└── mkosi.extra/
└── usr/local/lib/
├── scripts/
│ └── snpguest_key_derivation.py # Python test implementation
└── systemd/system/
└── key-derivation.service # Systemd service unit
```

## Running the Tests

### Prerequisites

The tests can only run inside an SNP-enabled guest VM with:
- `/dev/sev-guest` device available
- `snpguest` tool installed
- Python 3.x available

### Execution

The tests run automatically via systemd service after boot. Manual execution:

```bash
# Run the test suite (pass/fail output only)
/usr/local/lib/scripts/snpguest_key_derivation.py

# Run with verbose output (snpguest commands, key hex values, full attestation report)
/usr/local/lib/scripts/snpguest_key_derivation.py --debug

# Derive a key for every valid GFS value (0x00-0x7f) and report which produce distinct keys
/usr/local/lib/scripts/snpguest_key_derivation.py --gfs-sweep

# View test results
journalctl -u key-derivation.service

# Check test status log
cat /usr/local/lib/key_derivation_status
```

### Test Output

Each test produces:
- Status log in JSON format: `/usr/local/lib/key_derivation_status`
- Derived keys stored in: `/usr/local/lib/key_derivation_service/`
- Console output with pass/fail status (key hex values shown only with `--debug`)

### Expected Output

What follows is an example of a successful test run (default, no `--debug`):

```
======================================================================
ATTESTATION REPORT (reference values for key derivation bounds)
======================================================================
Guest SVN: 1 (upper bound for --guest_svn)
Current TCB: bl=0x07 tee=0x00 snp=0x0b mc=0x16
Committed TCB: bl=0x07 tee=0x00 snp=0x0b mc=0x16 (upper bound per component for --tcb_version)
Reported TCB: bl=0x07 tee=0x00 snp=0x0b mc=0x16
Launch TCB: bl=0x07 tee=0x00 snp=0x0b mc=0x16

======================================================================
TEST: Determinism
======================================================================
✓ PASS: Keys match (deterministic)

======================================================================
TEST: VMPL Isolation
======================================================================
✓ PASS: VMPL0 and VMPL1 keys differ (proper isolation)

======================================================================
TEST: Root Key Difference
======================================================================
✓ PASS: VCEK and VMRK keys differ

======================================================================
TEST: Guest SVN Sensitivity
======================================================================
Guest SVN upper bound: 1
Testing 2 SVN values: [0, 1]
✓ PASS: All 2 SVN values produce distinct keys

======================================================================
TEST: TCB Sensitivity
======================================================================
Committed TCB (upper bound per component): bl=0x07 tee=0x00 snp=0x0b mc=0x16
Testing 5 TCB candidate(s)
✓ PASS: All 5 TCB values produce distinct keys

======================================================================
TEST: Guest Field Select Sensitivity
======================================================================
✓ PASS: GFS=0x01 and GFS=0x02 keys differ

======================================================================
TEST SUMMARY
======================================================================
✓ PASS: Determinism
✓ PASS: VMPL Isolation
✓ PASS: Root Key Difference
✓ PASS: Guest SVN Sensitivity
✓ PASS: TCB Sensitivity
✓ PASS: Guest Field Select Sensitivity

Passed: 6/6

✓ All key derivation tests passed!
```

### N/A Cases

Some tests may report `✓ PASS: N/A` rather than a full result:

- **Guest SVN Sensitivity**: Reports N/A when the guest was launched without an ID block (`guest_svn=0` in the attestation report), or with an ID block that explicitly sets the Guest SVN value to zero. Either way, it leaves only one valid SVN value to test.
- **TCB Sensitivity**: Reports N/A when all committed TCB components are zero.
- **VMPL Isolation**: Reports N/A when VMPL1 key derivation fails (expected if not running at VMPL0 or VMPL1).

## Implementation Notes

### Python vs Bash

Unlike the attestation tests which use bash, this module uses Python for:
- Better error handling and structured output
- Type safety and code clarity
- Easier maintenance and extension
- Native JSON handling for status logs

### Attestation Report at Startup

Before running tests, the script fetches an attestation report to extract the guest SVN and committed TCB values. These provide the valid upper bounds for the SVN sensitivity and TCB sensitivity tests respectively, avoiding firmware rejections from out-of-range parameter values.

### VMPL Constraints

The VMPL isolation test may produce warnings if running at VMPL > 0, as the firmware enforces that derived keys can only be requested for VMPL values ≥ current VMPL. This is expected behavior and validates the security constraint.

### Key Reading

Derived keys are read directly from the output file bytes (`.read_bytes().hex()`). Key hex values are only printed when `--debug` is active.

Loading
Loading