Skip to content

Commit

Permalink
Merge pull request #316 from chrisccoulson/preinstall-add-check-fw-pr…
Browse files Browse the repository at this point in the history
…otection

preinstall: Add checks for platform firmware protections.

This adds checks for how the platform firmware is protected, with
specific tests for whether CPU silicon debugging features are enabled or
not locked off, whether a firmware debugging endpoint (such as UDK) is
enabled, whether the kernel IOMMU is available, whether the firmware
indicates that DMA protection was disabled at some point, and whether
the firmware is protected by a hardware root-of-trust (ie, keys fused
into the silicon during manufacturing).

The main entry point to this is `checkPlatformFirmwareProtections`.
There is an implementation for amd64 go builds (x86-64) and then a null
implementation for everything that is !amd64 which just returns an error.

Some of these checks are based on the HSI checks in fwupd with some minor
adaptions, particularly for the BootGuard checks where there are some tests
in fwupd that disagree with other publicly available documentation which
better aligns with hardware that is assumed to be good (note that Intel do
not publicly document how any of this works).

This has a significant limitation today - it only works on Intel based
systems. AMD support will happen when we figure out how the platform
firmware is protected on AMD platforms with the dedicated PSP.

There are some other limitations, eg, whilst we detect that the IOMMU
functionality in the kernel is enabled, we don't check that external
facing DMA capable ports or user accessible internal DMA capable ports
are protected by one of the domains (and this feature could be made
optional if there aren't external facing or user-accessible internal
ports that are capable of DMA). Microsoft say that externally accessible
ports can be identified using ACPI tables (https://learn.microsoft.com/en-us/windows-hardware/drivers/pci/dsd-for-pcie-root-ports#identifying-externally-exposed-pcie-root-ports), but we don't
do anything like this yet. I'm going to create a follow-on epic to expand support
for this feature, and this check will likely end up in there.

This does perform a check of the secure boot PCR for an event that is
not documented anywhere by the TCG, isn't in EDK2 or any of Microsoft's
public Project Mu repositories, but is a Windows requirement (measuring
a `EV_EFI_ACTION` event with the string "DMA Protection Disabled" to PCR7
if DMA protection is disabled by any settings in the firmware). Note
that strings associated with `EV_EFI_ACTION` events aren't meant to be
NULL terminated, although the one on my Dell XPS15 is so the code
handles the case where the event data is NULL terminated too. I don't
know if this is a Dell bug or if Microsoft generally expects this to be a NULL
terminated, but this contradicts the TCG spec for this event type. The only
places where NULL terminated strings are used are for `EV_S_CRTM_CONTENTS`
when measured by a H-CRTM event, and a NULL terminated UCS2 string in
the data associated with `EV_S_CRTM_VERSION` events (yay for consistency!)
  • Loading branch information
chrisccoulson authored Aug 20, 2024
2 parents 397b19c + b353807 commit b48732e
Show file tree
Hide file tree
Showing 13 changed files with 1,777 additions and 52 deletions.
107 changes: 107 additions & 0 deletions efi/preinstall/check_fw_protections.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// -*- 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 preinstall

import (
"bytes"
"fmt"

"github.com/canonical/tcglog-parser"
internal_efi "github.com/snapcore/secboot/internal/efi"
)

// checkForKernelIOMMU checks that the kernel has enabled some sort of DMA protection.
// On Intel devices, the domains are defined by the DMAR ACPI table. The check is quite
// simple, and based on the fwupd HSI checks.
// XXX: Figure out whether this is genuinely sufficient, eg:
// - Should we only mandate this if there are externally facing ports, or internal ports
// that are accessible to the user
// - Are all externally facing ports protected?
// - Are internal ports accessible to the user protected?
// - Are all addon devices with embedded controllers protected?
//
// This function is going to need some additional work later on.
func checkForKernelIOMMU(env internal_efi.HostEnvironment) error {
devices, err := env.DevicesForClass("iommu")
switch {
case err != nil:
return err
case len(devices) == 0:
return ErrNoKernelIOMMU
}

for _, device := range devices {
if device.Subsystem() == "iommu" {
return nil
}
}
return ErrNoKernelIOMMU
}

// checkSecureBootPolicyPCRForDegradedFirmwareSettings checks PCR7 for the indication of degraded
// firmware settings:
// - Whether a debugging endpoint is enabled, via the presence of a EV_EFI_ACTION event with the
// "UEFI Debug Mode" string. This is defined in the TCG PC-Client PFP spec.
// - Whether DMA protection was disabled at some point, via the presence of a EV_EFI_ACTION event
// with the "DMA Protection Disabled" string. This is a Windows requirement.
func checkSecureBootPolicyPCRForDegradedFirmwareSettings(log *tcglog.Log) error {
events := log.Events
for len(events) > 0 {
// Pop next event
event := events[0]
events = events[1:]

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

switch event.EventType {
case tcglog.EventTypeEFIAction:
if event.Data == tcglog.FirmwareDebuggerEvent {
// Debugger enabled
return ErrUEFIDebuggingEnabled
}
if event.Data == tcglog.DMAProtectionDisabled {
// DMA protection was disabled bt the firmware at some point
return ErrInsufficientDMAProtection
}
// XXX: My Dell NULL terminates this string which causes decoding to fail,
// as the TCG PC Client Platform Firmware Profile spec says that the event
// data in EV_EFI_ACTION events should not be NULL terminated.
if bytes.Equal(event.Data.Bytes(), append([]byte(tcglog.DMAProtectionDisabled), 0x00)) {
// DMA protection was disabled bt the firmware at some point
return ErrInsufficientDMAProtection
}
// Unexpected data
return fmt.Errorf("unexpected EV_EFI_ACTION event data in PCR7 event: %q", event.Data)
case tcglog.EventTypeEFIVariableDriverConfig, tcglog.EventTypeSeparator:
// ok
case tcglog.EventTypeEFIVariableAuthority:
return nil
default:
// Unexpected event type
return fmt.Errorf("unexpected event type (%v) in PCR7", event.EventType)
}
}

// This could only happen if there are no events in PCR7, but checkFirmwareLogAndChoosePCRBank
// verifies that there is a separator in all TCG defined PCRs.
panic("not reached")
}
134 changes: 134 additions & 0 deletions efi/preinstall/check_fw_protections_amd64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// -*- 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 preinstall

import (
"errors"
"fmt"

"github.com/canonical/go-tpm2"
"github.com/canonical/tcglog-parser"
"github.com/intel-go/cpuid"
internal_efi "github.com/snapcore/secboot/internal/efi"
)

const (
ia32DebugInterfaceMSR = 0xc80

ia32DebugEnable uint64 = 1 << 0
ia32DebugLock uint64 = 1 << 30
)

func checkCPUDebuggingLockedMSR(env internal_efi.HostEnvironmentAMD64) error {
// Check for "Silicon Debug Interface", returned in bit 11 of %ecx when calling
// cpuid with %eax=1.
debugSupported := env.HasCPUIDFeature(cpuid.SDBG)
if !debugSupported {
return nil
}

vals, err := env.ReadMSRs(ia32DebugInterfaceMSR)
if err != nil {
return err
}
if len(vals) == 0 {
return errors.New("no MSR values returned")
}

for _, val := range vals {
if val&ia32DebugEnable > 0 || val&ia32DebugLock == 0 {
return ErrCPUDebuggingNotLocked
}
}

return nil
}

type cpuVendor int

const (
cpuVendorUnknown cpuVendor = iota
cpuVendorIntel
cpuVendorAMD
)

func determineCPUVendor(env internal_efi.HostEnvironmentAMD64) (cpuVendor, error) {
switch env.CPUVendorIdentificator() {
case "GenuineIntel":
return cpuVendorIntel, nil
case "AuthenticAMD":
return cpuVendorAMD, nil
default:
return cpuVendorUnknown, fmt.Errorf("unknown CPU vendor: %s", env.CPUVendorIdentificator())
}
}

// checkPlatformFirmwareProtections is the main entry point for verifying that platform firmware
// protections are sufficient.
func checkPlatformFirmwareProtections(env internal_efi.HostEnvironment, log *tcglog.Log) (protectedStartupLocalities tpm2.Locality, err error) {
amd64Env, err := env.AMD64()
if err != nil {
return 0, fmt.Errorf("cannot obtain AMD64 environment: %w", err)
}

cpuVendor, err := determineCPUVendor(amd64Env)
if err != nil {
return 0, fmt.Errorf("cannot determine CPU vendor: %w", err)
}

switch cpuVendor {
case cpuVendorIntel:
if err := checkPlatformFirmwareProtectionsIntelMEI(env); err != nil {
return 0, fmt.Errorf("encountered an error when determining platform firmware protections using Intel MEI: %w", err)
}
if amd64Env.HasCPUIDFeature(cpuid.SMX) {
// The Intel TXT spec says that locality 4 is basically only available
// to microcode, and is locked before handing over to an ACM which
// has access to locality 3. Access to this is meant to be locked at the
// hardware level before running non-Intel code, although I'm not sure if
// this is only relevant in the D-CRTM case where the SINIT ACM has access
// to locality 3, and it locks access to it, leaving access to localities 2
// and 1 to the measured launch environment and dynamic OS respectively. We
// rely on the property of localities 3 and 4 being protected somewhat in order
// to attempt to mitigate discrete TPM reset attacks on Intel platforms (basically
// by including PCR0 in the policy, even though it's otherwise useless to include
// it, but locality 3 or 4 access is required in order to reconstruct PCR0 after a
// TPM reset. Mark localities 3 and 4 as protected if we have the right instructions
// for implementing a D-CRTM with Intel TXT (which I think is SMX).
protectedStartupLocalities |= tpm2.LocalityThree | tpm2.LocalityFour
}
case cpuVendorAMD:
return 0, &UnsupportedPlatformError{errors.New("checking platform firmware protections is not yet implemented for AMD")}
default:
panic("not reached")
}

if err := checkSecureBootPolicyPCRForDegradedFirmwareSettings(log); err != nil {
return 0, fmt.Errorf("encountered an error whilst checking the TCG log for degraded firmware settings: %w", err)
}
if err := checkForKernelIOMMU(env); err != nil {
return 0, fmt.Errorf("encountered an error whilst checking sysfs to determine that kernel IOMMU support is enabled: %w", err)
}
if err := checkCPUDebuggingLockedMSR(amd64Env); err != nil {
return 0, fmt.Errorf("encountered an error when determining CPU debugging configuration from MSRs: %w", err)
}

return protectedStartupLocalities, nil
}
Loading

0 comments on commit b48732e

Please sign in to comment.