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
19 changes: 19 additions & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,22 @@ jobs:
- name: Build darwin CLI
run: |
nix build ".#${SET}.contrast.cli"

sev-snp-measure-consistency:
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
env:
SET: base
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup_nix
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
cachixToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Run sev-snp-measure-consistency
run: |
nix run ".#${SET}.scripts.sev-snp-measure-consistency"
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use (
./tools/igvm
./tools/imagepuller-benchmark
./tools/kernelconfig
./tools/sev-snp-measure-go
./tools/snp-id-block-generator
./tools/tdx-measure
// keep-sorted end
Expand Down
105 changes: 105 additions & 0 deletions internal/snp/gctx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Translated from the Python sev-snp-measure tool:
// Copyright 2022- IBM Inc. All rights reserved
// SPDX-License-Identifier: Apache-2.0

package snp

import (
"crypto/sha512"
"encoding/binary"
)

const (
ldSize = sha512.Size384 // 48 bytes
vmsaGPA = uint64(0xFFFFFFFFF000)
)

var zeros [ldSize]byte

// GCTX holds the SNP guest launch digest state.
type GCTX struct {
ld [ldSize]byte
}

// NewGCTX creates a new GCTX with an all-zero seed.
func NewGCTX() *GCTX {
return &GCTX{}
}

// NewGCTXWithSeed creates a new GCTX with the provided seed (must be ldSize bytes).
func NewGCTXWithSeed(seed []byte) *GCTX {
g := &GCTX{}
copy(g.ld[:], seed)
return g
}

// LD returns the current launch digest.
func (g *GCTX) LD() [ldSize]byte {
return g.ld
}

// update calls the PAGE_INFO hash update as defined in SNP spec 8.17.2 Table 67.
func (g *GCTX) update(pageType uint8, gpa uint64, contents [ldSize]byte) {
const pageInfoLen = 0x70
isIMI := uint8(0)
vmpl3Perms := uint8(0)
vmpl2Perms := uint8(0)
vmpl1Perms := uint8(0)

// Build the PAGE_INFO structure (112 bytes = 0x70)
var info [pageInfoLen]byte
copy(info[0:ldSize], g.ld[:])
copy(info[ldSize:ldSize*2], contents[:])
binary.LittleEndian.PutUint16(info[ldSize*2:], pageInfoLen)
info[ldSize*2+2] = pageType
info[ldSize*2+3] = isIMI
info[ldSize*2+4] = vmpl3Perms
info[ldSize*2+5] = vmpl2Perms
info[ldSize*2+6] = vmpl1Perms
info[ldSize*2+7] = 0
binary.LittleEndian.PutUint64(info[ldSize*2+8:], gpa)

g.ld = sha384(info[:])
}

// UpdateNormalPages measures len(data)/4096 normal pages starting at startGPA.
// data must be page-aligned.
func (g *GCTX) UpdateNormalPages(startGPA uint64, data []byte) {
for offset := 0; offset < len(data); offset += 4096 {
page := data[offset : offset+4096]
g.update(0x01, startGPA+uint64(offset), sha384(page))
}
}

// UpdateVMSAPage measures a single VMSA page.
func (g *GCTX) UpdateVMSAPage(data [4096]byte) {
g.update(0x02, vmsaGPA, sha384(data[:]))
}

// UpdateZeroPages measures length/4096 zero pages starting at gpa.
func (g *GCTX) UpdateZeroPages(gpa uint64, lengthBytes int) {
for offset := 0; offset < lengthBytes; offset += 4096 {
g.update(0x03, gpa+uint64(offset), zeros)
}
}

// UpdateUnmeasuredPages measures length/4096 unmeasured pages starting at gpa.
func (g *GCTX) UpdateUnmeasuredPages(gpa uint64, lengthBytes int) {
for offset := 0; offset < lengthBytes; offset += 4096 {
g.update(0x04, gpa+uint64(offset), zeros)
}
}

// UpdateSecretsPage measures a secrets page at gpa.
func (g *GCTX) UpdateSecretsPage(gpa uint64) {
g.update(0x05, gpa, zeros)
}

// UpdateCPUIDPage measures a CPUID page at gpa.
func (g *GCTX) UpdateCPUIDPage(gpa uint64) {
g.update(0x06, gpa, zeros)
}

func sha384(data []byte) [ldSize]byte {
return sha512.Sum384(data)
}
116 changes: 116 additions & 0 deletions internal/snp/guest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Translated from the Python sev-snp-measure tool:
// Copyright 2022- IBM Inc. All rights reserved
// SPDX-License-Identifier: Apache-2.0

// Package snp implements AMD SEV-SNP launch measurement calculation.
// Only the SNP mode required by the Kata calculateSnpLaunchDigest nix package
// is implemented (--mode snp, VMM type QEMU).
package snp

import "fmt"

// CalcSNPLaunchDigest calculates the SEV-SNP launch measurement digest.
//
// - ovmfPath: path to the OVMF firmware binary
// - vcpus: number of guest vCPUs
// - vcpuSig: CPUID signature (e.g. from CPUSigs["EPYC-Milan"])
// - kernel: path to kernel bzImage (empty string to skip)
// - initrd: path to initrd (empty string to skip)
// - append: kernel command line string (empty string for none)
// - guestFeatures: guest feature flags (default 0x1)
func CalcSNPLaunchDigest(ovmfPath string, vcpus int, vcpuSig uint32,
kernel, initrd, appendStr string, guestFeatures uint64,
) ([ldSize]byte, error) {
ovmf, err := NewOVMF(ovmfPath)
if err != nil {
return [ldSize]byte{}, fmt.Errorf("parsing OVMF: %w", err)
}

gctx := NewGCTX()

// Measure the OVMF firmware pages.
data := ovmf.Data()
if len(data)%4096 != 0 {
return [ldSize]byte{}, fmt.Errorf("OVMF size %d is not page-aligned", len(data))
}
gctx.UpdateNormalPages(ovmf.GPA(), data)

// Measure metadata sections (zero pages, secrets, CPUID, kernel hashes).
var sevHashes *SevHashes
if kernel != "" {
sevHashes, err = NewSevHashes(kernel, initrd, appendStr)
if err != nil {
return [ldSize]byte{}, fmt.Errorf("computing kernel hashes: %w", err)
}
}
if err := updateMetadataPages(gctx, ovmf, sevHashes); err != nil {
return [ldSize]byte{}, err
}

// Measure VMSA pages (one per vCPU).
apEIP, err := ovmf.SEVESResetEIP()
if err != nil {
return [ldSize]byte{}, fmt.Errorf("reading OVMF reset EIP: %w", err)
}
for _, page := range VMSAPages(vcpus, apEIP, guestFeatures, vcpuSig) {
gctx.UpdateVMSAPage(page)
}

return gctx.LD(), nil
}

// updateMetadataPages processes each OVMF SEV metadata section in order,
// matching the logic in guest.py snp_update_metadata_pages for VMM type QEMU.
func updateMetadataPages(gctx *GCTX, ovmf *OVMF, sevHashes *SevHashes) error {
for _, desc := range ovmf.MetadataItems() {
if err := updateSection(gctx, ovmf, desc, sevHashes); err != nil {
return err
}
}

if sevHashes != nil && !ovmf.HasMetadataSection(SectionTypeSNPKernelHash) {
return fmt.Errorf("kernel specified but OVMF metadata doesn't include SNP_KERNEL_HASHES section")
}
return nil
}

// updateSection handles a single metadata section entry (QEMU VMM type).
func updateSection(gctx *GCTX, ovmf *OVMF, desc MetadataSection, sevHashes *SevHashes) error {
gpa := uint64(desc.GPA)
size := int(desc.Size)

switch desc.Type {
case SectionTypeSNPSecMem:
// QEMU uses zero pages (GCE uses unmeasured, but we only support QEMU).
gctx.UpdateZeroPages(gpa, size)

case SectionTypeSNPSecrets:
gctx.UpdateSecretsPage(gpa)

case SectionTypeCPUID:
gctx.UpdateCPUIDPage(gpa)

case SectionTypeSNPKernelHash:
if sevHashes != nil {
hashGPA, err := ovmf.SEVHashesTableGPA()
if err != nil {
return fmt.Errorf("getting SEV hashes table GPA: %w", err)
}
offsetInPage := hashGPA & 0xfff
page := sevHashes.ConstructPage(offsetInPage)
if size != 4096 {
return fmt.Errorf("SNP_KERNEL_HASHES section size %d != 4096", size)
}
gctx.UpdateNormalPages(gpa, page[:])
} else {
gctx.UpdateZeroPages(gpa, size)
}

case SectionTypeSVSMCAA:
gctx.UpdateZeroPages(gpa, size)

default:
return fmt.Errorf("unknown OVMF metadata section type 0x%x", uint32(desc.Type))
}
return nil
}
Loading
Loading