Skip to content

Commit

Permalink
Merge pull request #303 from chrisccoulson/efi-support-firmware-agent…
Browse files Browse the repository at this point in the history
…s-in-pcr4

efi: PCR4: Add support for certain firmware applications loaded during OS-present.

Some newer laptops are loading endpoint management applications that are
shipped with firmware, and these are being loaded as part of the OS-present
environment with the LoadImage API, resulting in them being measured to PCR4.
This currently causes us to mis-predict values for PCR4 because everything
after the separator (the pre-OS to OS-present transition) is assumed to
belong to the OS and gets dropped from the predicted measurements.

A future PR will provide support for detecting compatibility for FDE,
with an option for detecting and disallowing these firmware applications.
In most cases, they should be disabled (and it's possible to disable
Absolute from userspace on Dell laptops), but we should add support here
for specific, known endpoint management applications for any edge cases where
these agents cannot be disabled.

This adds support for any applications where the firmware volume
filename GUID is well known and maps to "AbsoluteAbtInstaller" or
"AbsoluteComputraceInstaller".
  • Loading branch information
chrisccoulson authored Jun 20, 2024
2 parents aafa7ff + e51f4c6 commit 54378e6
Show file tree
Hide file tree
Showing 8 changed files with 537 additions and 21 deletions.
72 changes: 62 additions & 10 deletions efi/fw_load_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
efi "github.com/canonical/go-efilib"
"github.com/canonical/go-tpm2"
"github.com/canonical/tcglog-parser"
"github.com/snapcore/secboot/efi/internal"
"golang.org/x/xerrors"
)

Expand Down Expand Up @@ -227,31 +228,82 @@ 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. This seems sensible, but newer Dell
// devices load an agent from firmware before shim is executed and measure this to PCR4 as part of the
// OS-present environment, which seems wrong. The approach here assumes that the EDK2 behaviour is
// correct.
for _, event := range h.log.Events {
// 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
for len(events) > 0 {
event := events[0]
events = events[1:]

if event.PCRIndex != tcglog.PCRIndex(bootManagerCodePCR) {
continue
}

if event.EventType == tcglog.EventTypeSeparator {
return h.measureSeparator(ctx, bootManagerCodePCR, event)
if err := h.measureSeparator(ctx, bootManagerCodePCR, event); err != nil {
return err
}
measuredSeparator = true
break
}
ctx.ExtendPCR(bootManagerCodePCR, tpm2.Digest(event.Digests[ctx.PCRAlg()]))
}

return errors.New("missing separator")
if !measuredSeparator {
return errors.New("missing separator")
}

// 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 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:]

if event.PCRIndex != tcglog.PCRIndex(bootManagerCodePCR) {
continue
}
if event.EventType != tcglog.EventTypeEFIBootServicesApplication {
return fmt.Errorf("unexpected OS-present event type: %v", event.EventType)
}

// once we encounter the first EV_EFI_BOOT_SERVICES_APPLICATION event in PCR4, this loop alway
// breaks or returns an error.

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)
}
if isAbsolute {
// copy the digest to the policy
ctx.ExtendPCR(bootManagerCodePCR, tpm2.Digest(event.Digests[ctx.PCRAlg()]))
}
// 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
}

// MeasureImageStart implements imageLoadHandler.MeasureImageStart.
Expand Down
112 changes: 110 additions & 2 deletions efi/fw_load_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,69 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartBootManagerCodeProfileIncludeS
})
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartBootManagerCodeProfileIncludeAbsoluteAbtInstaller(c *C) {
// Verify the events associated with the "AbsoluteAbtInstaller" application contained in the firmware
// that loads as part of the OS-present.
vars := makeMockVars(c, withMsSecureBootConfig())
s.testMeasureImageStart(c, &testFwMeasureImageStartData{
vars: vars,
logOptions: &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x821aca26, 0x29ea, 0x4993, 0x839f, [...]byte{0x59, 0x7f, 0xc0, 0x21, 0x70, 0x8d}),
},
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")},
{pcr: 4, eventType: mockPcrBranchExtendEvent, digest: testutil.DecodeHexString(c, "59b1f92051a43fea7ac3a846f2714c3e041a4153d581acd585914bcff2ad2781")},
},
})
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartBootManagerCodeProfileIncludeAbsoluteComputraceInstaller(c *C) {
// Verify the events associated with the "AbsoluteComputraceInstaller" application contained in the firmware
// that loads as part of the OS-present.
vars := makeMockVars(c, withMsSecureBootConfig())
s.testMeasureImageStart(c, &testFwMeasureImageStartData{
vars: vars,
logOptions: &efitest.LogOptions{
Algorithms: []tpm2.HashAlgorithmId{tpm2.HashAlgorithmSHA256, tpm2.HashAlgorithmSHA1},
IncludeOSPresentFirmwareAppLaunch: efi.MakeGUID(0x8feeecf1, 0xbcfd, 0x4a78, 0x9231, [...]byte{0x48, 0x01, 0x56, 0x6b, 0x35, 0x67}),
},
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")},
{pcr: 4, eventType: mockPcrBranchExtendEvent, digest: testutil.DecodeHexString(c, "e58b9aa46c99806ce57c805a78d8224dd174743341e03e8a68b13a0071785295")},
},
})
}

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 @@ -390,13 +453,13 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR0_1(c *C) {
continue
}
// Overwrite the event data with a mock error event
log.Events[i].Data = &mockErrLogData{fmt.Errorf("cannot decode StartupLocality data: %w", io.EOF)}
log.Events[i].Data = &mockErrLogData{fmt.Errorf("cannot decode StartupLocality data: %w", io.ErrUnexpectedEOF)}
break
}
}

handler := NewFwLoadHandler(log)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure platform firmware: cannot decode EV_NO_ACTION event data: cannot decode StartupLocality data: EOF`)
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure platform firmware: cannot decode EV_NO_ACTION event data: cannot decode StartupLocality data: unexpected EOF`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR0_2(c *C) {
Expand Down Expand Up @@ -428,6 +491,51 @@ func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR0_2(c *C) {
c.Check(handler.MeasureImageStart(ctx), ErrorMatches, `cannot measure platform firmware: log for PCR0 has an unexpected StartupLocality event`)
}

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_1(c *C) {
// Insert an unexpected event type 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].EventType = tcglog.EventTypeAction
break
}
}

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

func (s *fwLoadHandlerSuite) TestMeasureImageStartErrBadLogPCR4_2(c *C) {
// 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,
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 = efi.DevicePath{}
log.Events[i].Data = data
break
}
}

handler := NewFwLoadHandler(log)
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 {
// Insert an invalid error separator event into the log for the specified pcr
collector := NewRootVarsCollector(efitest.NewMockHostEnvironment(nil, nil))
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 54378e6

Please sign in to comment.