Include ID block when launching guests#247
Conversation
There was a problem hiding this comment.
Pull request overview
Adds SEV-SNP ID block generation and launch integration so guests can be launched with a signed ID block containing measurement, policy, and metadata.
Changes:
- Adds a host-side
generate-id-blockmodule and service to patch/signid-block.b64. - Updates guest launch to pass
policy,id-block, andid-authto QEMU. - Adds a browser-based ID block template builder and related documentation.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tools/id-block-README.md |
Documents the ID block builder and default policy/template. |
tools/id-block-builder.html |
Adds browser UI for generating unsigned ID block templates. |
modules/report/host/display-guest-logs/.../display-guest-logs.sh |
Adds timeout messaging for guest test log display. |
modules/launch/host/mkosi.conf |
Includes the new generate-id-block module. |
modules/launch/host/launch-guest/.../launch-guest.service |
Orders guest launch after ID block generation. |
modules/launch/host/launch-guest/.../launch-guest.sh |
Builds QEMU command dynamically and adds ID block arguments. |
modules/launch/host/launch-guest/.../id-block.b64 |
Adds default unsigned ID block template. |
modules/launch/host/launch-done/.../launch-done.service |
Adds ID block generation to launch completion dependencies. |
modules/launch/host/generate-id-block/README.md |
Documents runtime ID block patching/signing. |
modules/launch/host/generate-id-block/.../generate-id-block.service |
Adds oneshot service for ID block generation. |
modules/launch/host/generate-id-block/.../generate_id_block.py |
Implements measurement patching, ECDSA signing, and ID auth generation. |
modules/launch/host/generate-id-block/mkosi.conf |
Adds cryptography dependency for host image. |
modules/build/guest/mkosi.conf |
Adds Python package to guest build configuration. |
.github/workflows/build-and-release.yml |
Adds release job dependency to build job. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function dl(content, name, type) { | ||
| const url = URL.createObjectURL(new Blob([content], {type})); | ||
| Object.assign(document.createElement('a'), {href:url, download:name}).click(); | ||
| URL.revokeObjectURL(url); | ||
| } |
| # Extract policy from id-block (bytes 88-95, LE u64) so LAUNCH_START and | ||
| # LAUNCH_FINISH see the same value; without this QEMU uses its own default. | ||
| POLICY=$(base64 -d "${ID_BLOCK_FILE}" | python3 -c \ | ||
| "import sys; d=sys.stdin.buffer.read(); print(hex(int.from_bytes(d[88:96],'little')))") | ||
| SEV_SNP_OBJECT="${SEV_SNP_OBJECT},policy=${POLICY},id-block=${ID_BLOCK_B64},id-auth=${ID_AUTH_B64}" |
|
|
||
| The most permissive valid policy on the test platform is bits 16+17 set, all | ||
| else clear. The tool defaults to this value (`00000000000b` in the 48-bit hex | ||
| input, yielding full policy `0x000000000000b0000`). |
| [Unit] | ||
| Description=Patch guest measurement into id-block.b64 | ||
| DefaultDependencies=no |
| needs: create-release | ||
| if: ${{ always() && (github.event_name == 'pull_request' || needs.create-release.result == 'success') }} |
| 1. Reads the guest measurement from `guest_measurement.txt` (48-byte SHA-384, | ||
| written as `0x<96 hex chars>`) | ||
| 2. Decodes the ID block template from `id-block.b64` (must be exactly 96 bytes) | ||
| 3. Patches bytes 0–47 (the `ld` field) with the actual guest measurement | ||
| 4. Generates an ephemeral P-384 key pair | ||
| 5. Signs the patched ID block with ECDSA-P384-SHA384 | ||
| 6. Writes the signed ID block back to `id-block.b64` | ||
| 7. Writes a 4096-byte `ID_AUTH_INFO` structure to `id-auth.b64` containing the | ||
| signature and the ephemeral public key (no author key) |
There was a problem hiding this comment.
This is a good approach. I don't mind keeping this approach but snpguest actually provides functionality to generate the id-block and the id-auth. Also allowing to pass the measurement, familiy id, svn and policy you want to load into the id-block.
| Browser-based tool for building unsigned ID block templates for SEV-SNP guest | ||
| launch. Opens locally — no server needed. |
There was a problem hiding this comment.
That's a really cool tool! Again I'm sorry I didn't let you know that snpguest could actually calculate it, and that will probably work best for testing, but I wonder if there's somewhere we can place this work.
| POLICY=$(base64 -d "${ID_BLOCK_FILE}" | python3 -c " | ||
| import sys | ||
| d = sys.stdin.buffer.read() | ||
| n = len(d) | ||
| if n != 96: | ||
| print(f'ERROR: id-block decoded to {n} bytes (expected 96)', file=sys.stderr) | ||
| sys.exit(1) | ||
| print(hex(int.from_bytes(d[88:96], 'little'))) | ||
| ") |
| # Read metadata from environment, falling back to defaults | ||
| family_id = os.environ.get("ID_BLOCK_FAMILY_ID", DEFAULT_FAMILY_ID) | ||
| image_id = os.environ.get("ID_BLOCK_IMAGE_ID", DEFAULT_IMAGE_ID) | ||
| guest_svn = os.environ.get("ID_BLOCK_GUEST_SVN", DEFAULT_GUEST_SVN) | ||
| policy = os.environ.get("ID_BLOCK_POLICY", DEFAULT_POLICY) |
| result = subprocess.run(cmd, capture_output=True, text=True) | ||
| # Temp files are removed when the TemporaryDirectory context exits |
| Environment=ID_BLOCK_FAMILY_ID=sev-certify-fam0 | ||
| Environment=ID_BLOCK_IMAGE_ID=sev-certify-img0 |
| # Metadata defaults (match the committed template values documented in DESIGN.md) | ||
| DEFAULT_FAMILY_ID = "sev-certify-fam0" | ||
| DEFAULT_IMAGE_ID = "sev-certify-img0" | ||
| DEFAULT_GUEST_SVN = "48" | ||
| DEFAULT_POLICY = "0xb0000" |
| Override any of these with a systemd drop-in (`systemctl edit generate-id-block`) | ||
| without modifying the script or service file. | ||
|
|
||
| For the rationale behind the default values see `DESIGN.md` in this workspace. |
| launch-done.service | ||
| ``` | ||
|
|
||
| If `calculate-measurement.service` is skipped (no AMDSEV OVMF), `guest_measurement.txt` is absent. `generate-id-block.service` detects this, exits 0, and `launch-guest.sh` launches without an ID block. |
|
|
||
| - **Additive principle**: ID block support must not cause failures that would not have occurred without it. This is why absent `guest_measurement.txt` causes exit 0 (not 1). | ||
| - **Ephemeral keys**: Deliberate — for benchmark/test use, not production attestation. The signature satisfies firmware's structural requirement, not external verifiability. | ||
| - See `../DESIGN.md` (workspace-level) for full rationale on policy bit choices and why the DEBUG bit (19) is set. |
| # If the measurement file doesn't exist, calculate-measurement.service | ||
| # was skipped (e.g. AMDSEV OVMF not present). Nothing to do. |
| guest_measurement_sha256sum=$(echo "${calculated_measurement_hex}" | sha256sum | cut -d ' ' -f 1 | xxd -r -p | base64) | ||
| dbg "Measurement (hex): ${calculated_measurement_hex}" | ||
| dbg "Measurement (sha256): ${guest_measurement_sha256sum}" | ||
|
|
||
| # Convert Measurement to the appropriate sha format to pass in as host data | ||
| calculated_measurement_hex=$(awk -F "0x" '{print $2}' "${MEASUREMENT_FILE}" ) | ||
| guest_measurement_sha256sum=$(echo "${calculated_measurement_hex}" | sha256sum | cut -d ' ' -f 1 | xxd -r -p | base64 ) | ||
| # Build sev-snp-guest object; append ID block args if files are present | ||
| SEV_SNP_OBJECT="sev-snp-guest,id=sev0,cbitpos=51,reduced-phys-bits=1,kernel-hashes=on,host-data=${guest_measurement_sha256sum}" | ||
| if [ -f "${ID_BLOCK_FILE}" ] && [ -f "${ID_AUTH_FILE}" ]; then | ||
| ID_BLOCK_B64=$(cat "${ID_BLOCK_FILE}") | ||
| ID_AUTH_B64=$(cat "${ID_AUTH_FILE}") |
| # Print prompt to tty1 | ||
| echo "Tests complete. System will reboot in $TIMEOUT seconds." > /dev/$TTY | ||
| echo "Press any key to cancel reboot and drop into root shell..." > /dev/$TTY | ||
| # echo "Tests complete. System will reboot in $TIMEOUT seconds." > /dev/$TTY | ||
| # echo "Press any key to cancel reboot and drop into root shell..." > /dev/$TTY | ||
|
|
||
| # Wait for keypress on tty1 | ||
| if read -t "$TIMEOUT" -n 1 < /dev/$TTY; then | ||
| echo "Key pressed — reboot cancelled." > /dev/$TTY | ||
| # if read -t "$TIMEOUT" -n 1 < /dev/$TTY; then | ||
| # echo "Key pressed — reboot cancelled." > /dev/$TTY |
| if [ -f "${ID_BLOCK_FILE}" ] && [ -f "${ID_AUTH_FILE}" ]; then | ||
| ID_BLOCK_B64=$(cat "${ID_BLOCK_FILE}") | ||
| ID_AUTH_B64=$(cat "${ID_AUTH_FILE}") |
| # ID block metadata — override any of these to change what is embedded in the | ||
| # signed block without modifying the script. Defaults match the benchmark | ||
| # platform configuration documented in DESIGN.md. |
| | `ID_BLOCK_FAMILY_ID` | `0000000000000000000000000000fad0` | 16-byte family ID (32 hex chars) | | ||
| | `ID_BLOCK_IMAGE_ID` | `0000000000000000000000000000aed0` | 16-byte image ID (32 hex chars) | | ||
| | `ID_BLOCK_GUEST_SVN` | `48` | Guest security version number | | ||
| | `ID_BLOCK_POLICY` | `0xb0000` | Guest policy flags (bits 16+17+19) | |
| Override any of these with a systemd drop-in (`systemctl edit generate-id-block`) | ||
| without modifying the script or service file. | ||
|
|
||
| For the rationale behind the default values see `DESIGN.md` in this workspace. |
| Metadata (family_id, image_id, guest_svn, policy) is read from environment | ||
| variables with defaults chosen for the benchmark test platform: | ||
| ID_BLOCK_FAMILY_ID — 32 hex chars (default: 0000000000000000000000000000fad0) | ||
| ID_BLOCK_IMAGE_ID — 32 hex chars (default: 0000000000000000000000000000aed0) | ||
| ID_BLOCK_GUEST_SVN — decimal integer (default: 48) | ||
| ID_BLOCK_POLICY — decimal or 0x-prefixed hex (default: 0xb0000) |
| | `ID_BLOCK_FAMILY_ID` | `0000000000000000000000000000fad0` | 32 hex chars | | ||
| | `ID_BLOCK_IMAGE_ID` | `0000000000000000000000000000aed0` | 32 hex chars | | ||
| | `ID_BLOCK_GUEST_SVN` | `48` | Decimal | | ||
| | `ID_BLOCK_POLICY` | `0xb0000` | Bits 16 (SMT), 17 (MBO), 19 (DEBUG) | |
|
|
||
| - **Additive principle**: ID block support must not cause failures that would not have occurred without it. This is why absent `guest_measurement.txt` causes exit 0 (not 1). | ||
| - **Ephemeral keys**: Deliberate — for benchmark/test use, not production attestation. The signature satisfies firmware's structural requirement, not external verifiability. | ||
| - See `../DESIGN.md` (workspace-level) for full rationale on policy bit choices and why the DEBUG bit (19) is set. |
| ID_BLOCK_FAMILY_ID — 32 hex chars (default: 0000000000000000000000000000fad0) | ||
| ID_BLOCK_IMAGE_ID — 32 hex chars (default: 0000000000000000000000000000aed0) |
| | `ID_BLOCK_FAMILY_ID` | `0000000000000000000000000000fad0` | 16-byte family ID (32 hex chars) | | ||
| | `ID_BLOCK_IMAGE_ID` | `0000000000000000000000000000aed0` | 16-byte image ID (32 hex chars) | |
| Override any of these with a systemd drop-in (`systemctl edit generate-id-block`) | ||
| without modifying the script or service file. | ||
|
|
||
| For the rationale behind the default values see `DESIGN.md` in this workspace. |
| # ID block metadata — override any of these to change what is embedded in the | ||
| # signed block without modifying the script. Defaults match the benchmark | ||
| # platform configuration documented in DESIGN.md. |
| - See `../DESIGN.md` (workspace-level) for full rationale on policy bit choices and why the DEBUG bit (19) is set. | ||
|
|
||
| ## Cross-Repo Context | ||
|
|
||
| This branch (`pr/id-block`) is part of a workspace at `~/code/git/features/id-block/` comparing this implementation against `virtee/snpguest` (in the sibling `virtee/` directory). The workspace `CLAUDE.md` and `DESIGN.md` explain the comparison goals and key differences. |
| @@ -1,5 +1,6 @@ | |||
| [Include] | |||
| Include=./guest-measurement | |||
| Include=./generate-id-block | |||
- add guest policy to QEMU command - add ID block service logs to certificates - check for presence of measurement file in new ID block service
Handle unmatched regex.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Policy extraction from id-block bytes 88-95 was done via a python3 -c heredoc. Replace with od/tr/printf: decode to a flat hex string, validate the 192-char length (96 bytes), slice bytes 88-95 at offset 176, reverse the 8 byte-pairs to convert little-endian to big-endian, then print with printf '0x%x'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds shared callables (calculate_measurement, generate_id_block) in sev_verify/id_block.py for use by any test that requires an ID block. Extends VMProfile with id_block/id_auth fields so vm_launch passes them to QEMU when present. Fixes the step loop in cli.py so callable steps can update ctx.profile before vm_launch sees it. Adds a derived-keys placeholder test at cert level 3.0.0-2 that demonstrates the ID block step pattern and will receive the actual derived key test steps in a follow-on PR.
| if profile.id_block and profile.id_auth: | ||
| parts.append(f"id-block={profile.id_block}") | ||
| parts.append(f"id-auth={profile.id_auth}") |
| id_block_b64 = id_block_file.read_text().strip() | ||
| id_auth_b64 = id_auth_file.read_text().strip() | ||
|
|
||
| ctx.profile = replace(ctx.profile, id_block=id_block_b64, id_auth=id_auth_b64) |
| ) | ||
|
|
||
| from .models import StepContext, StepHandlerResult | ||
| from .vm_profile import VMProfile, VMProfileError |
| | `ID_BLOCK_FAMILY_ID` | `0000000000000000000000000000fad0` | 16-byte family ID (32 hex chars) | | ||
| | `ID_BLOCK_IMAGE_ID` | `0000000000000000000000000000aed0` | 16-byte image ID (32 hex chars) | | ||
| | `ID_BLOCK_GUEST_SVN` | `48` | Guest security version number | | ||
| | `ID_BLOCK_POLICY` | `0xb0000` | Guest policy flags (bits 16+17+19) | |
| Metadata (family_id, image_id, guest_svn, policy) is read from environment | ||
| variables with defaults chosen for the benchmark test platform: | ||
| ID_BLOCK_FAMILY_ID — 32 hex chars (default: 0000000000000000000000000000fad0) | ||
| ID_BLOCK_IMAGE_ID — 32 hex chars (default: 0000000000000000000000000000aed0) | ||
| ID_BLOCK_GUEST_SVN — decimal integer (default: 48) | ||
| ID_BLOCK_POLICY — decimal or 0x-prefixed hex (default: 0xb0000) |
ID block includes expected measurement, guest policy, Family ID, Image ID, etc. The expected measurement must match the calculated measurement. Note that sev-certify has always calculated an expected measurement even though it's only now, with this PR, that an ID block is included when launching guests. Ignoring the ID block, sev-certify used the expected measurement as "host data". This is still the case.
ID blocks are signed, but they can be self-signed, which is what this PR does.
Finally, there's a simple tool that allows user specification of some of the fields in the ID block. Just open tools/id-block-builder.html in a browser.