{"article":{"slug":"fetching-amd-sev-reports-with-zig","title":"Fetching AMD SEV Reports (With Zig)","subtitle":null,"summary":"A walkthrough of AMD SEV-SNP attestation: what confidential VMs protect against, how to retrieve a signed attestation report on Linux and through Coconut SVSM and OpenHCL, and how to verify its certificate chain and validate it against expected policy using a Zig library.","content_type":"tutorial","language":"en","canonical_url":"https://bschaatsbergen.com/posts/zig-sev-guest/","author":{"name":"Bruno Schaatsbergen","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Bruno Schaatsbergen","url":"https://bschaatsbergen.com/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":969,"reading_minutes":4,"published_at":"2026-10-10T22:00:00.000Z","added_at":"2026-10-11T23:10:13.323Z","updated_at":"2026-10-11T23:10:13.323Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/fetching-amd-sev-reports-with-zig","markdown_url":"https://listedarticles.com/articles/fetching-amd-sev-reports-with-zig.md","example":false,"citation":"Bruno Schaatsbergen, Bruno Schaatsbergen. \"Fetching AMD SEV Reports (With Zig).\" 10 Oct 2026. https://bschaatsbergen.com/posts/zig-sev-guest/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://bschaatsbergen.com/posts/zig-sev-guest/"},"body_markdown":"In an ordinary VM, the hypervisor can read the guest’s memory.\n[AMD SEV](https://www.amd.com/en/developer/sev.html), Secure Encrypted\nVirtualization, encrypts that memory with a per-VM key the hypervisor does\nnot receive. SEV-ES extends the protection to saved CPU register state.\n\nSEV-SNP, Secure Nested Paging, adds checks on memory ownership and mappings\nto prevent a malicious hypervisor from substituting or remapping private\npages. This protects the guest from the host; it doesn’t make software\ninside the guest trustworthy or prevent the host from stopping the VM.\n\nSNP lets the guest request a report signed by the AMD secure processor,\nproviding evidence of the VM’s launch state and configuration. It contains\nthe launch measurement, guest policy, platform security versions and 64\nbytes of caller-supplied `REPORT_DATA`.\n\nBefore the VM starts, the host loads its starting software into memory.\nThis includes the guest’s boot firmware: the startup software that initializes\nthe VM’s boot environment and hands control to the operating system’s\nbootloader. On Google Cloud,\nthat’s [Google-managed UEFI firmware](https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/verify-firmware)\nbased on [Open Virtual Machine Firmware (OVMF)](https://www.tianocore.org/tianocore-wiki.github.io/platforms-packages/platform-ports/ovmf.html).\n\nThe starting software can also include\n[Coconut SVSM](https://github.com/coconut-svsm/svsm), a separate program that\nhosts services such as a virtual TPM. The UEFI firmware still boots the OS;\nSVSM provides services that need protection from the OS itself.\n\nAMD’s SNP firmware runs on the secure processor. The host starts a launch\nwith `SNP_LAUNCH_START`, then supplies the starting image in chunks called\nmemory pages through\n[`SNP_LAUNCH_UPDATE`](https://docs.kernel.org/virt/kvm/x86/amd-memory-encryption.html#kvm-sev-snp-launch-update).\nFor each normal code or data page, the firmware hashes the bytes it installs,\ntheir guest-memory address, page type and permissions, together with the\nprevious hash. AMD’s firmware maintains this running hash in protected\nstate; the host cannot supply a replacement value.\n\n`SNP_LAUNCH_FINISH` closes the launch. The final SHA-384 hash is the\n**launch measurement**, a fingerprint of that initial image, including\nSVSM when present. Later, the firmware\n[copies this stored value into the report](https://github.com/amd/AMD-ASPFW/blob/3ca6650dd35d878b3fcbe5c7f58b145eed042bbf/fw/psp_bl_uapps/sev_uapp/src/sev_mcmd.c#L5677-L5679).\nIt stays fixed as the VM runs.\n\nWhen performing [remote attestation](https://www.rfc-editor.org/rfc/rfc9334.html),\na Verifier compares it with the expected fingerprint for the starting\nsoftware and launch configuration it trusts. Google publishes\n[signed launch endorsements](https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/verify-firmware#retrieve)\ncontaining those expected measurements for its firmware. Changing the\nfirmware bytes changes the fingerprint. Marking those pages as *unmeasured*\nskips hashing their contents, but changes the page type included in the hash.\nEither way, the result differs from the expected fingerprint, so the\nVerifier can reject the attestation.\n\nThe kernel and applications that the firmware loads afterwards are not\nautomatically covered by this fingerprint.\n\n## retrieving a report on Linux\n\nLinux exposes the [SNP guest interface](https://docs.kernel.org/virt/coco/sev-guest.html)\nat `/dev/sev-guest`. An application sends the `SNP_GET_REPORT` ioctl, and\nthe driver handles the protected exchange with AMD SNP firmware.\n\nAlongside the report data, the request supplies a VM privilege level, or\nVMPL: 0 through 3, with 0 most privileged. These levels matter because\nLinux isn’t always the most privileged software inside the VM. Coconut\nSVSM, for example, can use that separation to host a virtual TPM whose\nkeys are isolated from the guest kernel. Being root in Linux doesn’t put\nyou at VMPL0. Use the VMPL supported by your guest.\n\nHere’s the request in Zig (0.17.0), using the layouts from Linux’s guest\nAPI. `fd` is an open `/dev/sev-guest` descriptor inside an x86-64 Linux SNP\nguest:\n\n```\nconst std = @import(\"std\");\nconst linux = std.os.linux;\n\nconst ReportRequest = extern struct {\n    user_data: [64]u8,\n    vmpl: u32,\n    reserved: [28]u8 = @splat(0),\n};\n\nconst GuestRequest = extern struct {\n    msg_version: u8 = 1,\n    padding: [7]u8 = @splat(0),\n    req_data: u64,\n    resp_data: u64,\n    exitinfo2: u64 = 0,\n};\n\npub fn fetchReport(fd: i32, challenge: [64]u8, vmpl: u32) ![1184]u8 {\n    var request: ReportRequest = .{ .user_data = challenge, .vmpl = vmpl };\n    var response: [4000]u8 = @splat(0);\n    var call: GuestRequest = .{\n        .req_data = @intFromPtr(&request),\n        .resp_data = @intFromPtr(&response),\n    };\n\n    const SNP_GET_REPORT = 0xc0205300;\n    const result = linux.ioctl(fd, SNP_GET_REPORT, @intFromPtr(&call));\n    if (linux.errno(result) != .SUCCESS or call.exitinfo2 != 0) {\n        return error.ReportRequestFailed;\n    }\n\n    const status = std.mem.readInt(u32, response[0..4], .little);\n    const size = std.mem.readInt(u32, response[4..8], .little);\n    if (status != 0 or size != 1184) return error.InvalidResponse;\n\n    // The report follows the 32-byte firmware response header.\n    return response[32..][0..1184].*;\n}\n```\n\nPass a fresh 64-byte challenge from the verifier and the guest’s VMPL.\nThe challenge comes back in `REPORT_DATA`, so the verifier can reject an\nold report. The result is a raw report; its signature and policy still\nneed checking.\n\nI packaged the retrieval, report parsing and verification in\n[zig-sev-guest](https://github.com/bschaatsbergen/zig-sev-guest), so callers\ndon’t have to work with these buffers directly.\n\n## retrieving reports through Coconut SVSM and OpenHCL\n\n[Coconut SVSM](https://github.com/coconut-svsm/svsm) and\n[Microsoft’s OpenHCL](https://openvmm.dev/guide/reference/architecture/openhcl.html)\nhave different designs, but both run privileged services such as a vTPM\ninside the confidential VM. On SNP, AMD hardware encrypts that memory—including\nthe TPM’s private keys—with a per-VM key managed by the secure processor.\nThe guest kernel runs at a less privileged VMPL than SVSM or OpenHCL.\nSNP’s page permissions block it from reading the pages holding the vTPM’s\nprivate keys.\n\nThe guest can ask AMD to sign a report containing a hash of any public key.\nThat does not prove the key belongs to the protected vTPM. SVSM and OpenHCL\nbuild the request using the public keys of the TPM they host. The\napplication therefore obtains this report through them.\n\nThree request paths, all backed by SNP firmware.\n\n## checking the evidence\n\nIn [zig-sev-guest](https://github.com/bschaatsbergen/zig-sev-guest), I use OpenSSL to verify the signing certificate’s chain\nagainst the embedded AMD roots and check the report’s ECDSA P-384 signature.\nThe certificate’s security versions, and chip identity where present, must\nalso match the report.\n\nPolicy validation then compares the report with the expected launch\nmeasurement, `REPORT_DATA`, VMPL and minimum platform security versions.\nDebugging, SMT and migration are rejected unless explicitly allowed.\n\n`verify.andValidate` performs both steps on the same report. The caller\nsupplies the expected values, including the fresh challenge, and checks any\nvTPM-specific claims and quotes separately.","body_html":"<p>In an ordinary VM, the hypervisor can read the guest’s memory.\n<a href=\"https://www.amd.com/en/developer/sev.html\" rel=\"nofollow ugc noopener\">AMD SEV</a>, Secure Encrypted\nVirtualization, encrypts that memory with a per-VM key the hypervisor does\nnot receive. SEV-ES extends the protection to saved CPU register state.</p>\n<p>SEV-SNP, Secure Nested Paging, adds checks on memory ownership and mappings\nto prevent a malicious hypervisor from substituting or remapping private\npages. This protects the guest from the host; it doesn’t make software\ninside the guest trustworthy or prevent the host from stopping the VM.</p>\n<p>SNP lets the guest request a report signed by the AMD secure processor,\nproviding evidence of the VM’s launch state and configuration. It contains\nthe launch measurement, guest policy, platform security versions and 64\nbytes of caller-supplied <code>REPORT_DATA</code>.</p>\n<p>Before the VM starts, the host loads its starting software into memory.\nThis includes the guest’s boot firmware: the startup software that initializes\nthe VM’s boot environment and hands control to the operating system’s\nbootloader. On Google Cloud,\nthat’s <a href=\"https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/verify-firmware\" rel=\"nofollow ugc noopener\">Google-managed UEFI firmware</a>\nbased on <a href=\"https://www.tianocore.org/tianocore-wiki.github.io/platforms-packages/platform-ports/ovmf.html\" rel=\"nofollow ugc noopener\">Open Virtual Machine Firmware (OVMF)</a>.</p>\n<p>The starting software can also include\n<a href=\"https://github.com/coconut-svsm/svsm\" rel=\"nofollow ugc noopener\">Coconut SVSM</a>, a separate program that\nhosts services such as a virtual TPM. The UEFI firmware still boots the OS;\nSVSM provides services that need protection from the OS itself.</p>\n<p>AMD’s SNP firmware runs on the secure processor. The host starts a launch\nwith <code>SNP_LAUNCH_START</code>, then supplies the starting image in chunks called\nmemory pages through\n<a href=\"https://docs.kernel.org/virt/kvm/x86/amd-memory-encryption.html#kvm-sev-snp-launch-update\" rel=\"nofollow ugc noopener\"><code>SNP_LAUNCH_UPDATE</code></a>.\nFor each normal code or data page, the firmware hashes the bytes it installs,\ntheir guest-memory address, page type and permissions, together with the\nprevious hash. AMD’s firmware maintains this running hash in protected\nstate; the host cannot supply a replacement value.</p>\n<p><code>SNP_LAUNCH_FINISH</code> closes the launch. The final SHA-384 hash is the\n<strong>launch measurement</strong>, a fingerprint of that initial image, including\nSVSM when present. Later, the firmware\n<a href=\"https://github.com/amd/AMD-ASPFW/blob/3ca6650dd35d878b3fcbe5c7f58b145eed042bbf/fw/psp_bl_uapps/sev_uapp/src/sev_mcmd.c#L5677-L5679\" rel=\"nofollow ugc noopener\">copies this stored value into the report</a>.\nIt stays fixed as the VM runs.</p>\n<p>When performing <a href=\"https://www.rfc-editor.org/rfc/rfc9334.html\" rel=\"nofollow ugc noopener\">remote attestation</a>,\na Verifier compares it with the expected fingerprint for the starting\nsoftware and launch configuration it trusts. Google publishes\n<a href=\"https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/verify-firmware#retrieve\" rel=\"nofollow ugc noopener\">signed launch endorsements</a>\ncontaining those expected measurements for its firmware. Changing the\nfirmware bytes changes the fingerprint. Marking those pages as <em>unmeasured</em>\nskips hashing their contents, but changes the page type included in the hash.\nEither way, the result differs from the expected fingerprint, so the\nVerifier can reject the attestation.</p>\n<p>The kernel and applications that the firmware loads afterwards are not\nautomatically covered by this fingerprint.</p>\n<h2 id=\"retrieving-a-report-on-linux\">retrieving a report on Linux</h2>\n<p>Linux exposes the <a href=\"https://docs.kernel.org/virt/coco/sev-guest.html\" rel=\"nofollow ugc noopener\">SNP guest interface</a>\nat <code>/dev/sev-guest</code>. An application sends the <code>SNP_GET_REPORT</code> ioctl, and\nthe driver handles the protected exchange with AMD SNP firmware.</p>\n<p>Alongside the report data, the request supplies a VM privilege level, or\nVMPL: 0 through 3, with 0 most privileged. These levels matter because\nLinux isn’t always the most privileged software inside the VM. Coconut\nSVSM, for example, can use that separation to host a virtual TPM whose\nkeys are isolated from the guest kernel. Being root in Linux doesn’t put\nyou at VMPL0. Use the VMPL supported by your guest.</p>\n<p>Here’s the request in Zig (0.17.0), using the layouts from Linux’s guest\nAPI. <code>fd</code> is an open <code>/dev/sev-guest</code> descriptor inside an x86-64 Linux SNP\nguest:</p>\n<pre><code>const std = @import(&quot;std&quot;);\nconst linux = std.os.linux;\n\nconst ReportRequest = extern struct {\n    user_data: [64]u8,\n    vmpl: u32,\n    reserved: [28]u8 = @splat(0),\n};\n\nconst GuestRequest = extern struct {\n    msg_version: u8 = 1,\n    padding: [7]u8 = @splat(0),\n    req_data: u64,\n    resp_data: u64,\n    exitinfo2: u64 = 0,\n};\n\npub fn fetchReport(fd: i32, challenge: [64]u8, vmpl: u32) ![1184]u8 {\n    var request: ReportRequest = .{ .user_data = challenge, .vmpl = vmpl };\n    var response: [4000]u8 = @splat(0);\n    var call: GuestRequest = .{\n        .req_data = @intFromPtr(&amp;request),\n        .resp_data = @intFromPtr(&amp;response),\n    };\n\n    const SNP_GET_REPORT = 0xc0205300;\n    const result = linux.ioctl(fd, SNP_GET_REPORT, @intFromPtr(&amp;call));\n    if (linux.errno(result) != .SUCCESS or call.exitinfo2 != 0) {\n        return error.ReportRequestFailed;\n    }\n\n    const status = std.mem.readInt(u32, response[0..4], .little);\n    const size = std.mem.readInt(u32, response[4..8], .little);\n    if (status != 0 or size != 1184) return error.InvalidResponse;\n\n    // The report follows the 32-byte firmware response header.\n    return response[32..][0..1184].*;\n}</code></pre>\n<p>Pass a fresh 64-byte challenge from the verifier and the guest’s VMPL.\nThe challenge comes back in <code>REPORT_DATA</code>, so the verifier can reject an\nold report. The result is a raw report; its signature and policy still\nneed checking.</p>\n<p>I packaged the retrieval, report parsing and verification in\n<a href=\"https://github.com/bschaatsbergen/zig-sev-guest\" rel=\"nofollow ugc noopener\">zig-sev-guest</a>, so callers\ndon’t have to work with these buffers directly.</p>\n<h2 id=\"retrieving-reports-through-coconut-svsm-and-openhcl\">retrieving reports through Coconut SVSM and OpenHCL</h2>\n<p><a href=\"https://github.com/coconut-svsm/svsm\" rel=\"nofollow ugc noopener\">Coconut SVSM</a> and\n<a href=\"https://openvmm.dev/guide/reference/architecture/openhcl.html\" rel=\"nofollow ugc noopener\">Microsoft’s OpenHCL</a>\nhave different designs, but both run privileged services such as a vTPM\ninside the confidential VM. On SNP, AMD hardware encrypts that memory—including\nthe TPM’s private keys—with a per-VM key managed by the secure processor.\nThe guest kernel runs at a less privileged VMPL than SVSM or OpenHCL.\nSNP’s page permissions block it from reading the pages holding the vTPM’s\nprivate keys.</p>\n<p>The guest can ask AMD to sign a report containing a hash of any public key.\nThat does not prove the key belongs to the protected vTPM. SVSM and OpenHCL\nbuild the request using the public keys of the TPM they host. The\napplication therefore obtains this report through them.</p>\n<p>Three request paths, all backed by SNP firmware.</p>\n<h2 id=\"checking-the-evidence\">checking the evidence</h2>\n<p>In <a href=\"https://github.com/bschaatsbergen/zig-sev-guest\" rel=\"nofollow ugc noopener\">zig-sev-guest</a>, I use OpenSSL to verify the signing certificate’s chain\nagainst the embedded AMD roots and check the report’s ECDSA P-384 signature.\nThe certificate’s security versions, and chip identity where present, must\nalso match the report.</p>\n<p>Policy validation then compares the report with the expected launch\nmeasurement, <code>REPORT_DATA</code>, VMPL and minimum platform security versions.\nDebugging, SMT and migration are rejected unless explicitly allowed.</p>\n<p><code>verify.andValidate</code> performs both steps on the same report. The caller\nsupplies the expected values, including the fresh challenge, and checks any\nvTPM-specific claims and quotes separately.</p>","headings":[{"level":2,"text":"retrieving a report on Linux","id":"retrieving-a-report-on-linux"},{"level":2,"text":"retrieving reports through Coconut SVSM and OpenHCL","id":"retrieving-reports-through-coconut-svsm-and-openhcl"},{"level":2,"text":"checking the evidence","id":"checking-the-evidence"}]}}