Skip to content
Draft
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
34 changes: 34 additions & 0 deletions boot/asset_trust_sb.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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))
}
15 changes: 15 additions & 0 deletions boot/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
95 changes: 95 additions & 0 deletions boot/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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},
})
}
8 changes: 8 additions & 0 deletions boot/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
2 changes: 1 addition & 1 deletion bootloader/bootloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bootloader/grub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading