diff --git a/efi/efi_test.go b/efi/efi_test.go index 5877d12b..0663ed71 100644 --- a/efi/efi_test.go +++ b/efi/efi_test.go @@ -137,6 +137,8 @@ type mockPeImageHandle struct { func (*mockPeImageHandle) Close() error { return nil } func (h *mockPeImageHandle) Source() Image { return h.mockImage } +func (h *mockPeImageHandle) Machine() uint16 { return 0 } + func (h *mockPeImageHandle) OpenSection(name string) *io.SectionReader { data, exists := h.sections[name] if !exists { @@ -176,6 +178,10 @@ func (h *mockPeImageHandle) newShimImageHandle() *mockShimImageHandle { return &mockShimImageHandle{mockPeImageHandle: h} } +func (h *mockPeImageHandle) newGrubImageHandle() *mockGrubImageHandle { + return &mockGrubImageHandle{mockPeImageHandle: h} +} + type mockShimImageHandle struct { *mockPeImageHandle } @@ -205,6 +211,14 @@ func (h *mockShimImageHandle) ReadSbatLevel() (ShimSbatLevel, error) { return *h.shimSbatLevel, nil } +type mockGrubImageHandle struct { + *mockPeImageHandle +} + +func (h *mockGrubImageHandle) Prefix() (string, error) { + return h.grubPrefix, nil +} + type mockImage struct { sections map[string][]byte sbat []SbatComponent @@ -216,6 +230,8 @@ type mockImage struct { shimVendorDb efi.SignatureDatabase shimVendorDbFormat ShimVendorCertFormat shimSbatLevel *ShimSbatLevel + + grubPrefix string } func newMockImage() *mockImage { @@ -278,6 +294,11 @@ func (i *mockImage) withShimSbatLevel(sbatLevel ShimSbatLevel) *mockImage { return i } +func (i *mockImage) withGrubPrefix(prefix string) *mockImage { + i.grubPrefix = prefix + return i +} + func newMockUbuntuShimImage15a(c *C) *mockImage { return newMockImage(). appendSignatures(efitest.ReadWinCertificateAuthenticodeDetached(c, shimUbuntuSig1)). @@ -318,7 +339,8 @@ func newMockUbuntuShimImage15_7(c *C) *mockImage { func newMockUbuntuGrubImage1(c *C) *mockImage { return newMockImage(). appendSignatures(efitest.ReadWinCertificateAuthenticodeDetached(c, grubUbuntuSig1)). - addSection("mods", nil) + addSection("mods", nil). + withGrubPrefix("/EFI/ubuntu") } func newMockUbuntuGrubImage2(c *C) *mockImage { @@ -328,7 +350,8 @@ func newMockUbuntuGrubImage2(c *C) *mockImage { withSbat([]SbatComponent{ {Name: "grub"}, {Name: "grub.ubuntu"}, - }) + }). + withGrubPrefix("/EFI/ubuntu") } func newMockUbuntuGrubImage3(c *C) *mockImage { @@ -338,7 +361,8 @@ func newMockUbuntuGrubImage3(c *C) *mockImage { withSbat([]SbatComponent{ {Name: "grub"}, {Name: "grub.ubuntu"}, - }) + }). + withGrubPrefix("/EFI/ubuntu") } func newMockUbuntuKernelImage1(c *C) *mockImage { @@ -433,6 +457,27 @@ func (m *mockShimImageHandleMixin) TearDownTest(c *C) { } } +type mockGrubImageHandleMixin struct { + restore func() +} + +func (m *mockGrubImageHandleMixin) SetUpTest(c *C) { + orig := NewGrubImageHandle + m.restore = MockNewGrubImageHandle(func(image PeImageHandle) GrubImageHandle { + h, ok := image.(*mockPeImageHandle) + if !ok { + return orig(image) + } + return h.newGrubImageHandle() + }) +} + +func (m *mockGrubImageHandleMixin) TearDownTest(c *C) { + if m.restore != nil { + m.restore() + } +} + type mockImageLoadHandlerMap map[Image]ImageLoadHandler func (h mockImageLoadHandlerMap) LookupHandler(image PeImageHandle) (ImageLoadHandler, error) { diff --git a/efi/export_test.go b/efi/export_test.go index 107a65b5..425a1221 100644 --- a/efi/export_test.go +++ b/efi/export_test.go @@ -58,6 +58,7 @@ var ( MustParseShimVersion = mustParseShimVersion NewestSbatLevel = newestSbatLevel NewFwLoadHandler = newFwLoadHandler + NewGrubImageHandle = newGrubImageHandle NewImageLoadHandlerLazyMap = newImageLoadHandlerLazyMap NewImageRule = newImageRule NewImageRules = newImageRules @@ -86,6 +87,7 @@ var ( // unexported members of some unexported types. type FwContext = fwContext type GrubFlags = grubFlags +type GrubImageHandle = grubImageHandle type GrubLoadHandler = grubLoadHandler type ImageLoadHandler = imageLoadHandler type ImageLoadHandlerMap = imageLoadHandlerMap @@ -163,6 +165,14 @@ func MockNewFwLoadHandler(fn func(*tcglog.Log) ImageLoadHandler) (restore func() } } +func MockNewGrubImageHandle(fn func(peImageHandle) grubImageHandle) (restore func()) { + orig := newGrubImageHandle + newGrubImageHandle = fn + return func() { + newGrubImageHandle = orig + } +} + func MockNewShimImageHandle(fn func(peImageHandle) shimImageHandle) (restore func()) { orig := newShimImageHandle newShimImageHandle = fn diff --git a/efi/grub.go b/efi/grub.go new file mode 100644 index 00000000..6e3d5a3a --- /dev/null +++ b/efi/grub.go @@ -0,0 +1,158 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2023 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 efi + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "math" + + pe "github.com/snapcore/secboot/internal/pe1.14" +) + +const grubObjTypePrefix uint32 = 3 + +type grubModuleHeader struct { + Type uint32 + Size uint32 +} + +const grubModuleMagic uint32 = 0x676d696d + +type grubModuleInfo32 struct { + Magic uint32 + Offset uint32 + Size uint32 +} + +type grubModuleInfo64 struct { + Magic uint32 + Padding uint32 + Offset uint64 + Size uint64 +} + +type grubModule struct { + Type uint32 + *io.SectionReader +} + +type grubImageHandle interface { + peImageHandle + + Prefix() (string, error) +} + +type grubImageHandleImpl struct { + peImageHandle +} + +// newGrubImageHandle returns a new grubImageHandle for the supplied peImageHandle. +var newGrubImageHandle = func(image peImageHandle) grubImageHandle { + return &grubImageHandleImpl{peImageHandle: image} +} + +func (h *grubImageHandleImpl) mods() ([]grubModule, error) { + section := h.OpenSection("mods") + if section == nil { + return nil, errors.New("no mods section") + } + + var r *io.SectionReader + switch h.Machine() { + case pe.IMAGE_FILE_MACHINE_AMD64, pe.IMAGE_FILE_MACHINE_ARM64, pe.IMAGE_FILE_MACHINE_RISCV64: + var info grubModuleInfo64 + if err := binary.Read(section, binary.LittleEndian, &info); err != nil { + return nil, fmt.Errorf("cannot obtain modules info: %w", err) + } + if info.Magic != grubModuleMagic { + return nil, errors.New("invalid modules magic") + } + if info.Size > math.MaxInt64 { + return nil, errors.New("invalid modules offset") + } + if info.Size > math.MaxInt64 || info.Size < uint64(binary.Size(info)) { + return nil, errors.New("invalid modules size") + } + r = io.NewSectionReader(section, int64(info.Offset), int64(info.Size)-int64(binary.Size(info))) + case pe.IMAGE_FILE_MACHINE_ARM, pe.IMAGE_FILE_MACHINE_I386, pe.IMAGE_FILE_MACHINE_RISCV32: + var info grubModuleInfo32 + if err := binary.Read(section, binary.LittleEndian, &info); err != nil { + return nil, fmt.Errorf("cannot obtain modules info: %w", err) + } + if info.Magic != grubModuleMagic { + return nil, errors.New("invalid module magic") + } + if info.Size < uint32(binary.Size(info.Size)) { + return nil, errors.New("invalid modules size") + } + r = io.NewSectionReader(section, int64(info.Offset), int64(info.Size)-int64(binary.Size(info))) + default: + return nil, fmt.Errorf("unrecognized machine: %d", h.Machine()) + } + + var mods []grubModule + + for { + var hdr grubModuleHeader + if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("cannot obtain module header: %w", err) + } + + offset, _ := r.Seek(0, io.SeekCurrent) + size := int64(hdr.Size) - int64(binary.Size(hdr)) + mods = append(mods, grubModule{ + Type: hdr.Type, + SectionReader: io.NewSectionReader(r, offset, size), + }) + + if _, err := r.Seek(size, io.SeekCurrent); err != nil { + return nil, fmt.Errorf("cannot seek to next module: %w", err) + } + } + + return mods, nil +} + +func (h *grubImageHandleImpl) Prefix() (string, error) { + mods, err := h.mods() + if err != nil { + return "", err + } + + for _, mod := range mods { + if mod.Type != grubObjTypePrefix { + continue + } + + prefix, err := io.ReadAll(newCstringReader(mod)) + if err != nil { + return "", fmt.Errorf("cannot obtain prefix: %w", err) + } + return string(prefix), nil + } + + return "", nil +} diff --git a/efi/image_load_handler_test.go b/efi/image_load_handler_test.go index 88436b3f..33a8f82d 100644 --- a/efi/image_load_handler_test.go +++ b/efi/image_load_handler_test.go @@ -48,6 +48,17 @@ func (c *mockImageLoadHandlerConstructor) NewImageLoadHandler(image PeImageHandl type imageLoadHandlerSuite struct { mockShimImageHandleMixin + mockGrubImageHandleMixin +} + +func (s *imageLoadHandlerSuite) SetUpTest(c *C) { + s.mockShimImageHandleMixin.SetUpTest(c) + s.mockGrubImageHandleMixin.SetUpTest(c) +} + +func (s *imageLoadHandlerSuite) TearDownTest(c *C) { + s.mockShimImageHandleMixin.TearDownTest(c) + s.mockGrubImageHandleMixin.TearDownTest(c) } var _ = Suite(&imageLoadHandlerSuite{}) @@ -131,5 +142,5 @@ func (s *imageLoadHandlerSuite) TestDefaultLookupFallback(c *C) { handler, err := m.LookupHandler(image.newPeImageHandle()) c.Assert(err, IsNil) c.Assert(handler, testutil.ConvertibleTo, &GrubLoadHandler{}) - c.Check(handler, DeepEquals, new(GrubLoadHandler)) + c.Check(handler.(*GrubLoadHandler).Flags, Equals, GrubChainloaderUsesShimProtocol) } diff --git a/efi/image_rules.go b/efi/image_rules.go index 343f5b15..637832e8 100644 --- a/efi/image_rules.go +++ b/efi/image_rules.go @@ -241,3 +241,15 @@ func (p *shimVersionPredicate) Matches(image peImageHandle) (bool, error) { return false, fmt.Errorf("invalid shim version operator %s", p.operator) } } + +type grubHasPrefix string + +func (p grubHasPrefix) Matches(image peImageHandle) (bool, error) { + grub := newGrubImageHandle(image) + prefix, err := grub.Prefix() + if err != nil { + return false, fmt.Errorf("cannot obtain grub prefix: %w", err) + } + + return prefix == string(p), nil +} diff --git a/efi/image_rules_defs.go b/efi/image_rules_defs.go index 5b3e12fb..b30ac284 100644 --- a/efi/image_rules_defs.go +++ b/efi/image_rules_defs.go @@ -163,7 +163,7 @@ func makeMicrosoftUEFICASecureBootNamespaceRules() *secureBootNamespaceRules { ), imageMatchesAll( imageSectionExists("mods"), - imageSignedByOrganization("Canonical Ltd."), + grubHasPrefix("/EFI/ubuntu"), ), ), newGrubLoadHandlerConstructor(grubChainloaderUsesShimProtocol).New, @@ -209,14 +209,22 @@ func makeFallbackImageRules() *imageRules { imageSectionExists(".vendor_cert"), newShimLoadHandler, ), + // Ubuntu grub + newImageRule( + "grub", + imageMatchesAll( + imageSectionExists("mods"), + grubHasPrefix("/EFI/ubuntu"), + ), + newGrubLoadHandlerConstructor(grubChainloaderUsesShimProtocol).New, + ), // Grub newImageRule( "grub", imageSectionExists("mods"), newGrubLoadHandler, ), - // TODO: add rules for Ubuntu Core UKI and Ubuntu grub that are not part of - // the MS UEFI CA? + // TODO: add rules for Ubuntu Core UKIs that are not part of the MS UEFI CA // // Catch-all for unrecognized leaf images newImageRule( diff --git a/efi/image_rules_defs_test.go b/efi/image_rules_defs_test.go index 8f37b1b9..6ab36195 100644 --- a/efi/image_rules_defs_test.go +++ b/efi/image_rules_defs_test.go @@ -34,6 +34,17 @@ import ( type imageRulesDefsSuite struct { mockShimImageHandleMixin + mockGrubImageHandleMixin +} + +func (s *imageRulesDefsSuite) SetUpTest(c *C) { + s.mockShimImageHandleMixin.SetUpTest(c) + s.mockGrubImageHandleMixin.SetUpTest(c) +} + +func (s *imageRulesDefsSuite) TearDownTest(c *C) { + s.mockShimImageHandleMixin.TearDownTest(c) + s.mockGrubImageHandleMixin.TearDownTest(c) } var _ = Suite(&imageRulesDefsSuite{}) @@ -237,7 +248,7 @@ func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerShim(c *C) { c.Check(shimHandler.SbatLevel, DeepEquals, ShimSbatLevel{[]byte("sbat,1,2022111500\nshim,2\ngrub,3\n"), []byte("sbat,1,2022052400\ngrub,2\n")}) } -func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerGrub(c *C) { +func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerUbuntuGrub(c *C) { // verify that grub is recognized by the fallback rules image := newMockUbuntuGrubImage1(c) @@ -245,9 +256,21 @@ func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerGrub(c *C) { handler, err := rules.NewImageLoadHandler(image.newPeImageHandle()) c.Assert(err, IsNil) c.Assert(handler, testutil.ConvertibleTo, &GrubLoadHandler{}) - c.Check(handler.(*GrubLoadHandler), DeepEquals, new(GrubLoadHandler)) + c.Check(handler.(*GrubLoadHandler).Flags, Equals, GrubChainloaderUsesShimProtocol) } +func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerGrub(c *C) { + // verify that grub is recognized by the fallback rules + image := newMockImage(). + addSection("mods", nil). + withGrubPrefix("/EFI/debian") + + rules := MakeFallbackImageRules() + handler, err := rules.NewImageLoadHandler(image.newPeImageHandle()) + c.Assert(err, IsNil) + c.Assert(handler, testutil.ConvertibleTo, &GrubLoadHandler{}) + c.Check(handler.(*GrubLoadHandler), DeepEquals, new(GrubLoadHandler)) +} func (s *imageRulesDefsSuite) TestFallbackNewImageLoadHandlerNull(c *C) { // verify that an unrecognized leaf image is recognized by the fallback rules image := newMockImage() diff --git a/efi/pcr_profile_test.go b/efi/pcr_profile_test.go index 73d8cdb5..d494911c 100644 --- a/efi/pcr_profile_test.go +++ b/efi/pcr_profile_test.go @@ -586,16 +586,19 @@ type pcrProfileSuite struct { restoreNewShimImageHandle func() mockImageHandleMixin mockShimImageHandleMixin + mockGrubImageHandleMixin } func (s *pcrProfileSuite) SetUpTest(c *C) { s.mockImageHandleMixin.SetUpTest(c) s.mockShimImageHandleMixin.SetUpTest(c) + s.mockGrubImageHandleMixin.SetUpTest(c) } func (s *pcrProfileSuite) TearDownTest(c *C) { s.mockImageHandleMixin.TearDownTest(c) s.mockShimImageHandleMixin.TearDownTest(c) + s.mockGrubImageHandleMixin.TearDownTest(c) } var _ = Suite(&pcrProfileSuite{}) diff --git a/efi/pe.go b/efi/pe.go index aba91d88..057947dc 100644 --- a/efi/pe.go +++ b/efi/pe.go @@ -51,6 +51,8 @@ type peImageHandle interface { // Source returns the image source Source() Image + Machine() uint16 + // OpenSection returns a new io.SectionReader for the section with // the specified name, or nil if no section exists. OpenSection(name string) *io.SectionReader @@ -109,6 +111,10 @@ func (h *peImageHandleImpl) Source() Image { return h.source } +func (h *peImageHandleImpl) Machine() uint16 { + return h.pefile.Machine +} + func (h *peImageHandleImpl) OpenSection(name string) *io.SectionReader { section := h.pefile.Section(name) if section == nil {