Fetching AMD SEV Reports (With Zig)

In an ordinary VM, the hypervisor can read the guest’s memory. AMD SEV, Secure Encrypted Virtualization, encrypts that memory with a per-VM key the hypervisor does not receive. SEV-ES extends the protection to saved CPU register state.

SEV-SNP, Secure Nested Paging, adds checks on memory ownership and mappings to prevent a malicious hypervisor from substituting or remapping private pages. This protects the guest from the host; it doesn’t make software inside the guest trustworthy or prevent the host from stopping the VM.

SNP lets the guest request a report signed by the AMD secure processor, providing evidence of the VM’s launch state and configuration. It contains the launch measurement, guest policy, platform security versions and 64 bytes of caller-supplied REPORT_DATA.

Before the VM starts, the host loads its starting software into memory. This includes the guest’s boot firmware: the startup software that initializes the VM’s boot environment and hands control to the operating system’s bootloader. On Google Cloud, that’s Google-managed UEFI firmware based on Open Virtual Machine Firmware (OVMF).

The starting software can also include Coconut SVSM, a separate program that hosts services such as a virtual TPM. The UEFI firmware still boots the OS; SVSM provides services that need protection from the OS itself.

AMD’s SNP firmware runs on the secure processor. The host starts a launch with SNP_LAUNCH_START, then supplies the starting image in chunks called memory pages through SNP_LAUNCH_UPDATE. For each normal code or data page, the firmware hashes the bytes it installs, their guest-memory address, page type and permissions, together with the previous hash. AMD’s firmware maintains this running hash in protected state; the host cannot supply a replacement value.

SNP_LAUNCH_FINISH closes the launch. The final SHA-384 hash is the launch measurement, a fingerprint of that initial image, including SVSM when present. Later, the firmware copies this stored value into the report. It stays fixed as the VM runs.

When performing remote attestation, a Verifier compares it with the expected fingerprint for the starting software and launch configuration it trusts. Google publishes signed launch endorsements containing those expected measurements for its firmware. Changing the firmware bytes changes the fingerprint. Marking those pages as unmeasured skips hashing their contents, but changes the page type included in the hash. Either way, the result differs from the expected fingerprint, so the Verifier can reject the attestation.

The kernel and applications that the firmware loads afterwards are not automatically covered by this fingerprint.

retrieving a report on Linux

Linux exposes the SNP guest interface at /dev/sev-guest. An application sends the SNP_GET_REPORT ioctl, and the driver handles the protected exchange with AMD SNP firmware.

Alongside the report data, the request supplies a VM privilege level, or VMPL: 0 through 3, with 0 most privileged. These levels matter because Linux isn’t always the most privileged software inside the VM. Coconut SVSM, for example, can use that separation to host a virtual TPM whose keys are isolated from the guest kernel. Being root in Linux doesn’t put you at VMPL0. Use the VMPL supported by your guest.

Here’s the request in Zig (0.17.0), using the layouts from Linux’s guest API. fd is an open /dev/sev-guest descriptor inside an x86-64 Linux SNP guest:

const std = @import("std");
const linux = std.os.linux;

const ReportRequest = extern struct {
    user_data: [64]u8,
    vmpl: u32,
    reserved: [28]u8 = @splat(0),
};

const GuestRequest = extern struct {
    msg_version: u8 = 1,
    padding: [7]u8 = @splat(0),
    req_data: u64,
    resp_data: u64,
    exitinfo2: u64 = 0,
};

pub fn fetchReport(fd: i32, challenge: [64]u8, vmpl: u32) ![1184]u8 {
    var request: ReportRequest = .{ .user_data = challenge, .vmpl = vmpl };
    var response: [4000]u8 = @splat(0);
    var call: GuestRequest = .{
        .req_data = @intFromPtr(&request),
        .resp_data = @intFromPtr(&response),
    };

    const SNP_GET_REPORT = 0xc0205300;
    const result = linux.ioctl(fd, SNP_GET_REPORT, @intFromPtr(&call));
    if (linux.errno(result) != .SUCCESS or call.exitinfo2 != 0) {
        return error.ReportRequestFailed;
    }

    const status = std.mem.readInt(u32, response[0..4], .little);
    const size = std.mem.readInt(u32, response[4..8], .little);
    if (status != 0 or size != 1184) return error.InvalidResponse;

    // The report follows the 32-byte firmware response header.
    return response[32..][0..1184].*;
}

Pass a fresh 64-byte challenge from the verifier and the guest’s VMPL. The challenge comes back in REPORT_DATA, so the verifier can reject an old report. The result is a raw report; its signature and policy still need checking.

I packaged the retrieval, report parsing and verification in zig-sev-guest, so callers don’t have to work with these buffers directly.

retrieving reports through Coconut SVSM and OpenHCL

Coconut SVSM and Microsoft’s OpenHCL have different designs, but both run privileged services such as a vTPM inside the confidential VM. On SNP, AMD hardware encrypts that memory—including the TPM’s private keys—with a per-VM key managed by the secure processor. The guest kernel runs at a less privileged VMPL than SVSM or OpenHCL. SNP’s page permissions block it from reading the pages holding the vTPM’s private keys.

The guest can ask AMD to sign a report containing a hash of any public key. That does not prove the key belongs to the protected vTPM. SVSM and OpenHCL build the request using the public keys of the TPM they host. The application therefore obtains this report through them.

Application/dev/sev-guestLinux configfsvTPM interfaceCoconut SVSMOpenHCLSNP firmware
Three request paths, all backed by SNP firmware.

checking the evidence

In zig-sev-guest, I use OpenSSL to verify the signing certificate’s chain against the embedded AMD roots and check the report’s ECDSA P-384 signature. The certificate’s security versions, and chip identity where present, must also match the report.

Policy validation then compares the report with the expected launch measurement, REPORT_DATA, VMPL and minimum platform security versions. Debugging, SMT and migration are rejected unless explicitly allowed.

verify.andValidate performs both steps on the same report. The caller supplies the expected values, including the fresh challenge, and checks any vTPM-specific claims and quotes separately.