Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ on:
branches: [ "master" ]
jobs:
tests:
runs-on: ubuntu-24.04
runs-on: ${{ matrix.runner }}
strategy:
matrix:
runner:
- ubuntu-24.04
- ubuntu-24.04-arm
goversion:
- 1.18
# The unit tests currently fail against the new stable go
Expand Down
230 changes: 229 additions & 1 deletion efi/preinstall/check_host_security.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
* Copyright (C) 2024-2026 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
Expand All @@ -21,12 +21,20 @@ package preinstall

import (
"bytes"
"errors"
"fmt"
"runtime"

"github.com/canonical/tcglog-parser"
"github.com/pilebones/go-udev/netlink"
internal_efi "github.com/snapcore/secboot/internal/efi"
)

// runtimeGOARCH is the architecture that host security checks are performed
// for. It is a variable so that tests can run the checks for architectures
// other than the one the test binary was built for.
var runtimeGOARCH = runtime.GOARCH

// discreteTPMPartialResetAttackMitigationStatus indicates whether a partial mitigation against
// discrete TPM reset attacks should be enabled. See the documentation for
// RequestPartialDiscreteTPMResetAttackMitigation
Expand Down Expand Up @@ -145,3 +153,223 @@ Loop:
}
return nil
}

// Architecture-specific host security checks are dispatched at runtime rather than
// selected by build constraints, so that all architectures' checks are compiled and
// testable everywhere.

// checkHostSecurity is the main entry point for verifying that the host security
// is sufficient. Errors that can't be resolved or which should prevent further checks from running
// are returned immediately and without any wrapping. Errors that can be resolved and which shouldn't
// prevent further checks from running are returned wrapped in [joinError].
func checkHostSecurity(env internal_efi.HostEnvironment, log *tcglog.Log) (platformFirmwareIntegrityConfig, error) {
switch runtimeGOARCH {
case "amd64":
return checkHostSecurityAMD64(env, log)
case "arm64":
return checkHostSecurityARM64(env, log)
default:
return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("checking host security is not implemented on %s", runtimeGOARCH)}
}
}

// checkDiscreteTPMPartialResetAttackMitigationStatus determines whether a partial mitigation
// against discrete TPM reset attacks should be enabled. See the documentation for
// RequestPartialDiscreteTPMResetAttackMitigation.
func checkDiscreteTPMPartialResetAttackMitigationStatus(env internal_efi.HostEnvironment, logResults *pcrBankResults) (discreteTPMPartialResetAttackMitigationStatus, error) {
switch runtimeGOARCH {
case "amd64":
return checkDiscreteTPMPartialResetAttackMitigationStatusAMD64(env, logResults)
case "arm64":
return checkDiscreteTPMPartialResetAttackMitigationStatusARM64(env, logResults)
default:
return dtpmPartialResetAttackMitigationNotRequired, nil
}
}

func checkHostSecurityAMD64(env internal_efi.HostEnvironment, log *tcglog.Log) (platformFirmwareIntegrityConfig, error) {
cpuVendor, err := determineCPUVendor(env)
if err != nil {
return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU vendor: %w", err)}
}

amd64Env, err := env.AMD64()
if err != nil {
return platformFirmwareIntegrityNone, fmt.Errorf("cannot obtain AMD64 environment: %w", err)
}

var errs []error

var integrity platformFirmwareIntegrityConfig
switch cpuVendor {
case cpuVendorIntel:
if err := checkHostSecurityIntelBootGuard(env); err != nil {
var nohwrotErr *NoHardwareRootOfTrustError
ctxErr := fmt.Errorf("encountered an error when checking Intel BootGuard configuration: %w", err)
if !errors.As(err, &nohwrotErr) {
return platformFirmwareIntegrityNone, ctxErr
}
errs = append(errs, ctxErr)
}
if err := checkHostSecurityIntelCPUDebuggingLocked(amd64Env); err != nil {
return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error when checking Intel CPU debugging configuration: %w", err)
}
if len(errs) == 0 {
integrity = platformFirmwareIntegrityVerified
}
case cpuVendorAMD:
integrity, err = checkHostSecurityAMDPSP(env)
if err != nil {
ctxErr := fmt.Errorf("encountered an error when checking the AMD PSP configuration: %w", err)
var nohwrotErr *NoHardwareRootOfTrustError
if !errors.As(err, &nohwrotErr) {
return platformFirmwareIntegrityNone, ctxErr
}
errs = append(errs, ctxErr)
}
default:
panic("not reached")
}

if err := checkSecureBootPolicyPCRForDegradedFirmwareSettings(log); err != nil {
var ce CompoundError
if !errors.As(err, &ce) {
return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking the TCG log for degraded firmware settings: %w", err)
}
errs = append(errs, ce.Unwrap()...)
}
if err := checkForKernelIOMMU(env); err != nil {
switch {
case errors.Is(err, ErrNoKernelIOMMU):
errs = append(errs, err)
default:
return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking sysfs to determine that kernel IOMMU support is enabled: %w", err)
}
}

if len(errs) > 0 {
return platformFirmwareIntegrityNone, joinErrors(errs...)
}

return integrity, nil
}

func checkDiscreteTPMPartialResetAttackMitigationStatusAMD64(env internal_efi.HostEnvironment, logResults *pcrBankResults) (discreteTPMPartialResetAttackMitigationStatus, error) {
cpuVendor, err := determineCPUVendor(env)
if err != nil {
return dtpmPartialResetAttackMitigationUnknown, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU vendor: %w", err)}
}

if cpuVendor != cpuVendorIntel {
// Only enable this on Intel systems.
return dtpmPartialResetAttackMitigationNotRequired, nil
}

amd64Env, err := env.AMD64()
if err != nil {
return dtpmPartialResetAttackMitigationUnknown, fmt.Errorf("cannot obtain AMD64 environment: %w", err)
}

discreteTPM, err := isTPMDiscrete(env)
if err != nil {
return dtpmPartialResetAttackMitigationUnknown, &TPM2DeviceError{err}
}

switch {
case !discreteTPM:
// Not a discrete TPM.
return dtpmPartialResetAttackMitigationNotRequired, nil
case !logResults.Lookup(internal_efi.PlatformFirmwarePCR).Ok():
// PCR0 is unusable.
return dtpmPartialResetAttackMitigationUnavailable, nil
}

restrictedLocalities := restrictedTPMLocalitiesIntel(amd64Env)
for _, locality := range restrictedLocalities.Values() {
if locality == logResults.StartupLocality {
// The startup locality is not available to the OS, so
// we can enable the migitation because PCR0 cannot
// be recreated from the OS.
return dtpmPartialResetAttackMitigationPreferred, nil
}
}

// The startup locality is available to the OS, so the mitigation
// is unavailable even though it would have been desired because
// PCR0 can be recreated from the OS.
return dtpmPartialResetAttackMitigationUnavailable, nil
}

// checkHostSecurityARM64Platform selects the platform-specific firmware
// integrity check. Tests replace this to supply synthetic platforms.
var checkHostSecurityARM64Platform = func(env internal_efi.HostEnvironmentARM64, cpuManufacturer string) (platformFirmwareIntegrityConfig, error) {
switch cpuManufacturer {
case "NVIDIA":
return checkHostSecurityNVIDIA(env)
default:
return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("unsupported CPU manufacturer: %s", cpuManufacturer)}
}
}

func checkHostSecurityARM64(env internal_efi.HostEnvironment, log *tcglog.Log) (platformFirmwareIntegrityConfig, error) {
arm64Env, err := env.ARM64()
if err != nil {
return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot obtain ARM64 environment: %w", err)}
}

cpuManufacturer, err := arm64Env.CPUManufacturer()
if err != nil {
return platformFirmwareIntegrityNone, &UnsupportedPlatformError{fmt.Errorf("cannot determine CPU manufacturer: %w", err)}
}

integrity, err := checkHostSecurityARM64Platform(arm64Env, cpuManufacturer)
if err != nil {
return platformFirmwareIntegrityNone, err
}

return checkHostSecurityARM64Generic(env, log, integrity)
}

func checkHostSecurityARM64Generic(env internal_efi.HostEnvironment, log *tcglog.Log, integrity platformFirmwareIntegrityConfig) (platformFirmwareIntegrityConfig, error) {
var errs []error

if err := checkSecureBootPolicyPCRForDegradedFirmwareSettings(log); err != nil {
var ce CompoundError
if !errors.As(err, &ce) {
return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking the TCG log for degraded firmware settings: %w", err)
}
errs = append(errs, ce.Unwrap()...)
}

if err := checkForKernelIOMMU(env); err != nil {
switch {
case errors.Is(err, ErrNoKernelIOMMU):
errs = append(errs, err)
default:
return platformFirmwareIntegrityNone, fmt.Errorf("encountered an error whilst checking sysfs to determine that kernel IOMMU support is enabled: %w", err)
}
}

if len(errs) > 0 {
return integrity, joinErrors(errs...)
}

return integrity, nil
}

// checkDiscreteTPMPartialResetAttackMitigationStatusARM64 determines whether a partial mitigation
// against discrete TPM reset attacks should be enabled.
func checkDiscreteTPMPartialResetAttackMitigationStatusARM64(env internal_efi.HostEnvironment, _ *pcrBankResults) (discreteTPMPartialResetAttackMitigationStatus, error) {
discreteTPM, err := isTPMDiscrete(env)
if err != nil {
return dtpmPartialResetAttackMitigationUnknown, &TPM2DeviceError{err}
}
if !discreteTPM {
return dtpmPartialResetAttackMitigationNotRequired, nil
}

// ARM64 has no generic mechanism to establish that the TPM startup locality
// is protected by the hardware root of trust, so PCR0 binding cannot be relied
// on to mitigate an independent reset of a discrete TPM.
return dtpmPartialResetAttackMitigationUnavailable, nil
}
4 changes: 1 addition & 3 deletions efi/preinstall/check_host_security_amd.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
//go:build amd64

// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2025 Canonical Ltd
* Copyright (C) 2025-2026 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
Expand Down
Loading
Loading