diff --git a/Makefile b/Makefile index 0439f54afa..b5004f4a98 100644 --- a/Makefile +++ b/Makefile @@ -139,8 +139,6 @@ INSTALLER_ONLY_PKGS ?= \ zstd IMAGER_EXTRA_PKGS ?= \ - binutils-aarch64 \ - binutils-x86_64 \ dosfstools \ e2fsprogs \ mtools \ @@ -460,7 +458,7 @@ secureboot-iso: image-secureboot-iso ## Builds UEFI only ISO which uses UKI and .PHONY: secureboot-installer secureboot-installer: ## Builds UEFI only installer which uses UKI and push it to the registry. - @$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)" + @$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG) $(IMAGER_ARGS)" @for platform in $(subst $(,),$(space),$(PLATFORM)); do \ arch=$$(basename "$${platform}") && \ crane push $(ARTIFACTS)/installer-$${arch}-secureboot.tar $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)-$${arch}-secureboot ; \ diff --git a/internal/pkg/secureboot/uki/assemble.go b/internal/pkg/secureboot/uki/assemble.go index 9d3627e30c..27876772f9 100644 --- a/internal/pkg/secureboot/uki/assemble.go +++ b/internal/pkg/secureboot/uki/assemble.go @@ -5,80 +5,14 @@ package uki import ( - "debug/pe" - "errors" - "fmt" - "os" - "os/exec" "path/filepath" + + "github.com/siderolabs/talos/internal/pkg/secureboot/uki/internal/pe" ) // assemble the UKI file out of sections. func (builder *Builder) assemble() error { - peFile, err := pe.Open(builder.SdStubPath) - if err != nil { - return err - } - - defer peFile.Close() //nolint: errcheck - - // find the first VMA address - lastSection := peFile.Sections[len(peFile.Sections)-1] - - // align the VMA to 512 bytes - // https://github.com/saferwall/pe/blob/main/helper.go#L22-L26 - const alignment = 0x1ff - - header, ok := peFile.OptionalHeader.(*pe.OptionalHeader64) - if !ok { - return errors.New("failed to get optional header") - } - - baseVMA := header.ImageBase + uint64(lastSection.VirtualAddress) + uint64(lastSection.VirtualSize) - baseVMA = (baseVMA + alignment) &^ alignment - - // calculate sections size and VMA - for i := range builder.sections { - if !builder.sections[i].Append { - continue - } - - st, err := os.Stat(builder.sections[i].Path) - if err != nil { - return err - } - - builder.sections[i].Size = uint64(st.Size()) - builder.sections[i].VMA = baseVMA - - baseVMA += builder.sections[i].Size - baseVMA = (baseVMA + alignment) &^ alignment - } - - // create the output file - args := make([]string, 0, len(builder.sections)+2) - - for _, section := range builder.sections { - if !section.Append { - continue - } - - args = append(args, "--add-section", fmt.Sprintf("%s=%s", section.Name, section.Path), "--change-section-vma", fmt.Sprintf("%s=0x%x", section.Name, section.VMA)) - } - builder.unsignedUKIPath = filepath.Join(builder.scratchDir, "unsigned.uki") - args = append(args, builder.SdStubPath, builder.unsignedUKIPath) - - objcopy := "/usr/x86_64-alpine-linux-musl/bin/objcopy" - - if builder.Arch == "arm64" { - objcopy = "/usr/aarch64-alpine-linux-musl/bin/objcopy" - } - - cmd := exec.Command(objcopy, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return cmd.Run() + return pe.AssembleNative(builder.SdStubPath, builder.unsignedUKIPath, builder.sections) } diff --git a/internal/pkg/secureboot/uki/internal/pe/native.go b/internal/pkg/secureboot/uki/internal/pe/native.go new file mode 100644 index 0000000000..9a12b8a147 --- /dev/null +++ b/internal/pkg/secureboot/uki/internal/pe/native.go @@ -0,0 +1,280 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pe + +import ( + "debug/pe" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "slices" + "time" + + "github.com/siderolabs/gen/xslices" + + "github.com/siderolabs/talos/internal/pkg/secureboot" + "github.com/siderolabs/talos/pkg/imager/utils" +) + +const ( + dosHeaderLength = 0x40 + dosHeaderPadding = 0x40 +) + +// AssembleNative is a helper function to assemble the PE file without external programs. +// +//nolint:gocyclo,cyclop +func AssembleNative(srcPath, dstPath string, sections []Section) error { + in, err := os.Open(srcPath) + if err != nil { + return err + } + + peFile, err := pe.NewFile(in) + if err != nil { + return err + } + + defer in.Close() //nolint: errcheck + + if peFile.COFFSymbols != nil { + return errors.New("COFF symbols are not supported") + } + + if peFile.StringTable != nil { + return errors.New("COFF string table is not supported") + } + + if peFile.Symbols != nil { + return errors.New("symbols are not supported") + } + + _, err = in.Seek(0, io.SeekStart) + if err != nil { + return err + } + + out, err := os.Create(dstPath) + if err != nil { + return fmt.Errorf("failed to create output: %w", err) + } + + defer out.Close() //nolint: errcheck + + // 1. DOS header + var dosheader [dosHeaderLength]byte + + _, err = in.ReadAt(dosheader[:], 0) + if err != nil { + return fmt.Errorf("failed to read DOS header: %w", err) + } + + binary.LittleEndian.PutUint32(dosheader[dosHeaderLength-4:], dosHeaderLength+dosHeaderPadding) + + _, err = out.Write(append(append(dosheader[:], make([]byte, dosHeaderPadding)...), []byte("PE\x00\x00")...)) + if err != nil { + return fmt.Errorf("failed to write DOS header: %w", err) + } + + // 2. PE header and optional header + newFileHeader := peFile.FileHeader + + timestamp, ok, err := utils.SourceDateEpoch() + if err != nil { + return fmt.Errorf("failed to get SOURCE_DATE_EPOCH: %w", err) + } + + if !ok { + timestamp = time.Now().Unix() + } + + newFileHeader.TimeDateStamp = uint32(timestamp) + + // find the first VMA address + lastSection := peFile.Sections[len(peFile.Sections)-1] + + header, ok := peFile.OptionalHeader.(*pe.OptionalHeader64) + if !ok { + return errors.New("failed to get optional header") + } + + sectionAlignment := uint64(header.SectionAlignment - 1) + fileAlignment := uint64(header.FileAlignment - 1) + + baseVirtualAddress := uint64(lastSection.VirtualAddress) + uint64(lastSection.VirtualSize) + baseVirtualAddress = (baseVirtualAddress + sectionAlignment) &^ sectionAlignment + + newHeader := *header + newHeader.MajorLinkerVersion = 0 + newHeader.MinorLinkerVersion = 0 + newHeader.CheckSum = 0 + + newSections := slices.Clone(peFile.Sections) + + // calculate sections size and VMA + for i := range sections { + if !sections[i].Append { + continue + } + + st, err := os.Stat(sections[i].Path) + if err != nil { + return err + } + + sections[i].virtualSize = uint64(st.Size()) + sections[i].virtualAddress = baseVirtualAddress + + baseVirtualAddress += sections[i].virtualSize + baseVirtualAddress = (baseVirtualAddress + sectionAlignment) &^ sectionAlignment + + newFileHeader.NumberOfSections++ + + newSections = append(newSections, &pe.Section{ + SectionHeader: pe.SectionHeader{ + Name: string(sections[i].Name), + VirtualSize: uint32(sections[i].virtualSize), + VirtualAddress: uint32(sections[i].virtualAddress), + Size: uint32((sections[i].virtualSize + fileAlignment) &^ fileAlignment), + Characteristics: pe.IMAGE_SCN_CNT_INITIALIZED_DATA | pe.IMAGE_SCN_MEM_READ, + }, + }) + } + + newHeader.SizeOfInitializedData = 0 + newHeader.SizeOfCode = 0 + newHeader.SizeOfHeaders = 0x80 /* DOS header */ + uint32(binary.Size(pe.FileHeader{})+binary.Size(pe.OptionalHeader64{})+binary.Size(pe.SectionHeader32{})*len(newSections)) + newHeader.SizeOfHeaders = (newHeader.SizeOfHeaders + uint32(fileAlignment)) &^ uint32(fileAlignment) + + lastNewSection := newSections[len(newSections)-1] + + lastSectionPointer := uint64(lastNewSection.VirtualAddress) + uint64(lastNewSection.VirtualSize) + newHeader.ImageBase + lastSectionPointer = (lastSectionPointer + sectionAlignment) &^ sectionAlignment + + newHeader.SizeOfImage = uint32(lastSectionPointer - newHeader.ImageBase) + + for _, section := range newSections { + if section.Characteristics&pe.IMAGE_SCN_CNT_INITIALIZED_DATA != 0 { + newHeader.SizeOfInitializedData += section.Size + } else { + newHeader.SizeOfCode += section.Size + } + } + + // write the new file header + if err = binary.Write(out, binary.LittleEndian, newFileHeader); err != nil { + return fmt.Errorf("failed to write file header: %w", err) + } + + if err = binary.Write(out, binary.LittleEndian, newHeader); err != nil { + return fmt.Errorf("failed to write optional header: %w", err) + } + + // 3. Section headers + rawSections := xslices.Map(newSections, func(section *pe.Section) pe.SectionHeader32 { + var rawName [8]byte + + copy(rawName[:], section.Name) + + return pe.SectionHeader32{ + Name: rawName, + VirtualSize: section.VirtualSize, + VirtualAddress: section.VirtualAddress, + SizeOfRawData: section.Size, + Characteristics: section.Characteristics, + } + }, + ) + + sectionOffset := newHeader.SizeOfHeaders + + for i := range rawSections { + rawSections[i].PointerToRawData = sectionOffset + + sectionOffset += rawSections[i].SizeOfRawData + } + + for _, rawSection := range rawSections { + if err = binary.Write(out, binary.LittleEndian, rawSection); err != nil { + return fmt.Errorf("failed to write section header: %w", err) + } + } + + // 4. Section data + for i, rawSection := range rawSections { + name := newSections[i].Name + + if err := func(rawSection pe.SectionHeader32, name string) error { + // the section might come either from the input PE file or from a separate file + var sectionData io.ReadCloser + + for _, section := range sections { + if section.Append && section.Name == secureboot.Section(name) { + sectionData, err = os.Open(section.Path) + if err != nil { + return fmt.Errorf("failed to open section data: %w", err) + } + + defer sectionData.Close() //nolint: errcheck + + break + } + } + + if sectionData == nil { + for _, section := range peFile.Sections { + if section.Name == name { + sectionData = io.NopCloser(section.Open()) + + break + } + } + } + + if sectionData == nil { + return fmt.Errorf("failed to find section data for %q", name) + } + + _, err = out.Seek(int64(rawSection.PointerToRawData), io.SeekStart) + if err != nil { + return fmt.Errorf("failed to seek to section data: %w", err) + } + + n, err := io.Copy(out, sectionData) + if err != nil { + return fmt.Errorf("failed to copy section data: %w", err) + } + + if n > int64(rawSection.SizeOfRawData) { + return fmt.Errorf("section data is too large: %d > %d", n, rawSection.SizeOfRawData) + } + + if n < int64(rawSection.SizeOfRawData) { + _, err = io.CopyN(out, zeroReader{}, int64(rawSection.SizeOfRawData)-n) + if err != nil { + return fmt.Errorf("failed to zero-fill section data: %w", err) + } + } + + return nil + }(rawSection, name); err != nil { + return fmt.Errorf("failed to write section data %s: %w", name, err) + } + } + + return nil +} + +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + + return len(p), nil +} diff --git a/internal/pkg/secureboot/uki/internal/pe/objcopy.go b/internal/pkg/secureboot/uki/internal/pe/objcopy.go new file mode 100644 index 0000000000..af29023a3c --- /dev/null +++ b/internal/pkg/secureboot/uki/internal/pe/objcopy.go @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pe + +import ( + "debug/pe" + "errors" + "fmt" + "os" + "os/exec" +) + +// AssembleObjcopy is a helper function to assemble the PE file using objcopy. +func AssembleObjcopy(srcPath, dstPath string, sections []Section) error { + peFile, err := pe.Open(srcPath) + if err != nil { + return err + } + + defer peFile.Close() //nolint: errcheck + + // find the first VMA address + lastSection := peFile.Sections[len(peFile.Sections)-1] + + header, ok := peFile.OptionalHeader.(*pe.OptionalHeader64) + if !ok { + return errors.New("failed to get optional header") + } + + sectionAlignment := uint64(header.SectionAlignment - 1) + + baseVMA := header.ImageBase + uint64(lastSection.VirtualAddress) + uint64(lastSection.VirtualSize) + baseVMA = (baseVMA + sectionAlignment) &^ sectionAlignment + + // calculate sections size and VMA + for i := range sections { + if !sections[i].Append { + continue + } + + st, err := os.Stat(sections[i].Path) + if err != nil { + return err + } + + sections[i].virtualSize = uint64(st.Size()) + sections[i].virtualAddress = baseVMA + + baseVMA += sections[i].virtualSize + baseVMA = (baseVMA + sectionAlignment) &^ sectionAlignment + } + + // create the output file + args := make([]string, 0, len(sections)+2) + + for _, section := range sections { + if !section.Append { + continue + } + + args = append(args, "--add-section", fmt.Sprintf("%s=%s", section.Name, section.Path), "--change-section-vma", fmt.Sprintf("%s=0x%x", section.Name, section.virtualAddress)) + } + + args = append(args, srcPath, dstPath) + + cmd := exec.Command("objcopy", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/internal/pkg/secureboot/uki/internal/pe/pe.go b/internal/pkg/secureboot/uki/internal/pe/pe.go new file mode 100644 index 0000000000..5d9e4ba124 --- /dev/null +++ b/internal/pkg/secureboot/uki/internal/pe/pe.go @@ -0,0 +1,25 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package pe handles appending sections to PE files. +package pe + +import ( + "github.com/siderolabs/talos/internal/pkg/secureboot" +) + +// Section is a UKI file section. +type Section struct { + // Section name. + Name secureboot.Section + // Path to the contents of the section. + Path string + // Should the section be measured to the TPM? + Measure bool + // Should the section be appended, or is it already in the PE file. + Append bool + // Virtual virtualSize & VMA of the section. + virtualSize uint64 + virtualAddress uint64 +} diff --git a/internal/pkg/secureboot/uki/internal/pe/pe_test.go b/internal/pkg/secureboot/uki/internal/pe/pe_test.go new file mode 100644 index 0000000000..8d2ede41c9 --- /dev/null +++ b/internal/pkg/secureboot/uki/internal/pe/pe_test.go @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pe_test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/internal/pkg/secureboot/uki/internal/pe" +) + +func TestAssembleNative(t *testing.T) { + for _, tool := range []string{ + "objcopy", + "objdump", + "xxd", + } { + _, err := exec.LookPath(tool) + if err != nil { + t.Skipf("missing tool: %s", tool) + } + } + + t.Setenv("SOURCE_DATE_EPOCH", "1609459200") + + tmpDir := t.TempDir() + + outNative := filepath.Join(tmpDir, "uki-native.bin") + outObjcopy := filepath.Join(tmpDir, "uki-objcopy.bin") + + unamePath := filepath.Join(tmpDir, "uname") + require.NoError(t, os.WriteFile(unamePath, []byte("Talos"), 0o644)) + + linuxPath := filepath.Join(tmpDir, "linux") + require.NoError(t, os.WriteFile(linuxPath, bytes.Repeat([]byte{0xde, 0xad, 0xbe, 0xef}, 1048576), 0o644)) + + sections := func() []pe.Section { + return []pe.Section{ + { + Name: ".text", + }, + { + Name: ".uname", + Append: true, + + Path: unamePath, + }, + { + Name: ".linux", + Append: true, + + Path: linuxPath, + }, + } + } + + require.NoError(t, pe.AssembleNative("testdata/sd-stub-amd64.efi", outNative, sections())) + + require.NoError(t, pe.AssembleObjcopy("testdata/sd-stub-amd64.efi", outObjcopy, sections())) + + headersNative := dumpHeaders(t, outNative) + headersObjcopy := dumpHeaders(t, outObjcopy) + + // we don't compute the checksums, so ignore these fields + headersObjcopy = regexp.MustCompile(`(CheckSum\s+)[0-9a-fA-F]+`).ReplaceAllString(headersObjcopy, "${1}00000000") + // we don't set linker version + headersObjcopy = regexp.MustCompile(`((Major|Minor)LinkerVersion\s+)[0-9.]+`).ReplaceAllString(headersObjcopy, "${1}0") + + assert.Equal(t, headersObjcopy, headersNative) + + for _, sectionName := range []string{ + ".text", + ".rodata", + ".data", + ".sbat", + ".sdmagic", + ".reloc", + ".uname", + ".linux", + } { + sectionObjcopy := extractSection(t, outObjcopy, sectionName) + sectionNative := extractSection(t, outNative, sectionName) + + assert.Equal(t, sectionObjcopy, sectionNative) + } + + if false { + // deep binary comparison, disabled by default, as there will be some difference always + binaryObjcopy := binaryDump(t, outObjcopy) + binaryNative := binaryDump(t, outNative) + + assert.Equal(t, binaryObjcopy, binaryNative) + } +} + +func dumpHeaders(t *testing.T, path string) string { + t.Helper() + + output, err := exec.Command("objdump", "-x", path).CombinedOutput() + require.NoError(t, err, string(output)) + + output = bytes.ReplaceAll(output, []byte(path), []byte("uki.bin")) + + return string(output) +} + +func binaryDump(t *testing.T, path string) string { + t.Helper() + + output, err := exec.Command("xxd", path).CombinedOutput() + require.NoError(t, err, string(output)) + + return string(output) +} + +func extractSection(t *testing.T, path, section string) string { + t.Helper() + + output, err := exec.Command("objdump", "-s", "--section", section, path).CombinedOutput() + require.NoError(t, err, string(output)) + + output = bytes.ReplaceAll(output, []byte(path), []byte("uki.bin")) + + return string(output) +} diff --git a/internal/pkg/secureboot/uki/internal/pe/testdata/sd-stub-amd64.efi b/internal/pkg/secureboot/uki/internal/pe/testdata/sd-stub-amd64.efi new file mode 100644 index 0000000000..5f28245088 Binary files /dev/null and b/internal/pkg/secureboot/uki/internal/pe/testdata/sd-stub-amd64.efi differ diff --git a/internal/pkg/secureboot/uki/uki.go b/internal/pkg/secureboot/uki/uki.go index f762c24be8..a493102e92 100644 --- a/internal/pkg/secureboot/uki/uki.go +++ b/internal/pkg/secureboot/uki/uki.go @@ -10,26 +10,13 @@ import ( "log" "os" - "github.com/siderolabs/talos/internal/pkg/secureboot" "github.com/siderolabs/talos/internal/pkg/secureboot/measure" "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" + "github.com/siderolabs/talos/internal/pkg/secureboot/uki/internal/pe" "github.com/siderolabs/talos/pkg/imager/utils" ) -// section is a UKI file section. -type section struct { - // Section name. - Name secureboot.Section - // Path to the contents of the section. - Path string - // Should the section be measured to the TPM? - Measure bool - // Should the section be appended, or is it already in the PE file. - Append bool - // Size & VMA of the section. - Size uint64 - VMA uint64 -} +type section = pe.Section // Builder is a UKI file builder. type Builder struct {