diff --git a/src/runtime/pkg/sev/ovmf.go b/src/runtime/pkg/sev/ovmf.go new file mode 100644 index 0000000000..9c6947abef --- /dev/null +++ b/src/runtime/pkg/sev/ovmf.go @@ -0,0 +1,101 @@ +// Copyright contributors to AMD SEV/-ES in Go +// +// SPDX-License-Identifier: Apache-2.0 + +package sev + +import ( + "bytes" + "encoding/binary" + "errors" + "os" +) + +// GUID 96b582de-1fb2-45f7-baea-a366c55a082d +var ovmfTableFooterGuid = guidLE{0xde, 0x82, 0xb5, 0x96, 0xb2, 0x1f, 0xf7, 0x45, 0xba, 0xea, 0xa3, 0x66, 0xc5, 0x5a, 0x08, 0x2d} + +// GUID 00f771de-1a7e-4fcb-890e-68c77e2fb44e +var sevEsResetBlockGuid = guidLE{0xde, 0x71, 0xf7, 0x00, 0x7e, 0x1a, 0xcb, 0x4f, 0x89, 0x0e, 0x68, 0xc7, 0x7e, 0x2f, 0xb4, 0x4e} + +type ovmfFooterTableEntry struct { + Size uint16 + Guid guidLE +} + +type ovmf struct { + table map[guidLE][]byte +} + +func NewOvmf(filename string) (ovmf, error) { + buf, err := os.ReadFile(filename) + if err != nil { + return ovmf{}, err + } + table, err := parseFooterTable(buf) + if err != nil { + return ovmf{}, err + } + return ovmf{table}, nil +} + +// Parse the OVMF footer table and return a map from GUID to entry value +func parseFooterTable(data []byte) (map[guidLE][]byte, error) { + table := make(map[guidLE][]byte) + + buf := new(bytes.Buffer) + err := binary.Write(buf, binary.LittleEndian, ovmfFooterTableEntry{}) + if err != nil { + return table, err + } + entryHeaderSize := buf.Len() + + // The OVMF table ends 32 bytes before the end of the firmware binary + startOfFooterTable := len(data) - 32 - entryHeaderSize + footerBytes := bytes.NewReader(data[startOfFooterTable:]) + var footer ovmfFooterTableEntry + err = binary.Read(footerBytes, binary.LittleEndian, &footer) + if err != nil { + return table, err + } + if footer.Guid != ovmfTableFooterGuid { + // No OVMF footer table + return table, nil + } + tableSize := int(footer.Size) - entryHeaderSize + if tableSize < 0 { + return table, nil + } + tableBytes := data[(startOfFooterTable - tableSize):startOfFooterTable] + for len(tableBytes) >= entryHeaderSize { + tsize := len(tableBytes) + entryBytes := bytes.NewReader(tableBytes[tsize-entryHeaderSize:]) + var entry ovmfFooterTableEntry + err := binary.Read(entryBytes, binary.LittleEndian, &entry) + if err != nil { + return table, err + } + if int(entry.Size) < entryHeaderSize { + return table, errors.New("Invalid entry size") + } + entryData := tableBytes[tsize-int(entry.Size) : tsize-entryHeaderSize] + table[entry.Guid] = entryData + tableBytes = tableBytes[:tsize-int(entry.Size)] + } + return table, nil +} + +func (o *ovmf) tableItem(guid guidLE) ([]byte, error) { + value, ok := o.table[guid] + if !ok { + return []byte{}, errors.New("OVMF footer table entry not found") + } + return value, nil +} + +func (o *ovmf) sevEsResetEip() (uint32, error) { + value, err := o.tableItem(sevEsResetBlockGuid) + if err != nil { + return 0, err + } + return binary.LittleEndian.Uint32(value), nil +} diff --git a/src/runtime/pkg/sev/sev.go b/src/runtime/pkg/sev/sev.go index 0b84abaa97..22a1e9e1f9 100644 --- a/src/runtime/pkg/sev/sev.go +++ b/src/runtime/pkg/sev/sev.go @@ -138,8 +138,8 @@ func constructSevHashesTable(kernelPath, initrdPath, cmdline string) ([]byte, er return htBuf.Bytes(), nil } -// CalculateLaunchDigest returns the sha256 encoded launch digest based -// off the current firmware, kernel, and initrd images, and the kernel cmdline +// CalculateLaunchDigest returns the sha256 encoded SEV launch digest based off +// the current firmware, kernel, initrd, and the kernel cmdline func CalculateLaunchDigest(firmwarePath, kernelPath, initrdPath, cmdline string) (res [sha256.Size]byte, err error) { f, err := os.Open(firmwarePath) if err != nil { @@ -152,6 +152,10 @@ func CalculateLaunchDigest(firmwarePath, kernelPath, initrdPath, cmdline string) return res, err } + // When used for confidential containers in kata-containers, kernelPath + // is always set (direct boot). However, this current package can also + // be used by other programs which may calculate launch digests of + // arbitrary SEV guests without SEV kernel hashes table. if kernelPath != "" { ht, err := constructSevHashesTable(kernelPath, initrdPath, cmdline) if err != nil { @@ -163,3 +167,51 @@ func CalculateLaunchDigest(firmwarePath, kernelPath, initrdPath, cmdline string) copy(res[:], digest.Sum(nil)) return res, nil } + +// CalculateSEVESLaunchDigest returns the sha256 encoded SEV-ES launch digest +// based off the current firmware, kernel, initrd, and the kernel cmdline, and +// the number of vcpus and their type +func CalculateSEVESLaunchDigest(vcpus int, vcpuSig VCPUSig, firmwarePath, kernelPath, initrdPath, cmdline string) (res [sha256.Size]byte, err error) { + f, err := os.Open(firmwarePath) + if err != nil { + return res, err + } + defer f.Close() + + digest := sha256.New() + if _, err := io.Copy(digest, f); err != nil { + return res, err + } + + // When used for confidential containers in kata-containers, kernelPath + // is always set (direct boot). However, this current package can also + // be used by other programs which may calculate launch digests of + // arbitrary SEV guests without SEV kernel hashes table. + if kernelPath != "" { + ht, err := constructSevHashesTable(kernelPath, initrdPath, cmdline) + if err != nil { + return res, err + } + digest.Write(ht) + } + + o, err := NewOvmf(firmwarePath) + if err != nil { + return res, err + } + resetEip, err := o.sevEsResetEip() + if err != nil { + return res, err + } + v := vmsaBuilder{uint64(resetEip), vcpuSig} + for i := 0; i < vcpus; i++ { + vmsaPage, err := v.buildPage(i) + if err != nil { + return res, err + } + digest.Write(vmsaPage) + } + + copy(res[:], digest.Sum(nil)) + return res, nil +} diff --git a/src/runtime/pkg/sev/sev_test.go b/src/runtime/pkg/sev/sev_test.go index d1ffffa7ff..68a82ea90d 100644 --- a/src/runtime/pkg/sev/sev_test.go +++ b/src/runtime/pkg/sev/sev_test.go @@ -30,3 +30,25 @@ func TestCalculateLaunchDigestWithKernelHashes(t *testing.T) { t.Fatalf("wrong measurement: %s", hexld) } } + +func TestCalculateLaunchDigestWithKernelHashesSevEs(t *testing.T) { + ld, err := CalculateSEVESLaunchDigest(1, SigEpycV4, "testdata/ovmf_suffix.bin", "/dev/null", "/dev/null", "") + if err != nil { + t.Fatalf("unexpected err value: %s", err) + } + hexld := hex.EncodeToString(ld[:]) + if hexld != "7e5c26fb454621eb466978b4d0242b3c04b44a034de7fc0a2d8dac60ea2b6403" { + t.Fatalf("wrong measurement: %s", hexld) + } +} + +func TestCalculateLaunchDigestWithKernelHashesSevEsAndSmp(t *testing.T) { + ld, err := CalculateSEVESLaunchDigest(4, SigEpycV4, "testdata/ovmf_suffix.bin", "/dev/null", "/dev/null", "") + if err != nil { + t.Fatalf("unexpected err value: %s", err) + } + hexld := hex.EncodeToString(ld[:]) + if hexld != "b2111b0051fc3a06ec216899b2c78da99fb9d56c6ff2e8261dd3fe6cff79ecbc" { + t.Fatalf("wrong measurement: %s", hexld) + } +} diff --git a/src/runtime/pkg/sev/vcpu_sigs.go b/src/runtime/pkg/sev/vcpu_sigs.go new file mode 100644 index 0000000000..b28460b0fd --- /dev/null +++ b/src/runtime/pkg/sev/vcpu_sigs.go @@ -0,0 +1,48 @@ +// Copyright contributors to AMD SEV/-ES in Go +// +// SPDX-License-Identifier: Apache-2.0 + +package sev + +type VCPUSig uint64 + +const ( + // 'EPYC': family=23, model=1, stepping=2 + SigEpyc VCPUSig = 0x800f12 + + // 'EPYC-v1': family=23, model=1, stepping=2 + SigEpycV1 VCPUSig = 0x800f12 + + // 'EPYC-v2': family=23, model=1, stepping=2 + SigEpycV2 VCPUSig = 0x800f12 + + // 'EPYC-IBPB': family=23, model=1, stepping=2 + SigEpycIBPB VCPUSig = 0x800f12 + + // 'EPYC-v3': family=23, model=1, stepping=2 + SigEpycV3 VCPUSig = 0x800f12 + + // 'EPYC-v4': family=23, model=1, stepping=2 + SigEpycV4 VCPUSig = 0x800f12 + + // 'EPYC-Rome': family=23, model=49, stepping=0 + SigEpycRome VCPUSig = 0x830f10 + + // 'EPYC-Rome-v1': family=23, model=49, stepping=0 + SigEpycRomeV1 VCPUSig = 0x830f10 + + // 'EPYC-Rome-v2': family=23, model=49, stepping=0 + SigEpycRomeV2 VCPUSig = 0x830f10 + + // 'EPYC-Rome-v3': family=23, model=49, stepping=0 + SigEpycRomeV3 VCPUSig = 0x830f10 + + // 'EPYC-Milan': family=25, model=1, stepping=1 + SigEpycMilan VCPUSig = 0xa00f11 + + // 'EPYC-Milan-v1': family=25, model=1, stepping=1 + SigEpycMilanV1 VCPUSig = 0xa00f11 + + // 'EPYC-Milan-v2': family=25, model=1, stepping=1 + SigEpycMilanV2 VCPUSig = 0xa00f11 +) diff --git a/src/runtime/pkg/sev/vmsa.go b/src/runtime/pkg/sev/vmsa.go new file mode 100644 index 0000000000..c2bbc4122b --- /dev/null +++ b/src/runtime/pkg/sev/vmsa.go @@ -0,0 +1,172 @@ +// Copyright contributors to AMD SEV/-ES in Go +// +// SPDX-License-Identifier: Apache-2.0 + +package sev + +import ( + "bytes" + "encoding/binary" +) + +// VMCB Segment (struct vmcb_seg in the linux kernel) +type vmcbSeg struct { + selector uint16 + attrib uint16 + limit uint32 + base uint64 +} + +// VMSA page +// +// The names of the fields are taken from struct sev_es_work_area in the linux kernel: +// https://github.com/AMDESE/linux/blob/sev-snp-v12/arch/x86/include/asm/svm.h#L318 +// (following the definitions in AMD APM Vol 2 Table B-4) +type sevEsSaveArea struct { + es vmcbSeg + cs vmcbSeg + ss vmcbSeg + ds vmcbSeg + fs vmcbSeg + gs vmcbSeg + gdtr vmcbSeg + ldtr vmcbSeg + idtr vmcbSeg + tr vmcbSeg + vmpl0_ssp uint64 // nolint: unused + vmpl1_ssp uint64 // nolint: unused + vmpl2_ssp uint64 // nolint: unused + vmpl3_ssp uint64 // nolint: unused + u_cet uint64 // nolint: unused + reserved_1 [2]uint8 // nolint: unused + vmpl uint8 // nolint: unused + cpl uint8 // nolint: unused + reserved_2 [4]uint8 // nolint: unused + efer uint64 + reserved_3 [104]uint8 // nolint: unused + xss uint64 // nolint: unused + cr4 uint64 + cr3 uint64 // nolint: unused + cr0 uint64 + dr7 uint64 + dr6 uint64 + rflags uint64 + rip uint64 + dr0 uint64 // nolint: unused + dr1 uint64 // nolint: unused + dr2 uint64 // nolint: unused + dr3 uint64 // nolint: unused + dr0_addr_mask uint64 // nolint: unused + dr1_addr_mask uint64 // nolint: unused + dr2_addr_mask uint64 // nolint: unused + dr3_addr_mask uint64 // nolint: unused + reserved_4 [24]uint8 // nolint: unused + rsp uint64 // nolint: unused + s_cet uint64 // nolint: unused + ssp uint64 // nolint: unused + isst_addr uint64 // nolint: unused + rax uint64 // nolint: unused + star uint64 // nolint: unused + lstar uint64 // nolint: unused + cstar uint64 // nolint: unused + sfmask uint64 // nolint: unused + kernel_gs_base uint64 // nolint: unused + sysenter_cs uint64 // nolint: unused + sysenter_esp uint64 // nolint: unused + sysenter_eip uint64 // nolint: unused + cr2 uint64 // nolint: unused + reserved_5 [32]uint8 // nolint: unused + g_pat uint64 + dbgctrl uint64 // nolint: unused + br_from uint64 // nolint: unused + br_to uint64 // nolint: unused + last_excp_from uint64 // nolint: unused + last_excp_to uint64 // nolint: unused + reserved_7 [80]uint8 // nolint: unused + pkru uint32 // nolint: unused + reserved_8 [20]uint8 // nolint: unused + reserved_9 uint64 // nolint: unused + rcx uint64 // nolint: unused + rdx uint64 + rbx uint64 // nolint: unused + reserved_10 uint64 // nolint: unused + rbp uint64 // nolint: unused + rsi uint64 // nolint: unused + rdi uint64 // nolint: unused + r8 uint64 // nolint: unused + r9 uint64 // nolint: unused + r10 uint64 // nolint: unused + r11 uint64 // nolint: unused + r12 uint64 // nolint: unused + r13 uint64 // nolint: unused + r14 uint64 // nolint: unused + r15 uint64 // nolint: unused + reserved_11 [16]uint8 // nolint: unused + guest_exit_info_1 uint64 // nolint: unused + guest_exit_info_2 uint64 // nolint: unused + guest_exit_int_info uint64 // nolint: unused + guest_nrip uint64 // nolint: unused + sev_features uint64 + vintr_ctrl uint64 // nolint: unused + guest_exit_code uint64 // nolint: unused + virtual_tom uint64 // nolint: unused + tlb_id uint64 // nolint: unused + pcpu_id uint64 // nolint: unused + event_inj uint64 // nolint: unused + xcr0 uint64 + reserved_12 [16]uint8 // nolint: unused + x87_dp uint64 // nolint: unused + mxcsr uint32 // nolint: unused + x87_ftw uint16 // nolint: unused + x87_fsw uint16 // nolint: unused + x87_fcw uint16 // nolint: unused + x87_fop uint16 // nolint: unused + x87_ds uint16 // nolint: unused + x87_cs uint16 // nolint: unused + x87_rip uint64 // nolint: unused + fpreg_x87 [80]uint8 // nolint: unused + fpreg_xmm [256]uint8 // nolint: unused + fpreg_ymm [256]uint8 // nolint: unused + unused [2448]uint8 // nolint: unused +} + +type vmsaBuilder struct { + apEIP uint64 + vcpuSig VCPUSig +} + +func (v *vmsaBuilder) buildPage(i int) ([]byte, error) { + eip := uint64(0xfffffff0) // BSP (first vcpu) + if i > 0 { + eip = v.apEIP + } + saveArea := sevEsSaveArea{ + es: vmcbSeg{0, 0x93, 0xffff, 0}, + cs: vmcbSeg{0xf000, 0x9b, 0xffff, eip & 0xffff0000}, + ss: vmcbSeg{0, 0x93, 0xffff, 0}, + ds: vmcbSeg{0, 0x93, 0xffff, 0}, + fs: vmcbSeg{0, 0x93, 0xffff, 0}, + gs: vmcbSeg{0, 0x93, 0xffff, 0}, + gdtr: vmcbSeg{0, 0, 0xffff, 0}, + idtr: vmcbSeg{0, 0, 0xffff, 0}, + ldtr: vmcbSeg{0, 0x82, 0xffff, 0}, + tr: vmcbSeg{0, 0x8b, 0xffff, 0}, + efer: 0x1000, // KVM enables EFER_SVME + cr4: 0x40, // KVM enables X86_CR4_MCE + cr0: 0x10, + dr7: 0x400, + dr6: 0xffff0ff0, + rflags: 0x2, + rip: eip & 0xffff, + g_pat: 0x7040600070406, // PAT MSR: See AMD APM Vol 2, Section A.3 + rdx: uint64(v.vcpuSig), + sev_features: 0, // SEV-ES + xcr0: 0x1, + } + page := new(bytes.Buffer) + err := binary.Write(page, binary.LittleEndian, saveArea) + if err != nil { + return []byte{}, err + } + return page.Bytes(), nil +}