diff --git a/boot/asset_trust_sb.go b/boot/asset_trust_sb.go new file mode 100644 index 0000000000..754957600b --- /dev/null +++ b/boot/asset_trust_sb.go @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot + +import ( + efi "github.com/canonical/go-efilib" + sb_efi "github.com/snapcore/secboot/efi" +) + +func init() { + checkBootAssetAgainstSignatureDB = checkImageSignatureIsValidForHost +} + +func checkImageSignatureIsValidForHost(assetPath string) error { + return sb_efi.CheckImageSignatureIsValidForHost(efi.DefaultVarContext, sb_efi.FileImage(assetPath)) +} diff --git a/boot/assets.go b/boot/assets.go index 4f2bd8ab02..208471077a 100644 --- a/boot/assets.go +++ b/boot/assets.go @@ -41,6 +41,15 @@ import ( "github.com/snapcore/snapd/strutil" ) +// checkBootAssetAgainstSignatureDB, when set, validates that the boot asset at +// the given file path has at least one secure-boot signature that chains to a +// certificate authority in the host's UEFI authorized signature database (db). +// This prevents installing boot assets (e.g. shim) that the firmware will not +// be able to verify, which would result in an unbootable system. +// +// This is wired to the secboot package when secboot support is available. +var checkBootAssetAgainstSignatureDB func(assetPath string) error + type trustedAssetsCache struct { cacheDir string hash crypto.Hash @@ -677,6 +686,12 @@ func (o *TrustedAssetsUpdateObserver) observeUpdate(bl bootloader.Bootloader, re } } + if checkBootAssetAgainstSignatureDB != nil { + if err := checkBootAssetAgainstSignatureDB(change.After); err != nil { + return gadget.ChangeAbort, fmt.Errorf("cannot use boot asset %q: %v", trustedAssetName, err) + } + } + ta, err := o.cache.Add(change.After, bl.Name(), trustedAssetName) if err != nil { return gadget.ChangeAbort, err diff --git a/boot/assets_test.go b/boot/assets_test.go index 785647cedd..6a3d4b3890 100644 --- a/boot/assets_test.go +++ b/boot/assets_test.go @@ -58,6 +58,14 @@ func (s *assetsSuite) SetUpTest(c *C) { }) s.AddCleanup(restore) + // Most tests use synthetic boot assets (eg plain "C", "D", "shim") that + // are not valid signed EFI binaries. Keep signature validation permissive by + // default and let dedicated tests override this behavior explicitly. + restore = boot.MockCheckBootAssetAgainstSignatureDB(func(assetPath string) error { + return nil + }) + s.AddCleanup(restore) + c.Assert(os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755), IsNil) c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil) @@ -3144,3 +3152,90 @@ func (s *assetsSuite) TestUpdateBootEntryOnInstall(c *C) { c.Check(foundAsset2, Equals, 1) c.Check(foundOther, Equals, 0) } + +func (s *assetsSuite) TestUpdateObserverUpdateRejectsUntrustedAsset(c *C) { + // Test that the observer rejects an update when the new boot asset's + // signature is not trusted by the host's UEFI signature database. + + d := c.MkDir() + root := c.MkDir() + + data := []byte("shim-with-untrusted-sig") + err := os.WriteFile(filepath.Join(d, "shim.efi"), data, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "shim": []string{"one-hash"}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + s.bootloaderWithTrustedAssets(map[string]string{ + "shim.efi": "shim", + }) + + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // mock the signature check to fail + restore := boot.MockCheckBootAssetAgainstSignatureDB(func(assetPath string) error { + c.Check(assetPath, Equals, filepath.Join(d, "shim.efi")) + return fmt.Errorf("cannot find an secure-boot signature that is trusted by the current host's authorized signature database") + }) + defer restore() + + res, err := obs.Observe(gadget.ContentUpdate, gadget.SystemBoot, root, "shim.efi", + &gadget.ContentChange{After: filepath.Join(d, "shim.efi")}) + c.Assert(err, ErrorMatches, `cannot use boot asset "shim": cannot find an secure-boot signature.*`) + c.Check(res, Equals, gadget.ChangeAbort) +} + +func (s *assetsSuite) TestUpdateObserverUpdateAcceptsTrustedAsset(c *C) { + // Test that the observer accepts an update when the new boot asset's + // signature is trusted by the host's UEFI signature database. + + d := c.MkDir() + root := c.MkDir() + + data := []byte("shim-with-trusted-sig") + // SHA3-384 + dataHash := "0190c0f9c71326fa3583550ba4cbfd0d1321f90f11e0d8b75113f603bb00b957bc8d1f40454cef058cec64f9df4e39f9" + err := os.WriteFile(filepath.Join(d, "shim.efi"), data, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "shim": []string{"one-hash"}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + s.bootloaderWithTrustedAssets(map[string]string{ + "shim.efi": "shim", + }) + + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // mock the signature check to pass + restore := boot.MockCheckBootAssetAgainstSignatureDB(func(assetPath string) error { + c.Check(assetPath, Equals, filepath.Join(d, "shim.efi")) + return nil + }) + defer restore() + + res, err := obs.Observe(gadget.ContentUpdate, gadget.SystemBoot, root, "shim.efi", + &gadget.ContentChange{After: filepath.Join(d, "shim.efi")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + // verify the asset was cached and tracked + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "shim": []string{"one-hash", dataHash}, + }) +} diff --git a/boot/export_test.go b/boot/export_test.go index e133a5e200..9420853393 100644 --- a/boot/export_test.go +++ b/boot/export_test.go @@ -258,3 +258,11 @@ func MockCryptsetupSupportsTokenReplace(support bool) (restore func()) { return support }) } + +func MockCheckBootAssetAgainstSignatureDB(f func(assetPath string) error) (restore func()) { + old := checkBootAssetAgainstSignatureDB + checkBootAssetAgainstSignatureDB = f + return func() { + checkBootAssetAgainstSignatureDB = old + } +} diff --git a/bootloader/bootloader.go b/bootloader/bootloader.go index 804c307923..74a2f8fdb7 100644 --- a/bootloader/bootloader.go +++ b/bootloader/bootloader.go @@ -227,7 +227,7 @@ type TrustedAssetsBootloader interface { DefaultCommandLine(candidate bool) (string, error) // TrustedAssets returns a map of relative paths to asset - // identifers. The paths are inside the bootloader's rootdir + // identifiers. The paths are inside the bootloader's rootdir // that are measured in the boot process. The asset // identifiers correspond to the backward compatible names // recorded in the modeenv (CurrentTrustedBootAssets and diff --git a/bootloader/grub.go b/bootloader/grub.go index d91b1b7588..9d4e613232 100644 --- a/bootloader/grub.go +++ b/bootloader/grub.go @@ -603,7 +603,7 @@ func (g *grub) getGrubRunModeTrustedAssets() ([][]taggedPath, error) { } // TrustedAssets returns the map of relative paths to asset -// identifers. The relative paths are relative to the bootloader's +// identifiers. The relative paths are relative to the bootloader's // rootdir. The asset identifiers correspond to the backward // compatible names recorded in the modeenv (CurrentTrustedBootAssets // and CurrentTrustedRecoveryBootAssets). diff --git a/go.mod b/go.mod index c78fb3aeff..218c859558 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.18 // maze.io/x/crypto/afis imported by github.com/snapcore/secboot/tpm2 replace maze.io/x/crypto => github.com/snapcore/maze.io-x-crypto v0.0.0-20190131090603-9b94c9afe066 +// local development: use local secboot with CheckImageSignatureIsValidForHost +replace github.com/snapcore/secboot => ../secboot + require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/canonical/go-efilib v1.7.1-0.20260310185303-7166aa858b24 diff --git a/go.sum b/go.sum index b3c99316b5..59f4eb4291 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,6 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18 h1:A15 github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/snapcore/maze.io-x-crypto v0.0.0-20190131090603-9b94c9afe066 h1:InG0EmriMOiI4YgtQNOo+6fNxzLCYioo3Q3BCVLdMCE= github.com/snapcore/maze.io-x-crypto v0.0.0-20190131090603-9b94c9afe066/go.mod h1:VuAdaITF1MrGzxPU+8GxagM1HW2vg7QhEFEeGHbmEMU= -github.com/snapcore/secboot v0.0.0-20260320145120-26dce572077a h1:eKljZN+SzPKM1ua7KG5E2vQdbw9JQn17tbbukHj6hvw= -github.com/snapcore/secboot v0.0.0-20260320145120-26dce572077a/go.mod h1:+qs2Juv0XZeTmQHJgFTtAd9520h7QUWRDyvSwbJ3xEU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=