From 6313ec51d2c4a50d3ea303dbf3ca063cd471950c Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Tue, 7 Jan 2025 20:32:10 +0530 Subject: [PATCH] feat: uki iso Switch to using sd-boot for newer iso. New profile `legacy-iso` added to support booting with grub. Part of: #9633 Signed-off-by: Noel Georgi --- internal/pkg/secureboot/uki/generate.go | 10 ++- internal/pkg/secureboot/uki/uki.go | 63 +++++++++++++++++- pkg/imager/imager.go | 57 ++++++++++------ pkg/imager/iso/uefi.go | 83 +++++++++++++---------- pkg/imager/out.go | 87 +++++++++++++++---------- pkg/imager/profile/default.go | 20 ++++++ pkg/imager/profile/input.go | 14 ++-- pkg/imager/profile/output.go | 2 + pkg/imager/profile/outputkind_enumer.go | 12 ++-- pkg/imager/profile/profile.go | 7 +- 10 files changed, 245 insertions(+), 110 deletions(-) diff --git a/internal/pkg/secureboot/uki/generate.go b/internal/pkg/secureboot/uki/generate.go index a4d5623e0e..eb693a9755 100644 --- a/internal/pkg/secureboot/uki/generate.go +++ b/internal/pkg/secureboot/uki/generate.go @@ -185,10 +185,14 @@ func (builder *Builder) generatePCRPublicKey() error { } func (builder *Builder) generateKernel() error { - path := filepath.Join(builder.scratchDir, "kernel") + path := builder.KernelPath - if err := builder.peSigner.Sign(builder.KernelPath, path); err != nil { - return err + if builder.peSigner != nil { + path = filepath.Join(builder.scratchDir, "kernel") + + if err := builder.peSigner.Sign(builder.KernelPath, path); err != nil { + return err + } } builder.sections = append(builder.sections, diff --git a/internal/pkg/secureboot/uki/uki.go b/internal/pkg/secureboot/uki/uki.go index b917fc0d2e..416c2638b9 100644 --- a/internal/pkg/secureboot/uki/uki.go +++ b/internal/pkg/secureboot/uki/uki.go @@ -10,6 +10,8 @@ import ( "log" "os" + "github.com/siderolabs/go-copy/copy" + "github.com/siderolabs/talos/internal/pkg/secureboot" "github.com/siderolabs/talos/internal/pkg/secureboot/measure" "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" @@ -67,14 +69,14 @@ type Builder struct { unsignedUKIPath string } -// Build the UKI file. +// BuildSignedUKI builds the signed UKI file. // -// Build process is as follows: +// BuildSignedUKI process is as follows: // - sign the sd-boot EFI binary, and write it to the OutSdBootPath // - build ephemeral sections (uname, os-release), and other proposed sections // - measure sections, generate signature, and append to the list of sections // - assemble the final UKI file starting from sd-stub and appending generated section. -func (builder *Builder) Build(printf func(string, ...any)) error { +func (builder *Builder) BuildSignedUKI(printf func(string, ...any)) error { var err error builder.scratchDir, err = os.MkdirTemp("", "talos-uki") @@ -133,3 +135,58 @@ func (builder *Builder) Build(printf func(string, ...any)) error { // sign the UKI file return builder.peSigner.Sign(builder.unsignedUKIPath, builder.OutUKIPath) } + +// Build the unsigned UKI file. +// +// Build process is as follows: +// - build ephemeral sections (uname, os-release), and other proposed sections +// - assemble the final UKI file starting from sd-stub and appending generated section. +func (builder *Builder) Build(printf func(string, ...any)) error { + var err error + + builder.scratchDir, err = os.MkdirTemp("", "talos-uki") + if err != nil { + return err + } + + defer func() { + if err = os.RemoveAll(builder.scratchDir); err != nil { + log.Printf("failed to remove scratch dir: %v", err) + } + }() + + if err := copy.File(builder.SdBootPath, builder.OutSdBootPath); err != nil { + return err + } + + printf("generating UKI sections") + + // generate and build list of all sections + for _, generateSection := range []func() error{ + builder.generateOSRel, + builder.generateCmdline, + builder.generateInitrd, + builder.generateSplash, + builder.generateUname, + builder.generateSBAT, + // append kernel last to account for decompression + builder.generateKernel, + } { + if err = generateSection(); err != nil { + return fmt.Errorf("error generating sections: %w", err) + } + } + + printf("assembling UKI") + + // assemble the final UKI file + if err = builder.assemble(); err != nil { + return fmt.Errorf("error assembling UKI: %w", err) + } + + if err := copy.File(builder.unsignedUKIPath, builder.OutUKIPath); err != nil { + return err + } + + return nil +} diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index b3b3e22ff9..73d19584ba 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -98,24 +98,26 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte return "", err } - report.Report(reporter.Update{ - Message: fmt.Sprintf("kernel command line: %s", i.cmdline), - Status: reporter.StatusSucceeded, - }) - - // 4. Build UKI if Secure Boot is enabled. - if i.prof.SecureBootEnabled() { + // 4. Build UKI unless the output is a kernel or cmdline. + if i.prof.Output.Kind != profile.OutKindKernel && i.prof.Output.Kind != profile.OutKindCmdline { if err = i.buildUKI(ctx, report); err != nil { return "", err } } + report.Report(reporter.Update{ + Message: fmt.Sprintf("kernel command line: %s", i.cmdline), + Status: reporter.StatusSucceeded, + }) + // 5. Build the output. outputAssetPath = filepath.Join(outputPath, i.prof.OutputPath()) switch i.prof.Output.Kind { case profile.OutKindISO: err = i.outISO(ctx, outputAssetPath, report) + case profile.OutKindISOUKI: + err = i.outISOUKI(ctx, outputAssetPath, report) case profile.OutKindKernel: err = i.outKernel(outputAssetPath, report) case profile.OutKindUKI: @@ -406,17 +408,12 @@ func (i *Imager) buildCmdline() error { func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error { printf := progressPrintf(report, reporter.Update{Message: "building UKI...", Status: reporter.StatusRunning}) - i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed") - i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed") + i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi") + i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi") - pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx) - if err != nil { - return fmt.Errorf("failed to get PCR signer: %w", err) - } - - securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) - if err != nil { - return fmt.Errorf("failed to get SecureBoot signer: %w", err) + if i.prof.SecureBootEnabled() { + i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed") + i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed") } builder := uki.Builder{ @@ -428,14 +425,32 @@ func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error InitrdPath: i.initramfsPath, Cmdline: i.cmdline, - SecureBootSigner: securebootSigner, - PCRSigner: pcrSigner, - OutSdBootPath: i.sdBootPath, OutUKIPath: i.ukiPath, } - if err := builder.Build(printf); err != nil { + if i.prof.SecureBootEnabled() { + pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx) + if err != nil { + return fmt.Errorf("failed to get PCR signer: %w", err) + } + + securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) + if err != nil { + return fmt.Errorf("failed to get SecureBoot signer: %w", err) + } + + builder.PCRSigner = pcrSigner + builder.SecureBootSigner = securebootSigner + } + + buildFunc := builder.Build + + if i.prof.SecureBootEnabled() { + buildFunc = builder.BuildSignedUKI + } + + if err := buildFunc(printf); err != nil { return err } diff --git a/pkg/imager/iso/uefi.go b/pkg/imager/iso/uefi.go index 15418a1a3c..b90f1d6d69 100644 --- a/pkg/imager/iso/uefi.go +++ b/pkg/imager/iso/uefi.go @@ -15,6 +15,7 @@ import ( "github.com/siderolabs/go-cmd/pkg/cmd" "github.com/siderolabs/go-copy/copy" + "github.com/siderolabs/talos/pkg/imager/profile" "github.com/siderolabs/talos/pkg/imager/utils" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/imager/quirks" @@ -42,6 +43,9 @@ type UEFIOptions struct { ScratchDir string OutPath string + + // Secureboot is a flag to enable secureboot. + Secureboot bool } const ( @@ -85,18 +89,6 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { return err } - printf("preparing loader.conf") - - var loaderConfigOut bytes.Buffer - - if err := template.Must(template.New("loader.conf").Parse(loaderConfigTemplate)).Execute(&loaderConfigOut, struct { - SecureBootEnroll string - }{ - SecureBootEnroll: options.SDBootSecureBootEnrollKeys, - }); err != nil { - return fmt.Errorf("error rendering loader.conf: %w", err) - } - printf("creating vFAT EFI image") fopts := []makefs.Option{ @@ -116,11 +108,7 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { return err } - if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/keys"), 0o755); err != nil { - return err - } - - if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader/keys/auto"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader"), 0o755); err != nil { return err } @@ -138,30 +126,59 @@ func CreateUEFI(printf func(string, ...any), options UEFIOptions) error { return err } - if err := os.WriteFile(filepath.Join(options.ScratchDir, "loader/loader.conf"), loaderConfigOut.Bytes(), 0o644); err != nil { - return err - } + // if creating a non-secureboot image, we don;t want to try to enroll keys + sdBootEnrollOption := profile.SDBootEnrollKeysOff.String() - if err := copy.File(options.UKISigningCertDerPath, filepath.Join(options.ScratchDir, "EFI/keys/uki-signing-cert.der")); err != nil { - return err - } + if options.Secureboot { + sdBootEnrollOption = options.SDBootSecureBootEnrollKeys - if options.PlatformKeyPath != "" { - if err := copy.File(options.PlatformKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.PlatformKeyAsset)); err != nil { + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "loader/keys/auto"), 0o755); err != nil { return err } - } - if options.KeyExchangeKeyPath != "" { - if err := copy.File(options.KeyExchangeKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.KeyExchangeKeyAsset)); err != nil { - return err + if options.UKISigningCertDerPath != "" { + if err := os.MkdirAll(filepath.Join(options.ScratchDir, "EFI/keys"), 0o755); err != nil { + return err + } + + if err := copy.File(options.UKISigningCertDerPath, filepath.Join(options.ScratchDir, "EFI/keys/uki-signing-cert.der")); err != nil { + return err + } } - } - if options.SignatureKeyPath != "" { - if err := copy.File(options.SignatureKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.SignatureKeyAsset)); err != nil { - return err + if options.PlatformKeyPath != "" { + if err := copy.File(options.PlatformKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.PlatformKeyAsset)); err != nil { + return err + } } + + if options.KeyExchangeKeyPath != "" { + if err := copy.File(options.KeyExchangeKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.KeyExchangeKeyAsset)); err != nil { + return err + } + } + + if options.SignatureKeyPath != "" { + if err := copy.File(options.SignatureKeyPath, filepath.Join(options.ScratchDir, "loader/keys/auto", constants.SignatureKeyAsset)); err != nil { + return err + } + } + } + + printf("preparing loader.conf") + + var loaderConfigOut bytes.Buffer + + if err := template.Must(template.New("loader.conf").Parse(loaderConfigTemplate)).Execute(&loaderConfigOut, struct { + SecureBootEnroll string + }{ + SecureBootEnroll: sdBootEnrollOption, + }); err != nil { + return fmt.Errorf("error rendering loader.conf: %w", err) + } + + if err := os.WriteFile(filepath.Join(options.ScratchDir, "loader/loader.conf"), loaderConfigOut.Bytes(), 0o644); err != nil { + return err } if _, err := cmd.Run( diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 5b837ec058..687d7a5584 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -84,15 +84,12 @@ func (i *Imager) outCmdline(path string) error { } //nolint:gocyclo -func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Reporter) error { +func (i *Imager) outISOUKI(ctx context.Context, path string, report *reporter.Reporter) error { printf := progressPrintf(report, reporter.Update{Message: "building ISO...", Status: reporter.StatusRunning}) scratchSpace := filepath.Join(i.tempDir, "iso") - var ( - err error - zeroContainerAsset profile.ContainerAsset - ) + var zeroContainerAsset profile.ContainerAsset if i.prof.Input.ImageCache != zeroContainerAsset { if err := os.MkdirAll(filepath.Join(scratchSpace, "imagecache"), 0o755); err != nil { @@ -104,12 +101,27 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor } } + options := iso.UEFIOptions{ + UKIPath: i.ukiPath, + SDBootPath: i.sdBootPath, + + Arch: i.prof.Arch, + Version: i.prof.Version, + + ScratchDir: scratchSpace, + OutPath: path, + + Secureboot: i.prof.SecureBootEnabled(), + } + if i.prof.SecureBootEnabled() { isoOptions := pointer.SafeDeref(i.prof.Output.ISOOptions) + options.SDBootSecureBootEnrollKeys = isoOptions.SDBootEnrollKeys.String() + var signer pesign.CertificateSigner - signer, err = i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) + signer, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) if err != nil { return fmt.Errorf("failed to get SecureBoot signer: %w", err) } @@ -120,24 +132,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor return fmt.Errorf("failed to write uki.der: %w", err) } - options := iso.UEFIOptions{ - UKIPath: i.ukiPath, - SDBootPath: i.sdBootPath, - - SDBootSecureBootEnrollKeys: isoOptions.SDBootEnrollKeys.String(), - - UKISigningCertDerPath: derCrtPath, - - PlatformKeyPath: i.prof.Input.SecureBoot.PlatformKeyPath, - KeyExchangeKeyPath: i.prof.Input.SecureBoot.KeyExchangeKeyPath, - SignatureKeyPath: i.prof.Input.SecureBoot.SignatureKeyPath, - - Arch: i.prof.Arch, - Version: i.prof.Version, - - ScratchDir: scratchSpace, - OutPath: path, - } + options.UKISigningCertDerPath = derCrtPath if i.prof.Input.SecureBoot.PlatformKeyPath == "" { report.Report(reporter.Update{Message: "generating SecureBoot database...", Status: reporter.StatusRunning}) @@ -178,21 +173,43 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor options.KeyExchangeKeyPath = i.prof.Input.SecureBoot.KeyExchangeKeyPath options.SignatureKeyPath = i.prof.Input.SecureBoot.SignatureKeyPath } + } - err = iso.CreateUEFI(printf, options) - } else { - err = iso.CreateGRUB(printf, iso.GRUBOptions{ - KernelPath: i.prof.Input.Kernel.Path, - InitramfsPath: i.initramfsPath, - Cmdline: i.cmdline, - Version: i.prof.Version, + if err := iso.CreateUEFI(printf, options); err != nil { + return fmt.Errorf("failed to create UEFI ISO: %w", err) + } - ScratchDir: scratchSpace, - OutPath: path, - }) + report.Report(reporter.Update{Message: "ISO ready", Status: reporter.StatusSucceeded}) + + return nil +} + +func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Reporter) error { + printf := progressPrintf(report, reporter.Update{Message: "building legacy ISO...", Status: reporter.StatusRunning}) + + scratchSpace := filepath.Join(i.tempDir, "iso") + + var zeroContainerAsset profile.ContainerAsset + + if i.prof.Input.ImageCache != zeroContainerAsset { + if err := os.MkdirAll(filepath.Join(scratchSpace, "imagecache"), 0o755); err != nil { + return err + } + + if err := i.prof.Input.ImageCache.Extract(ctx, filepath.Join(scratchSpace, "imagecache"), i.prof.Arch, printf); err != nil { + return err + } } - if err != nil { + if err := iso.CreateGRUB(printf, iso.GRUBOptions{ + KernelPath: i.prof.Input.Kernel.Path, + InitramfsPath: i.initramfsPath, + Cmdline: i.cmdline, + Version: i.prof.Version, + + ScratchDir: scratchSpace, + OutPath: path, + }); err != nil { return err } diff --git a/pkg/imager/profile/default.go b/pkg/imager/profile/default.go index b780ff3ef8..d62b95415a 100644 --- a/pkg/imager/profile/default.go +++ b/pkg/imager/profile/default.go @@ -31,6 +31,14 @@ var Default = map[string]Profile{ OutFormat: OutFormatRaw, }, }, + "iso-uki": { + Platform: constants.PlatformMetal, + SecureBoot: pointer.To(false), + Output: Output{ + Kind: OutKindISOUKI, + OutFormat: OutFormatRaw, + }, + }, "secureboot-iso": { Platform: constants.PlatformMetal, SecureBoot: pointer.To(true), @@ -55,6 +63,18 @@ var Default = map[string]Profile{ }, }, }, + "metal-uki": { + Platform: constants.PlatformMetal, + SecureBoot: pointer.To(false), + Output: Output{ + Kind: OutKindImageUKI, + OutFormat: OutFormatZSTD, + ImageOptions: &ImageOptions{ + DiskSize: MinRAWDiskSize, + DiskFormat: DiskFormatRaw, + }, + }, + }, "secureboot-metal": { Platform: constants.PlatformMetal, SecureBoot: pointer.To(true), diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 0c891ffa6d..137f7f46b5 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -209,15 +209,15 @@ func (i *Input) FillDefaults(arch, version string, secureboot bool) { i.BaseInstaller.ImageRef = fmt.Sprintf("%s:%s", images.DefaultInstallerImageRepository, version) } - if secureboot { - if i.SDStub == zeroFileAsset { - i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch) - } + if i.SDStub == zeroFileAsset { + i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch) + } - if i.SDBoot == zeroFileAsset { - i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch) - } + if i.SDBoot == zeroFileAsset { + i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch) + } + if secureboot { if i.SecureBoot == nil { i.SecureBoot = &SecureBootAssets{} } diff --git a/pkg/imager/profile/output.go b/pkg/imager/profile/output.go index 715a69f58e..9fa4d8c3c2 100644 --- a/pkg/imager/profile/output.go +++ b/pkg/imager/profile/output.go @@ -60,6 +60,8 @@ const ( OutKindInitramfs // initramfs OutKindUKI // uki OutKindCmdline // cmdline + OutKindISOUKI // iso-uki + OutKindImageUKI // image-uki ) // OutFormat is output format specification. diff --git a/pkg/imager/profile/outputkind_enumer.go b/pkg/imager/profile/outputkind_enumer.go index 0139428ab2..f468b07258 100644 --- a/pkg/imager/profile/outputkind_enumer.go +++ b/pkg/imager/profile/outputkind_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _OutputKindName = "unknownisoimageinstallerkernelinitramfsukicmdline" +const _OutputKindName = "unknownisoimageinstallerkernelinitramfsukicmdlineiso-legacy" -var _OutputKindIndex = [...]uint8{0, 7, 10, 15, 24, 30, 39, 42, 49} +var _OutputKindIndex = [...]uint8{0, 7, 10, 15, 24, 30, 39, 42, 49, 59} -const _OutputKindLowerName = "unknownisoimageinstallerkernelinitramfsukicmdline" +const _OutputKindLowerName = "unknownisoimageinstallerkernelinitramfsukicmdlineiso-legacy" func (i OutputKind) String() string { if i < 0 || i >= OutputKind(len(_OutputKindIndex)-1) { @@ -32,9 +32,10 @@ func _OutputKindNoOp() { _ = x[OutKindInitramfs-(5)] _ = x[OutKindUKI-(6)] _ = x[OutKindCmdline-(7)] + _ = x[OutKindISOUKI-(8)] } -var _OutputKindValues = []OutputKind{OutKindUnknown, OutKindISO, OutKindImage, OutKindInstaller, OutKindKernel, OutKindInitramfs, OutKindUKI, OutKindCmdline} +var _OutputKindValues = []OutputKind{OutKindUnknown, OutKindISO, OutKindImage, OutKindInstaller, OutKindKernel, OutKindInitramfs, OutKindUKI, OutKindCmdline, OutKindISOUKI} var _OutputKindNameToValueMap = map[string]OutputKind{ _OutputKindName[0:7]: OutKindUnknown, @@ -53,6 +54,8 @@ var _OutputKindNameToValueMap = map[string]OutputKind{ _OutputKindLowerName[39:42]: OutKindUKI, _OutputKindName[42:49]: OutKindCmdline, _OutputKindLowerName[42:49]: OutKindCmdline, + _OutputKindName[49:59]: OutKindISOUKI, + _OutputKindLowerName[49:59]: OutKindISOUKI, } var _OutputKindNames = []string{ @@ -64,6 +67,7 @@ var _OutputKindNames = []string{ _OutputKindName[30:39], _OutputKindName[39:42], _OutputKindName[42:49], + _OutputKindName[49:59], } // OutputKindString retrieves an enum value from the enum constants string name. diff --git a/pkg/imager/profile/profile.go b/pkg/imager/profile/profile.go index 2cb244ed2d..4fe9e0643f 100644 --- a/pkg/imager/profile/profile.go +++ b/pkg/imager/profile/profile.go @@ -94,7 +94,7 @@ func (p *Profile) Validate() error { switch p.Output.Kind { case OutKindUnknown: return errors.New("unknown output kind") - case OutKindISO: + case OutKindISO, OutKindISOUKI: // ISO supports all kinds of customization case OutKindCmdline: // cmdline supports all kinds of customization @@ -124,9 +124,6 @@ func (p *Profile) Validate() error { return fmt.Errorf("customization of meta partition is not supported for %s output", p.Output.Kind) } case OutKindUKI: - if !p.SecureBootEnabled() { - return fmt.Errorf("!secureboot is not supported for %s output", p.Output.Kind) - } } return nil @@ -153,6 +150,8 @@ func (p *Profile) OutputPath() string { panic("unknown output kind") case OutKindISO: path += ".iso" + case OutKindISOUKI: + path += "-legacy.iso" case OutKindImage: path += "." + p.Output.ImageOptions.DiskFormat.String() case OutKindInstaller: