Skip to content

Commit

Permalink
Merge pull request #337 from chrisccoulson/preinstall-add-check-pcr7
Browse files Browse the repository at this point in the history
preinstall: Add checks for PCR7.

This adds some checks for PCR7.

The caller supplies a context.Context to which an EFI variable backend
is attached, a internal_efi.HostEnvironment implementation, a TCG log, a
PCR digest algorith (the optimum for this is computed earlier by
another function that does a more general check of the TCG log) and the
image of the initial boot loader for the current boot.

As we're testing the firmware, this only checks the log up to the launch
of the initial boot loader. It ignores events after this, as these are
under the control of the OS and don't rely on any special firmware
features such as EFI_TCG2_PROTOCOL + PE_COFF_IMAGE vs EFI_TCG_PROTOCOL
in the same way that OS components measuring to PCR4 do.

This ensures that secure boot is enabled, else an error is returned, as
WithSecureBootPolicyProfile only generates profiles compatible with
secure boot being enabled.

If the version of UEFI is >= 2.5, it also makes sure that the secure
boot mode is "deployed mode". If the secure boot mode is "user mode",
then the "AuditMode" and "DeployedMode" values are measured to PCR7,
something that WithSecureBootPolicyProfile doesn't support today.
Support for "user mode" will be added in the future, although the public
RunChecks API will probably require a flag to opt in to supporting user
mode, as it is the less secure mode of the 2 (see the documentation for
SecureBootMode in github.com/canonical/go-efilib).

It also reads the "OsIndicationsSupported" variable to test for features
that are not supported by WithSecureBootPolicyProfile. These are
timestamp revocation (which requires an extra signature database -
"dbt") and OS recovery (which requires an extra signature database -
"dbr", used to control access to OsRecoveryOrder and OsRecover####
variables). Of the 2, it's likely that we might need to add support for
timestamp revocation at some point.

It reads the "BootCurrent" EFI variable and matches this to the
EFI_LOAD_OPTION associated with the current boot from the TCG log - it
uses the log as "BootXXXX" EFI variables can be updated at runtime and
might be out of data when this code runs. It uses this to detect the
launch of the initial boot loader, which might not necessarily be the
first EV_EFI_BOOT_SERVICES_APPLICATION event in the OS-present
environment in PCR4 (eg, if Absolute is active).

First of all, it iterates over the secure boot configuration in the log,
making sure that the configuration is measured in the correct order,
that the event data is valid, and that the measured digest is the tagged
hash of the event data. It makes sure that the value of "SecureBoot" in
the log is consistent with the "SecureBoot" variable (which is read-only
at runtime), and it verifies that all of the signature databases are
formatted correctly. It will return an error if any of these checks
fail.

If the pre-OS environment contains events other than
EV_EFI_VARIABLE_DRIVER_CONFIG, it will return an error. This can happen
if a firmware debugger is enabled, in which case PCR7 will begin with a
EV_EFI_ACTION "UEFI Debug Mode" event. This case is detected by earlier
firmware protection checks.

If not all of the expected secure boot configuration is measured, an
error is returned.

Once the secure boot configuration has been measured, it looks for
EV_EFI_VARIABLE_AUTHORITY events in PCR7, until it detects the launch of
the initial boot loader. It verifies that each of these come from db,
and if the log is in the OS-present environment, it ensures that the
measured digest is the tagged hash of the event data. It doesn't do this
for events in the pre-OS environment because WithSecureBootPolicyProfile
just copies these to the profile. It verifies that the firmware doesn't
measure a signature more than once. For each EV_EFI_VARIABLE_AUTHORITY
event, it also matches the measured signature to a EFI_SIGNATURE_LIST
structure in db. If the matched ESL is a X.509 certificate, it records
the use of this CA in the return value. If the CA is an RSA certificate
with a public modulus of < 256 bytes, it sets a flag in the return
value indicating a weak algorithm. If the matched ESL is a Authenticode
digest, it sets a flag in the return value indicating that pre-OS
components were verified using digests rather than signatures. This
makes PCR7 fragile wrt firmware updates, because it means db needs to
be updated to reflect the new components each time. If the digest being
matched is SHA-1, it sets the flag in the return value indicating a weak
algorithm. If any of these checks fail, an error is returned. If an
event type other than EV_EFI_VARIABLE_AUTHORITY is detected, an error is
returned.

Upon detecting the launch of the initial boot loader in PCR4, it
extracts the authenticode signatures from the supplied image, and
matches these to a previously measured CA. If no match is found, an
error is returned. If a match is found, it ensures that the signing
certificate has an RSA public key with a modulus at least as large as
256 bytes, else it sets a flag in the return value indicating a weak algorithm.
Once the event for the initial boot loader is complete, the function
returns.

If the end of the log is reached without encountering the launch of the
initial boot loader, an error is returned.
  • Loading branch information
chrisccoulson authored Nov 21, 2024
2 parents b240ede + c281620 commit a5c8f67
Show file tree
Hide file tree
Showing 22 changed files with 3,219 additions and 774 deletions.
65 changes: 2 additions & 63 deletions efi/pe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import (
"crypto"
"encoding/csv"
"errors"
"fmt"
"io"
"strconv"

efi "github.com/canonical/go-efilib"
"golang.org/x/xerrors"

internal_efi "github.com/snapcore/secboot/internal/efi"
pe "github.com/snapcore/secboot/internal/pe1.14"
)

Expand Down Expand Up @@ -179,73 +179,12 @@ func (h *peImageHandleImpl) SbatComponents() ([]sbatComponent, error) {
return components, nil
}

const (
certTableIndex = 4 // Index of the Certificate Table entry in the Data Directory of a PE image optional header
)

func (h *peImageHandleImpl) ImageDigest(alg crypto.Hash) ([]byte, error) {
return efi.ComputePeImageDigest(alg, h.r, h.r.Size())
}

func (h *peImageHandleImpl) SecureBootSignatures() ([]*efi.WinCertificateAuthenticode, error) {
// Obtain security directory entry from optional header
var dd []pe.DataDirectory
switch oh := h.pefile.OptionalHeader.(type) {
case *pe.OptionalHeader32:
dd = oh.DataDirectory[0:oh.NumberOfRvaAndSizes]
case *pe.OptionalHeader64:
dd = oh.DataDirectory[0:oh.NumberOfRvaAndSizes]
default:
return nil, errors.New("cannot obtain security directory entry: no optional header")
}

if len(dd) <= certTableIndex {
// This image doesn't include a certificate table entry, so has no signatures.
return nil, nil
}

// Create a reader for the security directory entry, which points to one or more WIN_CERTIFICATE structs
certReader := io.NewSectionReader(
h.r,
int64(dd[certTableIndex].VirtualAddress),
int64(dd[certTableIndex].Size))

// Binaries can have multiple signers - this is achieved using multiple single-signed Authenticode
// signatures - see section 32.5.3.3 ("Secure Boot and Driver Signing - UEFI Image Validation -
// Signature Database Update - Authorization Process") of the UEFI Specification, version 2.8.
var sigs []*efi.WinCertificateAuthenticode

SignatureLoop:
for i := 0; ; i++ {
// Signatures in this section are 8-byte aligned - see the PE spec:
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only
off, _ := certReader.Seek(0, io.SeekCurrent)
alignSize := (8 - (off & 7)) % 8
certReader.Seek(alignSize, io.SeekCurrent)

c, err := efi.ReadWinCertificate(certReader)
switch {
case xerrors.Is(err, io.EOF):
break SignatureLoop
case err != nil:
return nil, xerrors.Errorf("cannot decode WIN_CERTIFICATE from security directory entry %d: %w", i, err)
}

sig, ok := c.(*efi.WinCertificateAuthenticode)
if !ok {
return nil, fmt.Errorf("unexpected WIN_CERTIFICATE type from security directory entry %d: not an Authenticode signature", i)
}

// Reject any signature with a digest algorithm other than SHA256, as that's the only algorithm used
// for binaries we're expected to support, and therefore required by the UEFI implementation.
if sig.DigestAlgorithm() != crypto.SHA256 {
return nil, fmt.Errorf("signature from security directory entry %d has unexpected digest algorithm", i)
}

sigs = append(sigs, sig)
}

return sigs, nil
return internal_efi.SecureBootSignaturesFromPEFile(h.pefile, h.r)
}

// cstringReader is a reader that can read a C-style NULL terminated string.
Expand Down
145 changes: 9 additions & 136 deletions efi/preinstall/check_pcr4.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"errors"
"fmt"
"io"
"strings"

efi "github.com/canonical/go-efilib"
"github.com/canonical/go-tpm2"
Expand All @@ -38,120 +37,6 @@ var (
efiComputePeImageDigest = efi.ComputePeImageDigest
)

// readLoadOptionFromLog reads the corresponding Boot#### load option from the log,
// which reflects the value of it at boot time, as opposed to reading it from an
// EFI variable which may have been modified since booting.
func readLoadOptionFromLog(log *tcglog.Log, n uint16) (*efi.LoadOption, error) {
events := log.Events
for len(events) > 0 {
ev := events[0]
events = events[1:]

if ev.PCRIndex != internal_efi.PlatformConfigPCR {
continue
}

if ev.EventType != tcglog.EventTypeEFIVariableBoot && ev.EventType != tcglog.EventTypeEFIVariableBoot2 {
// not a boot variable
continue
}

data, ok := ev.Data.(*tcglog.EFIVariableData)
if !ok {
// decode error data is guaranteed to implement the error interface
return nil, fmt.Errorf("boot variable measurement has wrong data format: %w", ev.Data.(error))
}
if data.VariableName != efi.GlobalVariable {
// not a global variable
continue
}
if !strings.HasPrefix(data.UnicodeName, "Boot") || len(data.UnicodeName) != 8 {
// name has unexpected prefix or length
continue
}

var x uint16
if c, err := fmt.Sscanf(data.UnicodeName, "Boot%x", &x); err != nil || c != 1 {
continue
}
if x != n {
// wrong load option
continue
}

// We've found the correct load option. Decode it from the data stored in the log.
opt, err := efi.ReadLoadOption(bytes.NewReader(data.VariableData))
if err != nil {
return nil, fmt.Errorf("cannot read load option from event data: %w", err)
}
return opt, nil
}

return nil, errors.New("cannot find specified boot option")
}

// isLaunchedFromLoadOption returns true if the supplied EV_EFI_BOOT_SERVICES_APPLICATION event
// is associated with the supplied load option. This will panic if the event is of the
// wrong type or the event data decodes incorrectly. This works by doing a device path match,
// which can either be a full match, or a recognized short-form match. This also handles the case
// where the boot option points to a removable device and the executable associated with the load
// event is loaded from that device.
func isLaunchedFromLoadOption(ev *tcglog.Event, opt *efi.LoadOption) (yes bool, err error) {
if ev.EventType != tcglog.EventTypeEFIBootServicesApplication {
// The caller should check this.
panic("unexpected event type")
}

// Grab the device path from the event. For the launch of the initial boot loader, this
// will always be a full path.
eventDevicePath := ev.Data.(*tcglog.EFIImageLoadEvent).DevicePath
if len(eventDevicePath) == 0 {
return false, errors.New("EV_EFI_BOOT_SERVICES_APPLICATION event has empty device path")
}

// Try to match the load option.
if opt.Attributes&efi.LoadOptionActive == 0 {
// the load option isn't active.
return false, errors.New("boot option is not active")
}

// Test to see if the load option path matches the load event path in some way. Note
// that the load option might be in short-form, but this function takes that into
// account.
if eventDevicePath.Matches(opt.FilePath) != efi.DevicePathNoMatch {
// We have a match. This is very likely to be a launch of the
// load option.
return true, nil
}

// There's no match with the load option. This might happen when booting from
// removable media where the load option specifies the device path pointing to
// the bus that the removable media is connected to, but the load event contains
// the full path to the initial boot loader, using some extra components.
// Unless the load option is already using a short-form path, try appending the
// extra components for the removable media from the load event to the load option
// path and try testing for a match again.
if opt.FilePath.ShortFormType().IsShortForm() {
// The load option path is in short-form. We aren't going to find a match.
return false, nil
}

// Copy the load option path
optFilePath := append(efi.DevicePath{}, opt.FilePath...)
if cdrom := efi.DevicePathFindFirstOccurrence[*efi.CDROMDevicePathNode](eventDevicePath); len(cdrom) > 0 {
// Booting from CD-ROM.
optFilePath = append(optFilePath, cdrom...)
} else if hd := efi.DevicePathFindFirstOccurrence[*efi.HardDriveDevicePathNode](eventDevicePath); len(hd) > 0 {
// Booting from any removable device with a GPT, such as a USB drive.
optFilePath = append(optFilePath, hd...)
}

// With the CDROM() or HD() components of the event file path appended to the
// load option path, test for a match again. In this case, we expect a full
// match as neither paths are in short-form.
return eventDevicePath.Matches(optFilePath) == efi.DevicePathFullMatch, nil
}

type bootManagerCodeResultFlags int

const (
Expand Down Expand Up @@ -214,17 +99,11 @@ func checkBootManagerCodeMeasurements(ctx context.Context, env internal_efi.Host
return 0, fmt.Errorf("cannot obtain boot option support: %w", err)
}

// Obtain the BootCurrent variable and use this to obtain the corresponding load entry
// that was measured to the log. BootXXXX variables are measured to the TPM and so we don't
// need to read back from an EFI variable that could have been modified between boot time
// and now.
current, err := efi.ReadBootCurrentVariable(varCtx)
// Obtain the load option from the current boot so we can identify which load
// event corresponds to the initial OS boot loader.
bootOpt, err := readCurrentBootLoadOptionFromLog(varCtx, log)
if err != nil {
return 0, fmt.Errorf("cannot read BootCurrent variable: %w", err)
}
bootOpt, err := readLoadOptionFromLog(log, current)
if err != nil {
return 0, fmt.Errorf("cannot read current Boot%04x load option from log: %w", current, err)
return 0, err
}

var (
Expand Down Expand Up @@ -348,35 +227,29 @@ NextEvent:
continue NextEvent
}

data, eventDataOk := ev.Data.(*tcglog.EFIImageLoadEvent)

switch seenOSComponentLaunches {
case 0:
if !eventDataOk {
// Only require the event data to be ok for firmware generated events. This is because
// OS components might create invalid data (and shim actually does), so we ignore those
// errors.
return 0, fmt.Errorf("invalid OS-present EV_EFI_BOOT_SERVICES_APPLICATION event data: %w", ev.Data.(error))
}
// Check if this launch is associated with the EFI_LOAD_OPTION associated with
// the current boot.
// the current boot. This will fail if the data associated with the event is invalid.
isBootOptLaunch, err := isLaunchedFromLoadOption(ev, bootOpt)
if err != nil {
return 0, fmt.Errorf("cannot determine if OS-present EV_EFI_BOOT_SERVICES_APPLICATION event for %v is associated with the current boot load option: %w", data.DevicePath, err)
return 0, fmt.Errorf("cannot determine if OS-present EV_EFI_BOOT_SERVICES_APPLICATION event is associated with the current boot load option: %w", err)
}
if isBootOptLaunch {
// We have the EV_EFI_BOOT_SERVICES_APPLICATION event associated with the IBL launch.
seenOSComponentLaunches += 1
} else {
// We have an EV_EFI_BOOT_SERVICES_APPLICATION that didn't come from the load option
// associated with the current boot.
// Test to see if it's part of Absolute. If it is, that's fine - we copy this into
// Test to see if it's part of Absolute. I it is, that's fine - we copy this into
// the profile, so we don't need to do any other verification of it and we don't have
// anything to verify the Authenticode digest against anyway. We have a device path,
// but not one that we're able to read back from.
//
// If this isn't Absolute, we bail with an error. We don't support anything else being
// loaded here, and ideally Absolute will be turned off as well.

data := ev.Data.(*tcglog.EFIImageLoadEvent) // this is safe, else the earlier isLaunchedFromLoadOption would have returned an error
if result&bootManagerCodeAbsoluteComputraceRunning > 0 {
return 0, fmt.Errorf("OS-present EV_EFI_BOOT_SERVICES_APPLICATION event for %v is not associated with the current boot load option and is not Absolute", data.DevicePath)
}
Expand Down
Loading

0 comments on commit a5c8f67

Please sign in to comment.