Skip to content

Commit

Permalink
efi: introduce internal package and move Absolute detection there
Browse files Browse the repository at this point in the history
The same code will be shared by the pre-install checks which are
going to go into a new package (efi/preinstall), so create an internal
package for removing some interdependencies and move the Absolute
detection logic there instead
  • Loading branch information
chrisccoulson committed Jun 19, 2024
1 parent c028e6b commit e51f4c6
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 144 deletions.
67 changes: 25 additions & 42 deletions efi/fw_load_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import (
"fmt"

efi "github.com/canonical/go-efilib"
"github.com/canonical/go-efilib/guids"
"github.com/canonical/go-tpm2"
"github.com/canonical/tcglog-parser"
"github.com/snapcore/secboot/efi/internal"
"golang.org/x/xerrors"
)

Expand Down Expand Up @@ -228,17 +228,23 @@ func (h *fwLoadHandler) measureBootManagerCodePreOS(ctx pcrBranchContext) error
// we've enabled follow section 8.2.4 when they measure the first EV_EFI_ACTION event (which is
// optional - firmware should measure a EV_OMIT_BOOT_DEVICE_EVENTS event if they are not measured,
// although some implementations don't do this either). I've not seen any implementations use the
// EV_ACTION events, and these would probably require explicit support here.
// mentioned EV_ACTION events, and these look like they are only relevant to BIOS boot anyway.
//
// The TCG PFP 1.06 r49 cleans this up a bit - it removes reference to the EV_ACTION events, and
// corrects the "Method for measurement" subsection of section 3.3.4.5 to describe that things work
// how we previously assumed. It does introduce a new EV_EFI_ACTION event ("Booting to <Boot####> Option")
// which will require explicit support in this package so it is currently rejected by the
// preinstall.RunChecks logic.
//
// This also retains measurements associated with the launch of any system preparation applications,
// although note that the inclusion of these make a profile inherently fragile. The TCG PC Client PFP
// spec v1.05r23 doesn't specify whether these are launched as part of the pre-OS environment or as
// part of the OS-present environment. It defines the boundary between the pre-OS environment and
// OS-present environment as a separator event measured to PCRs 0-7, but EDK2 measures a separator to
// PCR7 as soon as the secure boot policy is measured and system preparation applications are considered
// part of the pre-OS environment - they are measured to PCR4 before the pre-OS to OS-present transition
// is signalled by measuring separators to the remaining PCRs. The UEFI specification says that system
// preparation applications are executed before the ready to boot signal, which is when the transition
// PCR7 as soon as the secure boot configuration is measured and system preparation applications are
// considered part of the pre-OS environment - they are measured to PCR4 before the pre-OS to OS-present
// transition is signalled by measuring separators to the remaining PCRs. The UEFI specification says that
// system preparation applications are executed before the ready to boot signal, which is when the transition
// from pre-OS to OS-present occurs, so I think we can be confident that we're correct here.
events := h.log.Events
measuredSeparator := false
Expand Down Expand Up @@ -267,8 +273,8 @@ func (h *fwLoadHandler) measureBootManagerCodePreOS(ctx pcrBranchContext) error
// Some newer laptops including those from Dell and Lenovo execute code from a firmware volume as part
// of the OS-present environment, before shim runs, and using the LoadImage API which results in an
// additional measurement to PCR4. Copy this into the profile if it's part of a well-known endpoint
// management application, else return an error. Anything else here will be picked up by the pre-install
// checks.
// management application known as "Absolute" (formerly "Computrace"). Discard anything else which
// will result in an invalid profile but will be picked up by the preinstall.RunChecks API anyway.
for len(events) > 0 {
event := events[0]
events = events[1:]
Expand All @@ -280,44 +286,21 @@ func (h *fwLoadHandler) measureBootManagerCodePreOS(ctx pcrBranchContext) error
return fmt.Errorf("unexpected OS-present event type: %v", event.EventType)
}

data, ok := event.Data.(*tcglog.EFIImageLoadEvent)
if !ok {
return fmt.Errorf("invalid event data for OS-present event: %w", event.Data.(error))
}

if len(data.DevicePath) == 0 {
return errors.New("invalid device path for first OS-present image load event: device path is empty")
}
if _, isFv := data.DevicePath[0].(efi.MediaFvDevicePathNode); !isFv {
// Not loaded from flash, so we'll break here, assuming that this is the first OS component
break
}

// The image is loaded from flash - we should have a path of the form "Fv()\FvFile()".
if len(data.DevicePath) != 2 {
return fmt.Errorf("invalid firmware volume device path (%v) for OS-present image load: invalid length", data.DevicePath)
}
fvf, isFvf := data.DevicePath[1].(efi.MediaFvFileDevicePathNode)
if !isFvf {
// The second component should be the firmware volume file name.
return fmt.Errorf("invalid firmware volume device path (%v) for OS-present image load: doesn't terminate with FvFile", data.DevicePath)
}
// once we encounter the first EV_EFI_BOOT_SERVICES_APPLICATION event in PCR4, this loop alway
// breaks or returns an error.

name, known := guids.FileNameString(efi.GUID(fvf))
if !known {
return fmt.Errorf("unknown image loaded from firmware volume during OS-present: %v", data.DevicePath)
isAbsolute, err := internal.IsAbsoluteAgentLaunch(event)
if err != nil {
return fmt.Errorf("encountered an error determining whether an OS-present launch is related to Absolute: %w", err)
}
switch name {
case "AbsoluteAbtInstaller", "AbsoluteComputraceInstaller":
if isAbsolute {
// copy the digest to the policy
ctx.ExtendPCR(bootManagerCodePCR, tpm2.Digest(event.Digests[ctx.PCRAlg()]))
default:
return fmt.Errorf("unexpected image loaded from firmware volume during OS-present: %v", data.DevicePath)
}
if known {
// We've copied the measurement for "AbsoluteAbtInstaller" or "AbsoluteComputraceInstaller",
// so we're finished here.
break
}
// If it's not Absolute, we assume it's related to the OS launch which we will predict
// later on. If it's something else, discarding it here creates an invalid policy but this is
// picked up by the preinstall.RunChecks API anyway.
break
}

return nil
Expand Down
124 changes: 23 additions & 101 deletions efi/fw_load_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,27 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartBootManagerCodeProfileIncludeA
})
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartBootManagerCodeProfileIgnoreUnknownFirmwareAgentLaunch(c *C) {
// Verify that the profile ignores any firmware application launch that isn't "AbsoluteAbtInstaller" or
// "AbsoluteComputraceInstaller". This will generate an invalid profile, but will be detected by the
// pre-install checks.
vars := makeMockVars(c, withMsSecureBootConfig())
s.testMeasureImageStart(c, &testFwMeasureImageStartData{
vars: vars,
logOptions: &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0xee993080, 0x5197, 0x4d4e, 0xb63c, [...]byte{0xf1, 0xf7, 0x41, 0x3e, 0x33, 0xce}),
},
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR),
expectedEvents: []*mockPcrBranchEvent{
{pcr: 4, eventType: mockPcrBranchResetEvent},
{pcr: 4, eventType: mockPcrBranchExtendEvent, digest: testutil.DecodeHexString(c, "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba")},
{pcr: 4, eventType: mockPcrBranchExtendEvent, digest: testutil.DecodeHexString(c, "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119")},
},
})
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartSecureBootPolicyAndBootManagerCodeProfile(c *C) {
vars := makeMockVars(c, withMsSecureBootConfig())
s.testMeasureImageStart(c, &testFwMeasureImageStartData{
Expand Down Expand Up @@ -492,28 +513,7 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_1(c *C) {
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_2(c *C) {
// Insert invalid event data in the OS-present phase
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR)}, nil, collector.Next())

log := efitest.NewLog(c, &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x821aca26, 0x29ea, 0x4993, 0x839f, [...]byte{0x59, 0x7f, 0xc0, 0x21, 0x70, 0x8d})})
for i, event := range log.Events {
if event.PCRIndex == 4 && event.EventType == tcglog.EventTypeEFIBootServicesApplication {
log.Events[i].Data = &mockErrLogData{io.ErrUnexpectedEOF}
break
}
}

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: invalid event data for OS-present event: unexpected EOF`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_3(c *C) {
// Delete the device path for the first OS-present application launch
// Insert invalid event data in the OS-present phase so that internal.IsAbsoluteAgentLaunch returns an error
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
Expand All @@ -533,85 +533,7 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_3(c *C) {
}

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: invalid device path for first OS-present image load event: device path is empty`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_4(c *C) {
// Delete the FvFile component of the device path for the first OS-present application launch
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR)}, nil, collector.Next())

log := efitest.NewLog(c, &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x821aca26, 0x29ea, 0x4993, 0x839f, [...]byte{0x59, 0x7f, 0xc0, 0x21, 0x70, 0x8d})})
for i, event := range log.Events {
if event.PCRIndex == 4 && event.EventType == tcglog.EventTypeEFIBootServicesApplication {
data, ok := event.Data.(*tcglog.EFIImageLoadEvent)
c.Assert(ok, testutil.IsTrue)
data.DevicePath = data.DevicePath[:1]
log.Events[i].Data = data
break
}
}

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: invalid firmware volume device path \(\\Fv\(983cc241-b4f6-4a85-9733-4c154b3aa327\)\) for OS-present image load: invalid length`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_5(c *C) {
// Replace the FvFile component of the device path for the first OS-present application launch with something else
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR)}, nil, collector.Next())

log := efitest.NewLog(c, &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x821aca26, 0x29ea, 0x4993, 0x839f, [...]byte{0x59, 0x7f, 0xc0, 0x21, 0x70, 0x8d})})
for i, event := range log.Events {
if event.PCRIndex == 4 && event.EventType == tcglog.EventTypeEFIBootServicesApplication {
data, ok := event.Data.(*tcglog.EFIImageLoadEvent)
c.Assert(ok, testutil.IsTrue)
data.DevicePath[1] = efi.FilePathDevicePathNode("/foo")
log.Events[i].Data = data
break
}
}

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: invalid firmware volume device path \(\\Fv\(983cc241-b4f6-4a85-9733-4c154b3aa327\)\\\/foo\) for OS-present image load: doesn't terminate with FvFile`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_6(c *C) {
// Have an unknown component load from firmware as part of the OS-present environment
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR)}, nil, collector.Next())

log := efitest.NewLog(c, &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0xa52f647a, 0x2554, 0x4721, 0x80e5, [...]byte{0x61, 0x7e, 0x4c, 0x50, 0xf2, 0xd8})})

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: unknown image loaded from firmware volume during OS-present: \\Fv\(983cc241-b4f6-4a85-9733-4c154b3aa327\)\\FvFile\(a52f647a-2554-4721-80e5-617e4c50f2d8\)`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_7(c *C) {
// Have an unexpected component load from firmware as part of the OS-present environment
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
ctx := newMockPcrBranchContext(&mockPcrProfileContext{
alg: tpm2.HashAlgorithmSHA256,
pcrs: MakePcrFlags(BootManagerCodePCR)}, nil, collector.Next())

log := efitest.NewLog(c, &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x8218965d, 0x20c0, 0x4dd6, 0x81a0, [...]byte{0x84, 0x5c, 0x52, 0x27, 0x07, 0x43})})

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: unexpected image loaded from firmware volume during OS-present: \\Fv\(983cc241-b4f6-4a85-9733-4c154b3aa327\)\\FvFile\(LenovoSetupDateTimeDxe\)`)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure boot manager code: encountered an error determining whether an OS-present launch is related to Absolute: EV_EFI_BOOT_SERVICES_APPLICATION event has empty device path`)
}

func (s *fwLoadHandlerSuite) testMeasureImageStartErrBadLogSeparatorError(c *C, pcr tpm2.Handle) error {
Expand Down
86 changes: 86 additions & 0 deletions efi/internal/absolute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 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 internal

import (
"fmt"

efi "github.com/canonical/go-efilib"
"github.com/canonical/go-efilib/guids"
"github.com/canonical/tcglog-parser"
)

// IsAbsoluteAgentLaunch returns true if the supplied event corresponds to the launch of an
// application that is associated with the Absolute (formerly Computrace) endpoint management
// firmware. This will return false if the event is not associated with an application launch,
// or the launch is not from a firmware volume, or the launch is from a firmware volume with
// a filename that is not known to be Absolute.
//
// It will return an error if the event data is badly formed, ie, it doesn't decode properly
// to the EFI_IMAGE_LOAD_EVENT structure, there is an empty device path or a badly formed
// firmware device path that begins with a firmware volume that is not followed by a single
// firmware volume filename.
func IsAbsoluteAgentLaunch(ev *tcglog.Event) (bool, error) {
if ev.EventType != tcglog.EventTypeEFIBootServicesApplication {
// Wrong event type
return false, nil
}
data, ok := ev.Data.(*tcglog.EFIImageLoadEvent)
if !ok {
// the data resulting from decode errors is guaranteed to implement the error interface
return false, fmt.Errorf("%s event has wrong data format: %w", tcglog.EventTypeEFIBootServicesApplication, ev.Data.(error))
}
if len(data.DevicePath) == 0 {
return false, fmt.Errorf("%s event has empty device path", tcglog.EventTypeEFIBootServicesApplication)
}

if _, isFv := data.DevicePath[0].(efi.MediaFvDevicePathNode); !isFv {
// Not loaded from a flash volume, so this isn't Absolute
return false, nil
}

// The image is loaded from a flash volume - we should have a path of the form "Fv()\FvFile()".
if len(data.DevicePath) != 2 {
return false, fmt.Errorf("invalid firmware volume device path (%v): invalid length (expected 2 components)", data.DevicePath)
}

// The second component should be the filename in the firmware volume (both firmware volumes and the names
// of files inside those volumes are identified with a GUID, for which there is a public database of well
// known GUIDs).
fvf, isFvf := data.DevicePath[1].(efi.MediaFvFileDevicePathNode)
if !isFvf {
// The second component is not a firmware volume filename
return false, fmt.Errorf("invalid firmware volume device path (%v): doesn't terminate with FvFile", data.DevicePath)
}

// We have a complete firmware volume file path. The Absolute installer application has 2 well
// known names. We can match directly by GUID or do a lookup using data in the public database.
name, known := guids.FileOrVolumeNameString(efi.GUID(fvf))
if !known {
// This is not a well known GUID and is not Absolute.
return false, nil
}
switch name {
case "AbsoluteAbtInstaller", "AbsoluteComputraceInstaller":
return true, nil
default:
return false, nil
}
}
Loading

0 comments on commit e51f4c6

Please sign in to comment.