diff --git a/efi/efi_test.go b/efi/efi_test.go
index fd444057..4b6d9487 100644
--- a/efi/efi_test.go
+++ b/efi/efi_test.go
@@ -764,7 +764,7 @@ func (v *mockPcrProfileOptionVisitor) AddPCRs(pcrs ...tpm2.Handle) {
v.pcrs = append(v.pcrs, pcrs...)
}
-func (v *mockPcrProfileOptionVisitor) SetEnvironment(env internal_efi.HostEnvironment) {
+func (v *mockPcrProfileOptionVisitor) SetEnvironment(env HostEnvironment) {
v.env = env
}
diff --git a/efi/env.go b/efi/env.go
index 3afbf00a..c690f599 100644
--- a/efi/env.go
+++ b/efi/env.go
@@ -29,7 +29,6 @@ import (
"sort"
efi "github.com/canonical/go-efilib"
- "github.com/canonical/tcglog-parser"
internal_efi "github.com/snapcore/secboot/internal/efi"
"golang.org/x/xerrors"
)
@@ -37,20 +36,7 @@ import (
// HostEnvironment is an interface that abstracts out an EFI environment, so that
// consumers of the API can provide a custom mechanism to read EFI variables or parse
// the TCG event log.
-type HostEnvironment interface {
- // VarContext returns a copy of parent containing a VarsBackend, keyed by efi.VarsBackendKey,
- // for interacting with EFI variables via go-efilib. This context can be passed to any
- // go-efilib function that interacts with EFI variables. Right now, go-efilib doesn't
- // support any other uses of the context such as cancelation or deadlines. The efivarfs
- // backend will support this eventually for variable writes because it currently implements
- // a retry loop that has a potential to race with other processes. Cancelation and / or
- // deadlines for sections of code that performs multiple reads using efivarfs may be useful
- // in the future because the kernel rate-limits reads per-user.
- VarContext(parent context.Context) context.Context
-
- // ReadEventLog reads the TCG event log
- ReadEventLog() (*tcglog.Log, error)
-}
+type HostEnvironment = internal_efi.HostEnvironmentEFI
type hostEnvironmentOption struct {
HostEnvironment
diff --git a/efi/env_test.go b/efi/env_test.go
index cbf75556..456efccb 100644
--- a/efi/env_test.go
+++ b/efi/env_test.go
@@ -251,7 +251,7 @@ func (s *envSuite) TestRootVarsCollectorWriteOne(c *C) {
func (s *envSuite) TestRootVarsCollectorWriteOneNew(c *C) {
// Test that one write in the initial state works and creates one new starting state
s.testRootVarsCollector(c, &testRootVarsCollectorData{
- env: efitest.NewMockHostEnvironment(efitest.MockVars{}, nil),
+ env: efitest.NewMockHostEnvironment(nil, nil),
expected: []efitest.MockVars{
{
{Name: "foo", GUID: testGuid1}: nil,
@@ -557,7 +557,7 @@ func (s *envSuite) TestRootVarsCollectorPeekAll(c *C) {
}
func (s *envSuite) TestVarBranchReadsUpdate(c *C) {
- env := efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)
+ env := efitest.NewMockHostEnvironment(nil, nil)
collector := NewVariableSetCollector(env)
root := collector.Next()
diff --git a/efi/pcr_images_measurer_test.go b/efi/pcr_images_measurer_test.go
index 26de687a..f84fe6d7 100644
--- a/efi/pcr_images_measurer_test.go
+++ b/efi/pcr_images_measurer_test.go
@@ -45,7 +45,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureOneLeaf(c *C) {
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -94,7 +94,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerTwoLeaf(c *C) {
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -146,7 +146,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerNonLeaf(c *C) {
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -203,7 +203,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerTwoNonLeaf(c *C) {
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo1")
@@ -279,7 +279,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureWithParams(c *C) {
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -341,7 +341,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureWithInheritedParams
profile := secboot_tpm2.NewPCRProtectionProfile()
params := &LoadParams{SnapModel: model}
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -504,7 +504,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureWithFwContext(c *C)
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -555,7 +555,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureEnsureFwContextIsCo
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -612,7 +612,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureWithShimContext(c *
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
@@ -663,7 +663,7 @@ func (s *pcrImagesMeasurerSuite) TestPcrImagesMeasurerMeasureEnsureShimContextIs
profile := secboot_tpm2.NewPCRProtectionProfile()
params := new(LoadParams)
- vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(efitest.MockVars{}, nil)).Next()
+ vars := NewVariableSetCollector(efitest.NewMockHostEnvironment(nil, nil)).Next()
h := crypto.SHA256.New()
io.WriteString(h, "foo")
diff --git a/efi/pcr_profile.go b/efi/pcr_profile.go
index ce988349..b20ace63 100644
--- a/efi/pcr_profile.go
+++ b/efi/pcr_profile.go
@@ -327,10 +327,11 @@ func (g *pcrProfileGenerator) AddPCRs(pcrs ...tpm2.Handle) {
}
// SetEnvironment implements [internal_efi.PCRProfileOptionVisitor.SetEnvironment]
-func (g *pcrProfileGenerator) SetEnvironment(env internal_efi.HostEnvironment) {
+func (g *pcrProfileGenerator) SetEnvironment(env HostEnvironment) {
g.env = env
}
+// AddInitialVariablesModifier implements [internal_efi.PCRProfileOptionVisitor.AddInitialVariablesModifier]
func (g *pcrProfileGenerator) AddInitialVariablesModifier(fn internal_efi.InitialVariablesModifier) {
g.varModifiers = append(g.varModifiers, fn)
}
diff --git a/efi/pcr_profile_test.go b/efi/pcr_profile_test.go
index f1ad92d9..cbb76685 100644
--- a/efi/pcr_profile_test.go
+++ b/efi/pcr_profile_test.go
@@ -112,7 +112,7 @@ func (s *pcrProfileMockedSuite) TestAddPCRProfileLog(c *C) {
defer restore()
c.Check(AddPCRProfile(tpm2.HashAlgorithmSHA256, profile.RootBranch(), sequences,
- WithHostEnvironment(efitest.NewMockHostEnvironment(efitest.MockVars{}, expectedLog)),
+ WithHostEnvironment(efitest.NewMockHostEnvironment(nil, expectedLog)),
WithSecureBootPolicyProfile(),
), IsNil)
}
diff --git a/go.mod b/go.mod
index 86d54cc9..2237bd6b 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/canonical/go-efilib v1.2.0
github.com/canonical/go-sp800.108-kdf v0.0.0-20210315104021-ead800bbf9a0
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3
- github.com/canonical/go-tpm2 v1.3.0
+ github.com/canonical/go-tpm2 v1.6.2
github.com/canonical/tcglog-parser v0.0.0-20240502135731-7e805de2ca0d
github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85
golang.org/x/crypto v0.9.0
@@ -19,6 +19,7 @@ require (
require (
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
+ github.com/intel-go/cpuid v0.0.0-20220614022739-219e067757cb // indirect
github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
diff --git a/go.sum b/go.sum
index e9a0914d..954540ff 100644
--- a/go.sum
+++ b/go.sum
@@ -29,6 +29,10 @@ github.com/canonical/go-tpm2 v1.1.0 h1:i3YeiYTWciamtbUpKZC9FIGhCP9rWSu1AZpYnWUO9
github.com/canonical/go-tpm2 v1.1.0/go.mod h1:kLkR1//7ocrPDl6LZfijTKEoPGxRIZSbb8GuWaO1JM8=
github.com/canonical/go-tpm2 v1.3.0 h1:+xc2++IM4kaMCJruFzlgtYgQyV5Q0EReaP++z8VTqJk=
github.com/canonical/go-tpm2 v1.3.0/go.mod h1:kLkR1//7ocrPDl6LZfijTKEoPGxRIZSbb8GuWaO1JM8=
+github.com/canonical/go-tpm2 v1.4.0 h1:qdOqD2tpww/8TeKdj86GatKcGrIjRl0wSwcWdqtuE6I=
+github.com/canonical/go-tpm2 v1.4.0/go.mod h1:Dz0PQRmoYrmk/4BLILjRA+SFzuqEo1etAvYeAJiMhYU=
+github.com/canonical/go-tpm2 v1.6.2 h1:hLGwI7rSSwExQZkyPF7jtAfEfH0tykxCf+O2SjXdxac=
+github.com/canonical/go-tpm2 v1.6.2/go.mod h1:Dz0PQRmoYrmk/4BLILjRA+SFzuqEo1etAvYeAJiMhYU=
github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2/go.mod h1:QoW2apR2tBl6T/4czdND/EHjL1Ia9cCmQnIj9Xe0Kt8=
github.com/canonical/tcglog-parser v0.0.0-20230929123437-16b3d8d08691 h1:EMZbYZXGGmtSaS2+DIza1gZ54+KVjzsw/NEUAY8me1E=
github.com/canonical/tcglog-parser v0.0.0-20230929123437-16b3d8d08691/go.mod h1:EPlw+kpcTgSHXkLiUP/Jqp4CmkNPyVnJLAk4oSjNFrQ=
@@ -45,6 +49,8 @@ github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzr
github.com/gorilla/mux v1.7.4-0.20190701202633-d83b6ffe499a/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gvalkov/golang-evdev v0.0.0-20191114124502-287e62b94bcb/go.mod h1:SAzVFKCRezozJTGavF3GX8MBUruETCqzivVLYiywouA=
+github.com/intel-go/cpuid v0.0.0-20220614022739-219e067757cb h1:Fg0Y/RDZ6UPwl3o7/IzPbneDq8g9+gH6DPs42KFUsy8=
+github.com/intel-go/cpuid v0.0.0-20220614022739-219e067757cb/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4=
github.com/jessevdk/go-flags v1.4.1-0.20180927143258-7309ec74f752/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
diff --git a/internal/efi/default_env.go b/internal/efi/default_env.go
index ca4e1b04..acddf272 100644
--- a/internal/efi/default_env.go
+++ b/internal/efi/default_env.go
@@ -20,26 +20,38 @@
package efi
import (
+ "bytes"
"context"
+ "errors"
+ "fmt"
+ "io"
"os"
+ "os/exec"
+ "path/filepath"
efi "github.com/canonical/go-efilib"
+ "github.com/canonical/go-tpm2"
+ "github.com/canonical/go-tpm2/linux"
"github.com/canonical/tcglog-parser"
)
var (
+ linuxDefaultTPM2Device = linux.DefaultTPM2Device
+ linuxRawDeviceResourceManagedDevice = (*linux.RawDevice).ResourceManagedDevice
+
eventLogPath = "/sys/kernel/security/tpm0/binary_bios_measurements" // Path of the TCG event log for the default TPM, in binary form
+ sysfsPath = "/sys"
)
type defaultEnvImpl struct{}
-// VarContext implements [HostEnvironment.VarContext].
-func (e defaultEnvImpl) VarContext(parent context.Context) context.Context {
+// VarContext implements [HostEnvironmentEFI.VarContext].
+func (defaultEnvImpl) VarContext(parent context.Context) context.Context {
return efi.WithDefaultVarsBackend(parent)
}
-// ReadEventLog implements [HostEnvironment.ReadEventLog].
-func (e defaultEnvImpl) ReadEventLog() (*tcglog.Log, error) {
+// ReadEventLog implements [HostEnvironmentEFI.ReadEventLog].
+func (defaultEnvImpl) ReadEventLog() (*tcglog.Log, error) {
f, err := os.Open(eventLogPath)
if err != nil {
return nil, err
@@ -49,6 +61,146 @@ func (e defaultEnvImpl) ReadEventLog() (*tcglog.Log, error) {
return tcglog.ReadLog(f, &tcglog.LogOptions{})
}
+// TPMDevice implements [HostEnvironment.TPMDevice].
+func (defaultEnvImpl) TPMDevice() (tpm2.TPMDevice, error) {
+ device, err := linuxDefaultTPM2Device()
+ switch {
+ case errors.Is(err, linux.ErrNoTPMDevices) || errors.Is(err, linux.ErrDefaultNotTPM2Device):
+ return nil, ErrNoTPM2Device
+ case err != nil:
+ return nil, err
+ }
+
+ rmDevice, err := linuxRawDeviceResourceManagedDevice(device)
+ switch {
+ case errors.Is(err, linux.ErrNoResourceManagedDevice):
+ // Return the raw device. This can only be open once, so can block and may block other users.
+ return device, nil
+ case err != nil:
+ return nil, err
+ default:
+ // Return the resource managed device. There is no limit as to how may of these can be opened,
+ // although note that they can't be opened if the raw device is opened so this can still block
+ // if something else has the raw device open and might block other raw device users.
+ return rmDevice, nil
+ }
+}
+
+// DetectVirtMode implements [HostEnvironment.DetectVirtMode].
+func (defaultEnvImpl) DetectVirtMode(mode DetectVirtMode) (string, error) {
+ var extraArgs []string
+ switch mode {
+ case DetectVirtModeAll:
+ // no extra args
+ case DetectVirtModeContainer:
+ extraArgs = []string{"--container"}
+ case DetectVirtModeVM:
+ extraArgs = []string{"--vm"}
+ default:
+ panic("not reached")
+ }
+
+ output, err := exec.Command("systemd-detect-virt", extraArgs...).Output()
+ virt := string(bytes.TrimSpace(output)) // The stdout is newline terminated
+ if err != nil {
+ if _, ok := err.(*exec.ExitError); ok && virt == VirtModeNone {
+ // systemd-detect-virt returns non zero exit code if no virtualization is detected
+ return virt, nil
+ }
+ return "", err
+ }
+ return virt, nil
+}
+
+type defaultEnvSysfsDevice struct {
+ name string
+ path string
+ subsystem string
+}
+
+// Name implements [SysfsDevice.Name].
+func (d *defaultEnvSysfsDevice) Name() string {
+ return d.name
+}
+
+// Path implements [SysfsDevice.Path].
+func (d *defaultEnvSysfsDevice) Path() string {
+ return d.path
+}
+
+// Subsystem implements [SysfsDevice.Subsystem].
+func (d *defaultEnvSysfsDevice) Subsystem() string {
+ return d.subsystem
+}
+
+// AttributeReader implements [SysfsDevice.AttributeReader].
+func (d *defaultEnvSysfsDevice) AttributeReader(attr string) (rc io.ReadCloser, err error) {
+ if attr == "uevent" {
+ return nil, ErrNoDeviceAttribute
+ }
+
+ f, err := os.Open(filepath.Join(d.path, attr))
+ switch {
+ case os.IsNotExist(err):
+ return nil, ErrNoDeviceAttribute
+ case err != nil:
+ return nil, err
+ }
+ defer func() {
+ if err == nil {
+ return
+ }
+ f.Close()
+ }()
+
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+ if !fi.Mode().IsRegular() {
+ return nil, ErrNoDeviceAttribute
+ }
+
+ return f, nil
+}
+
+// DeviceForClass implements [HostEnvironment.DevicesForClass].
+func (defaultEnvImpl) DevicesForClass(class string) ([]SysfsDevice, error) {
+ classPath := filepath.Join(sysfsPath, "class", class)
+ f, err := os.Open(classPath)
+ switch {
+ case os.IsNotExist(err):
+ // it's ok to have no devices for the specified class
+ return nil, nil
+ case err != nil:
+ return nil, err
+ }
+ defer f.Close()
+
+ entries, err := f.ReadDir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ var out []SysfsDevice
+ for _, entry := range entries {
+ path, err := filepath.EvalSymlinks(filepath.Join(classPath, entry.Name()))
+ if err != nil {
+ return nil, fmt.Errorf("cannot resolve path for %s: %w", entry.Name(), err)
+ }
+ subsystem, err := filepath.EvalSymlinks(filepath.Join(path, "subsystem"))
+ if err != nil {
+ return nil, fmt.Errorf("cannot resolve subsystem for %s: %w", entry.Name(), err)
+ }
+ out = append(out, &defaultEnvSysfsDevice{
+ name: entry.Name(),
+ path: path,
+ subsystem: filepath.Base(subsystem),
+ })
+ }
+ return out, nil
+}
+
// DefaultEnv corresponds to the environment associated with the host
// machine.
var DefaultEnv = defaultEnvImpl{}
diff --git a/internal/efi/default_env_amd64.go b/internal/efi/default_env_amd64.go
new file mode 100644
index 00000000..cb510b58
--- /dev/null
+++ b/internal/efi/default_env_amd64.go
@@ -0,0 +1,111 @@
+// -*- 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 .
+ *
+ */
+
+package efi
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "syscall"
+
+ "github.com/intel-go/cpuid"
+)
+
+var (
+ cpuidHasFeature = cpuid.HasFeature
+ devcpuPath = "/dev/cpu"
+)
+
+type defaultEnvAMD64Impl struct{}
+
+// CPUVendorIdentificator implements [HostEnvironmentAMD64.CPUVendorIdentificator].
+func (defaultEnvAMD64Impl) CPUVendorIdentificator() string {
+ return cpuid.VendorIdentificatorString
+}
+
+// HasCPUIDFeature implements [HostEnvironmentAMD64.HasCPUIDFeature].
+func (defaultEnvAMD64Impl) HasCPUIDFeature(feature uint64) bool {
+ return cpuidHasFeature(feature)
+}
+
+// ReadMSR implements [HostEnvironmentAMD64.ReadMSRs].
+func (defaultEnvAMD64Impl) ReadMSRs(msr uint32) (map[uint32]uint64, error) {
+ dir, err := os.Open(devcpuPath)
+ switch {
+ case os.IsNotExist(err):
+ return nil, ErrNoKernelMSRSupport
+ case err != nil:
+ return nil, err
+ }
+ defer dir.Close()
+
+ entries, err := dir.ReadDir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ out := make(map[uint32]uint64)
+
+ for _, entry := range entries {
+ cpuNo, err := strconv.ParseUint(entry.Name(), 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("invalid CPU number for name %s: %w", entry.Name(), err)
+ }
+
+ val, err := func(name string) (uint64, error) {
+ f, err := os.Open(filepath.Join(dir.Name(), name, "msr"))
+ switch {
+ case os.IsNotExist(err):
+ return 0, ErrNoKernelMSRSupport
+ case errors.Is(err, syscall.EIO):
+ return 0, ErrNoMSRSupport
+ case err != nil:
+ return 0, err
+ }
+ defer f.Close()
+
+ var data [8]byte
+ _, err = f.ReadAt(data[:], int64(msr))
+ switch {
+ case errors.Is(err, syscall.EIO): // I think the kernel returns -EIO if the MSR is not supported, but this is poorly documented.
+ return 0, ErrNoMSRSupport
+ case err != nil:
+ return 0, fmt.Errorf("cannot read from MSR device: %w", err)
+ }
+
+ return binary.LittleEndian.Uint64(data[:]), nil
+ }(entry.Name())
+ if err != nil {
+ return nil, fmt.Errorf("cannot read value for CPU %s: %w", entry.Name(), err)
+ }
+
+ out[uint32(cpuNo)] = val
+ }
+
+ return out, nil
+}
+
+// AMD64 implements [HostEnvironment.AMD64].
+func (defaultEnvImpl) AMD64() (HostEnvironmentAMD64, error) {
+ return defaultEnvAMD64Impl{}, nil
+}
diff --git a/internal/efi/default_env_amd64_test.go b/internal/efi/default_env_amd64_test.go
new file mode 100644
index 00000000..e7b8f22a
--- /dev/null
+++ b/internal/efi/default_env_amd64_test.go
@@ -0,0 +1,119 @@
+//go:build amd64
+
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2021-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 .
+ *
+ */
+
+package efi_test
+
+import (
+ "encoding/binary"
+ "os"
+ "path/filepath"
+
+ "github.com/intel-go/cpuid"
+ . "github.com/snapcore/secboot/internal/efi"
+ "github.com/snapcore/secboot/internal/testutil"
+ . "gopkg.in/check.v1"
+)
+
+type defaultEnvAMD64Suite struct{}
+
+var _ = Suite(&defaultEnvAMD64Suite{})
+
+func (s *defaultEnvAMD64Suite) TestCPUVendorIdentificatorIntel(c *C) {
+ orig := cpuid.VendorIdentificatorString
+ cpuid.VendorIdentificatorString = "GenuineIntel"
+ defer func() { cpuid.VendorIdentificatorString = orig }()
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ c.Check(amd64.CPUVendorIdentificator(), Equals, "GenuineIntel")
+}
+
+func (s *defaultEnvAMD64Suite) TestCPUVendorIdentificatorAMD(c *C) {
+ orig := cpuid.VendorIdentificatorString
+ cpuid.VendorIdentificatorString = "AuthenticAMD"
+ defer func() { cpuid.VendorIdentificatorString = orig }()
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ c.Check(amd64.CPUVendorIdentificator(), Equals, "AuthenticAMD")
+}
+
+func (s *defaultEnvAMD64Suite) TestCPUIDHasFeatureSDBGTrue(c *C) {
+ restore := MockCPUIDHasFeature(func(feature uint64) bool {
+ c.Check(feature, Equals, cpuid.SDBG)
+ return true
+ })
+ defer restore()
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ c.Check(amd64.HasCPUIDFeature(cpuid.SDBG), testutil.IsTrue)
+}
+
+func (s *defaultEnvAMD64Suite) TestCPUIDHasFeatureSDBGFalse(c *C) {
+ restore := MockCPUIDHasFeature(func(feature uint64) bool {
+ c.Check(feature, Equals, cpuid.SDBG)
+ return false
+ })
+ defer restore()
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ c.Check(amd64.HasCPUIDFeature(cpuid.SDBG), testutil.IsFalse)
+}
+
+func (s *defaultEnvAMD64Suite) TestCPUIDHasFeatureSSE3True(c *C) {
+ restore := MockCPUIDHasFeature(func(feature uint64) bool {
+ c.Check(feature, Equals, cpuid.SSE3)
+ return true
+ })
+ defer restore()
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ c.Check(amd64.HasCPUIDFeature(cpuid.SSE3), testutil.IsTrue)
+}
+
+func (s *defaultEnvAMD64Suite) TestReadMSR(c *C) {
+ dir := c.MkDir()
+ restore := MockDevcpuPath(dir)
+ defer restore()
+
+ c.Assert(os.Mkdir(filepath.Join(dir, "0"), 0755), IsNil)
+ c.Assert(os.Mkdir(filepath.Join(dir, "1"), 0755), IsNil)
+
+ data := make([]byte, 0xc80)
+ var data8 [8]byte
+ binary.LittleEndian.PutUint64(data8[:], 0x40000000)
+ data = append(data, data8[:]...)
+
+ c.Assert(os.WriteFile(filepath.Join(dir, "0/msr"), data, 0644), IsNil)
+ c.Assert(os.WriteFile(filepath.Join(dir, "1/msr"), data, 0644), IsNil)
+
+ amd64, err := DefaultEnv.AMD64()
+ c.Assert(err, IsNil)
+ vals, err := amd64.ReadMSRs(0xc80)
+ c.Assert(err, IsNil)
+ c.Check(vals, DeepEquals, map[uint32]uint64{
+ 0: 0x40000000,
+ 1: 0x40000000,
+ })
+}
diff --git a/internal/efi/default_env_not_amd64.go b/internal/efi/default_env_not_amd64.go
new file mode 100644
index 00000000..dbea4dc4
--- /dev/null
+++ b/internal/efi/default_env_not_amd64.go
@@ -0,0 +1,27 @@
+//go:build !amd64
+
+// -*- 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 .
+ *
+ */
+
+package efi
+
+// AMD64 implements [HostEnvironment.AMD64].
+func (defaultEnvImpl) AMD64() (HostEnvironmentAMD64, error) {
+ return nil, ErrNotAMD64Host
+}
diff --git a/internal/efi/default_env_not_amd64_test.go b/internal/efi/default_env_not_amd64_test.go
new file mode 100644
index 00000000..316544c5
--- /dev/null
+++ b/internal/efi/default_env_not_amd64_test.go
@@ -0,0 +1,36 @@
+//go:build !amd64
+
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2021-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 .
+ *
+ */
+
+package efi_test
+
+import (
+ . "github.com/snapcore/secboot/internal/efi"
+ . "gopkg.in/check.v1"
+)
+
+type defaultEnvAMD64Suite struct{}
+
+var _ = Suite(&defaultEnvAMD64Suite{})
+
+func (s *defaultEnvAMD64Suite) TestNotAMD64Host(c *C) {
+ _, err := DefaultEnv.AMD64()
+ c.Check(err, Equals, ErrNotAMD64Host)
+}
diff --git a/internal/efi/default_env_test.go b/internal/efi/default_env_test.go
index a034d465..a2879bad 100644
--- a/internal/efi/default_env_test.go
+++ b/internal/efi/default_env_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2021 Canonical Ltd
+ * Copyright (C) 2021-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
@@ -21,23 +21,35 @@ package efi_test
import (
"context"
- _ "embed"
+ "errors"
"io"
"os"
+ "os/exec"
"path/filepath"
efi "github.com/canonical/go-efilib"
"github.com/canonical/go-tpm2"
+ "github.com/canonical/go-tpm2/linux"
"github.com/canonical/tcglog-parser"
. "github.com/snapcore/secboot/internal/efi"
"github.com/snapcore/secboot/internal/efitest"
"github.com/snapcore/secboot/internal/testutil"
+ snapd_testutil "github.com/snapcore/snapd/testutil"
. "gopkg.in/check.v1"
)
type defaultEnvSuite struct{}
+func (s *defaultEnvSuite) mockSysfsPath(c *C, path string) (mockedPath string, restore func()) {
+ dir := c.MkDir()
+
+ cmd := exec.Command("tar", "xaf", path, "-C", dir)
+ c.Assert(cmd.Run(), IsNil)
+
+ return dir, MockSysfsPath(dir)
+}
+
var _ = Suite(&defaultEnvSuite{})
type testKey struct{}
@@ -94,3 +106,202 @@ func (s *defaultEnvSuite) TestReadEventLog2(c *C) {
SecureBootDisabled: true,
})
}
+
+func (s *defaultEnvSuite) TestTPMDeviceRM(c *C) {
+ rawDev := new(linux.RawDevice)
+ restore := MockLinuxDefaultTPM2Device(rawDev, nil)
+ defer restore()
+
+ rmDev := new(linux.RMDevice)
+ restore = MockLinuxRawDeviceResourceManagedDevice(c, rawDev, rmDev, nil)
+ defer restore()
+
+ dev, err := DefaultEnv.TPMDevice()
+ c.Check(err, IsNil)
+ c.Check(dev, Equals, rmDev)
+}
+
+func (s *defaultEnvSuite) TestTPMDeviceRaw(c *C) {
+ rawDev := new(linux.RawDevice)
+ restore := MockLinuxDefaultTPM2Device(rawDev, nil)
+ defer restore()
+
+ restore = MockLinuxRawDeviceResourceManagedDevice(c, rawDev, nil, linux.ErrNoResourceManagedDevice)
+ defer restore()
+
+ dev, err := DefaultEnv.TPMDevice()
+ c.Check(err, IsNil)
+ c.Check(dev, Equals, rawDev)
+}
+
+func (s *defaultEnvSuite) TestTPMDeviceNoDevicesErr(c *C) {
+ restore := MockLinuxDefaultTPM2Device(nil, linux.ErrNoTPMDevices)
+ defer restore()
+
+ _, err := DefaultEnv.TPMDevice()
+ c.Check(err, Equals, ErrNoTPM2Device)
+}
+
+func (s *defaultEnvSuite) TestTPMDeviceNoDevicesOtherErr(c *C) {
+ restore := MockLinuxDefaultTPM2Device(nil, errors.New("some error"))
+ defer restore()
+
+ _, err := DefaultEnv.TPMDevice()
+ c.Check(err, ErrorMatches, `some error`)
+}
+
+func (s *defaultEnvSuite) TestTPMDeviceRMOtherErr(c *C) {
+ rawDev := new(linux.RawDevice)
+ restore := MockLinuxDefaultTPM2Device(rawDev, nil)
+ defer restore()
+
+ restore = MockLinuxRawDeviceResourceManagedDevice(c, rawDev, nil, errors.New("some error"))
+ defer restore()
+
+ _, err := DefaultEnv.TPMDevice()
+ c.Check(err, ErrorMatches, `some error`)
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeNoneAny(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo none; exit 1`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeAll)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, VirtModeNone)
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeKVMAny(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo kvm`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeAll)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, "kvm")
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeNoneContainer(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo none; exit 1`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeContainer)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, VirtModeNone)
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt", "--container"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeLXCContainer(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo lxc`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeContainer)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, "lxc")
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt", "--container"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeNoneVM(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo none; exit 1`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeVM)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, VirtModeNone)
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt", "--vm"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeKVMVM(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo kvm`)
+ defer cmd.Restore()
+
+ virt, err := DefaultEnv.DetectVirtMode(DetectVirtModeVM)
+ c.Check(err, IsNil)
+ c.Check(virt, Equals, "kvm")
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt", "--vm"})
+}
+
+func (s *defaultEnvSuite) TestDetectVirtModeErr(c *C) {
+ cmd := snapd_testutil.MockCommand(c, "systemd-detect-virt", `echo kvm; exit 1`)
+ defer cmd.Restore()
+
+ _, err := DefaultEnv.DetectVirtMode(DetectVirtModeAll)
+ c.Check(err, ErrorMatches, `exit status 1`)
+
+ c.Check(cmd.Calls(), HasLen, 1)
+ c.Check(cmd.Calls()[0], DeepEquals, []string{"systemd-detect-virt"})
+}
+
+func (s *defaultEnvSuite) TestDevicesForClassMEI(c *C) {
+ path, restore := s.mockSysfsPath(c, "testdata/sys.tar")
+ defer restore()
+
+ devices, err := DefaultEnv.DevicesForClass("mei")
+ c.Check(err, IsNil)
+ c.Assert(devices, HasLen, 1)
+ c.Check(devices[0].Name(), Equals, "mei0")
+ c.Check(devices[0].Path(), Equals, filepath.Join(path, "devices/pci00000:00/0000:00:16.0/mei/mei0"))
+ c.Check(devices[0].Subsystem(), Equals, "mei")
+
+ rc, err := devices[0].AttributeReader("fw_ver")
+ c.Assert(err, IsNil)
+ defer rc.Close()
+
+ data, err := io.ReadAll(rc)
+ c.Check(err, IsNil)
+ c.Check(data, DeepEquals, []byte(`0:16.1.27.2176
+0:16.1.27.2176
+0:16.0.15.1624
+`))
+}
+
+func (s *defaultEnvSuite) TestDevicesForClassIOMMU(c *C) {
+ path, restore := s.mockSysfsPath(c, "testdata/sys.tar")
+ defer restore()
+
+ devices, err := DefaultEnv.DevicesForClass("iommu")
+ c.Check(err, IsNil)
+ c.Assert(devices, HasLen, 1)
+ c.Check(devices[0].Name(), Equals, "dmar0")
+ c.Check(devices[0].Path(), Equals, filepath.Join(path, "devices/virtual/iommu/dmar0"))
+ c.Check(devices[0].Subsystem(), Equals, "iommu")
+}
+
+func (s *defaultEnvSuite) TestDevicesForClassNotExist(c *C) {
+ _, restore := s.mockSysfsPath(c, "testdata/sys.tar")
+ defer restore()
+
+ devices, err := DefaultEnv.DevicesForClass("foo")
+ c.Check(err, IsNil)
+ c.Assert(devices, HasLen, 0)
+}
+
+func (s *defaultEnvSuite) TestSysfsDeviceAttributeReaderNoAttr(c *C) {
+ _, restore := s.mockSysfsPath(c, "testdata/sys.tar")
+ defer restore()
+
+ devices, err := DefaultEnv.DevicesForClass("mei")
+ c.Check(err, IsNil)
+ c.Assert(devices, HasLen, 1)
+
+ _, err = devices[0].AttributeReader("uevent")
+ c.Check(err, Equals, ErrNoDeviceAttribute)
+ _, err = devices[0].AttributeReader("foo")
+ c.Check(err, Equals, ErrNoDeviceAttribute)
+ _, err = devices[0].AttributeReader("subsystem")
+ c.Check(err, Equals, ErrNoDeviceAttribute)
+
+}
diff --git a/internal/efi/env.go b/internal/efi/env.go
index d6404a73..e4dc9b31 100644
--- a/internal/efi/env.go
+++ b/internal/efi/env.go
@@ -21,14 +21,21 @@ package efi
import (
"context"
+ "errors"
+ "io"
+ "github.com/canonical/go-tpm2"
"github.com/canonical/tcglog-parser"
)
-// HostEnvironment is an interface that abstracts out an EFI environment, so that
+// XXX: Some of the interfaces here are really public, but they are here because they are shared by
+// the public efi and efi/preinstall packages. I wonder if there needs to be a public efi/common
+// package for these interfaces to live in.
+
+// HostEnvironmentEFI is an interface that abstracts out an EFI environment, so that
// consumers of the API can provide a custom mechanism to read EFI variables or parse
-// the TCG event log. This needs to be kept in sync with [efi.HostEnvironment].
-type HostEnvironment interface {
+// the TCG event log.
+type HostEnvironmentEFI interface {
// VarContext returns a copy of parent containing a VarsBackend, keyed by efi.VarsBackendKey,
// for interacting with EFI variables via go-efilib. This context can be passed to any
// go-efilib function that interacts with EFI variables. Right now, go-efilib doesn't
@@ -42,3 +49,90 @@ type HostEnvironment interface {
// ReadEventLog reads the TCG event log
ReadEventLog() (*tcglog.Log, error)
}
+
+// SysfsDevice corresponds to a device in the sysfs tree.
+type SysfsDevice interface {
+ Name() string // the name of the device
+ Path() string // the fully evaluated sysfs path for the device
+ Subsystem() string // the device subsystem name
+
+ // AttributeReader returns an io.ReadCloser to read the specified
+ // attribute for the device. The caller should call Close when
+ // finished.
+ AttributeReader(attr string) (io.ReadCloser, error)
+}
+
+// HostEnvironmentAMD64 is an interface that abstracts out a host environment specific
+// to AMD64 platforms.
+type HostEnvironmentAMD64 interface {
+ // CPUVendorIdentificator returns the CPU vendor.
+ CPUVendorIdentificator() string
+
+ // HasCPUIDFeature returns if feature from FeatureNames map in the
+ // github.com/intel-go/cpuid package is available.
+ HasCPUIDFeature(feature uint64) bool
+
+ // ReadMSRs reads the value of the specified MSR for all CPUs,
+ // returning a map of the results keyed by the CPU numbers.
+ ReadMSRs(msr uint32) (map[uint32]uint64, error)
+}
+
+// DetectVirtMode controls what type of virtualization to test for.
+type DetectVirtMode int
+
+const (
+ // DetectVirtModeAll detects for all types of virtualization.
+ DetectVirtModeAll DetectVirtMode = iota
+
+ // DetectVirtModeContainer detects for container types of virtualization.
+ DetectVirtModeContainer
+
+ // DetectVirtModeVM detects for fully virtualized types of environments.
+ DetectVirtModeVM
+)
+
+// VirtModeNone corresponds to no virtualization.
+const VirtModeNone = "none"
+
+var (
+ // ErrNoTPM2Device is returned from HostEnvironment.TPMDevice if no TPM2
+ // device is available.
+ ErrNoTPM2Device = errors.New("no TPM2 device is available")
+
+ // ErrNoDeviceAttribute is returned from SysfsDevice.Attribute if the supplied attribute
+ // does not exist.
+ ErrNoDeviceAttribute = errors.New("device attribute does not exist")
+
+ // ErrNotAMD64Host is returned from HostEnvironment.AMD64 on environments that
+ // are not AMD64.
+ ErrNotAMD64Host = errors.New("not a AMD64 host")
+
+ // ErrNoKernelMSRSupport is returned from HostEnvironmentAMD64.ReadMSRs if there is
+ // no support for reading MSRs.
+ ErrNoKernelMSRSupport = errors.New("missing kernel support for reading MSRs")
+
+ // ErrNoMSRSupport is returned from HostEnvironmentAMD64.ReadMSRs if there is
+ // no MSR support or the specified MSR cannot be read.
+ ErrNoMSRSupport = errors.New("missing MSR support")
+)
+
+// HostEnvironment is an interface that abstracts out a host environment, so that
+// consumers of the API can provide ways to mock parts of an environment.
+type HostEnvironment interface {
+ HostEnvironmentEFI
+
+ // TPMDevice returns a TPMDevice that can be used to open a tpm2.TPMContext.
+ TPMDevice() (tpm2.TPMDevice, error)
+
+ // DetectVirtMode returns whether the environment is virtualized. If not, it returns
+ // (VirtModeNone, nil). The mode can be used to choose what type of virtualization to
+ // test for.
+ DetectVirtMode(mode DetectVirtMode) (string, error)
+
+ // DevicesForClass returns a list of devices with the specified class.
+ DevicesForClass(class string) ([]SysfsDevice, error)
+
+ // AMD64 returns an interface that can be used to mock some parts of an AMD64 platform.
+ // This will return ErrNotAMD64CPU on non-AMD64 platforms.
+ AMD64() (HostEnvironmentAMD64, error)
+}
diff --git a/internal/efi/export_amd64_test.go b/internal/efi/export_amd64_test.go
new file mode 100644
index 00000000..d75b464d
--- /dev/null
+++ b/internal/efi/export_amd64_test.go
@@ -0,0 +1,38 @@
+//go:build amd64
+
+// -*- 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 .
+ *
+ */
+
+package efi
+
+func MockDevcpuPath(path string) (restore func()) {
+ orig := devcpuPath
+ devcpuPath = path
+ return func() {
+ devcpuPath = orig
+ }
+}
+
+func MockCPUIDHasFeature(fn func(uint64) bool) (restore func()) {
+ orig := cpuidHasFeature
+ cpuidHasFeature = fn
+ return func() {
+ cpuidHasFeature = orig
+ }
+}
diff --git a/internal/efi/export_test.go b/internal/efi/export_test.go
index d37b2909..3c3e949e 100644
--- a/internal/efi/export_test.go
+++ b/internal/efi/export_test.go
@@ -19,6 +19,11 @@
package efi
+import (
+ "github.com/canonical/go-tpm2/linux"
+ . "gopkg.in/check.v1"
+)
+
func MockEventLogPath(path string) (restore func()) {
origPath := eventLogPath
eventLogPath = path
@@ -26,3 +31,32 @@ func MockEventLogPath(path string) (restore func()) {
eventLogPath = origPath
}
}
+
+func MockLinuxDefaultTPM2Device(dev *linux.RawDevice, err error) (restore func()) {
+ orig := linuxDefaultTPM2Device
+ linuxDefaultTPM2Device = func() (*linux.RawDevice, error) {
+ return dev, err
+ }
+ return func() {
+ linuxDefaultTPM2Device = orig
+ }
+}
+
+func MockLinuxRawDeviceResourceManagedDevice(c *C, expectedDev *linux.RawDevice, dev *linux.RMDevice, err error) (restore func()) {
+ orig := linuxRawDeviceResourceManagedDevice
+ linuxRawDeviceResourceManagedDevice = func(device *linux.RawDevice) (*linux.RMDevice, error) {
+ c.Check(device, Equals, expectedDev)
+ return dev, err
+ }
+ return func() {
+ linuxRawDeviceResourceManagedDevice = orig
+ }
+}
+
+func MockSysfsPath(path string) (restore func()) {
+ orig := sysfsPath
+ sysfsPath = path
+ return func() {
+ sysfsPath = orig
+ }
+}
diff --git a/internal/efi/options.go b/internal/efi/options.go
index 679ca1fc..939075cf 100644
--- a/internal/efi/options.go
+++ b/internal/efi/options.go
@@ -31,7 +31,7 @@ type PCRProfileOptionVisitor interface {
AddPCRs(pcrs ...tpm2.Handle)
// SetEnvironment overrides the host environment to the supplied environment.
- SetEnvironment(env HostEnvironment)
+ SetEnvironment(env HostEnvironmentEFI)
// AddInitialVariablesModifier adds a function that will be called to allow
// the initial variable set for profile generation to be modified.
diff --git a/internal/efi/testdata/sys.tar b/internal/efi/testdata/sys.tar
new file mode 100644
index 00000000..5eef5c80
Binary files /dev/null and b/internal/efi/testdata/sys.tar differ
diff --git a/internal/efitest/hostenv.go b/internal/efitest/hostenv.go
index 152ab767..0f63fecf 100644
--- a/internal/efitest/hostenv.go
+++ b/internal/efitest/hostenv.go
@@ -20,35 +20,254 @@
package efitest
import (
+ "bytes"
"context"
"errors"
+ "io"
efi "github.com/canonical/go-efilib"
+ "github.com/canonical/go-tpm2"
"github.com/canonical/tcglog-parser"
+ internal_efi "github.com/snapcore/secboot/internal/efi"
)
-// MockHostEnvironment provides a mock EFI host environment.
+// MockHostEnvironment provides a mock host environment that can be used by both
+// the efi and efi/preinstall packages.
type MockHostEnvironment struct {
Vars MockVars
Log *tcglog.Log
+
+ TPM2Device tpm2.TPMDevice
+
+ VirtMode string
+ VirtModeType internal_efi.DetectVirtMode
+ VirtModeErr error
+ DelayedVirtModeOptions []MockHostEnvironmentOption
+
+ Devices map[string][]internal_efi.SysfsDevice
+
+ AMD64Env internal_efi.HostEnvironmentAMD64
}
-// NewMockHostEnvironment returns a new MockHostEnvironment.
func NewMockHostEnvironment(vars MockVars, log *tcglog.Log) *MockHostEnvironment {
return &MockHostEnvironment{
- Vars: vars,
- Log: log}
+ Vars: vars,
+ Log: log,
+ VirtMode: internal_efi.VirtModeNone,
+ }
+}
+
+// MockHostEnvironmentOption is an option supplied to [NewMockHostEnvironmentWithOpts].
+type MockHostEnvironmentOption func(*MockHostEnvironment)
+
+// WithMockVars adds the supplied mock EFI variables to a [MockHostEnvironment].
+func WithMockVars(vars MockVars) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.Vars = vars
+ }
+}
+
+// WithLog adds the supplied TCG log to a [MockHostEnvironment].
+func WithLog(log *tcglog.Log) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.Log = log
+ }
+}
+
+// WithTPMDevice adds the specified TPM device to a [MockHostEnvironment].
+func WithTPMDevice(device tpm2.TPMDevice) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.TPM2Device = device
+ }
+}
+
+// WithVirtMode adds the supplied virtualization mode and type to a [MockHostEnvironment].
+func WithVirtMode(mode string, modeType internal_efi.DetectVirtMode) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.VirtMode = mode
+ env.VirtModeType = modeType
+ }
+}
+
+// WithVirtModeError makes [MockHostEnvironment.DetectVirtMode] return an error.
+func WithVirtModeError(err error) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.VirtModeErr = err
+ }
+}
+
+// WithDelayedVirtMode queues to add the supplied virtualization mode and type to a [MockHostEnvironment].
+// These will be applied in turn on each call to [MockHostEnvironment.DetectVirtMode].
+func WithDelayedVirtMode(mode string, modeType internal_efi.DetectVirtMode) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.DelayedVirtModeOptions = append(env.DelayedVirtModeOptions, WithVirtMode(mode, modeType))
+ }
+}
+
+// WithVirtModeError queues to make [MockHostEnvironment.DetectVirtMode] return an error. These will be
+// applied in turn on each call to [MockHostEnvironment.DetectVirtMode].
+func WithDelayedVirtModeError(err error) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.DelayedVirtModeOptions = append(env.DelayedVirtModeOptions, WithVirtModeError(err))
+ }
+}
+
+type mockHostEnvironmentAMD64 struct {
+ vendorIdentificator string
+ features map[uint64]struct{}
+ cpus uint32
+ msrs map[uint32]uint64
}
-// VarContext implements [github.com/snapcore/secboot/efi.HostEnvironment.VarContext].
+func (e *mockHostEnvironmentAMD64) CPUVendorIdentificator() string {
+ return e.vendorIdentificator
+}
+
+func (e *mockHostEnvironmentAMD64) HasCPUIDFeature(feature uint64) bool {
+ _, has := e.features[feature]
+ return has
+}
+
+func (e *mockHostEnvironmentAMD64) ReadMSRs(msr uint32) (map[uint32]uint64, error) {
+ val, exists := e.msrs[msr]
+ if !exists {
+ return nil, errors.New("MSR does not exist")
+ }
+ out := make(map[uint32]uint64)
+ for i := uint32(0); i < e.cpus; i++ {
+ out[i] = val
+ }
+ return out, nil
+}
+
+// MockSysfsDevice is a mock implementation of [internal_efi.SysfsDevice].
+type MockSysfsDevice struct {
+ DeviceName string
+ DevicePath string
+ DeviceSubsystem string
+
+ DeviceAttributeVals map[string][]byte
+}
+
+func (d *MockSysfsDevice) Name() string { return d.DeviceName }
+func (d *MockSysfsDevice) Path() string { return d.DevicePath }
+func (d *MockSysfsDevice) Subsystem() string { return d.DeviceSubsystem }
+
+func (d *MockSysfsDevice) AttributeReader(attr string) (rc io.ReadCloser, err error) {
+ if d.DeviceAttributeVals == nil {
+ return nil, internal_efi.ErrNoDeviceAttribute
+ }
+ data, exists := d.DeviceAttributeVals[attr]
+ if !exists {
+ return nil, internal_efi.ErrNoDeviceAttribute
+ }
+ return io.NopCloser(bytes.NewReader(data)), nil
+}
+
+// NewMockSysfsDevice returns a new MockSysfsDevice.
+func NewMockSysfsDevice(name, path, subsystem string, attributeVals map[string][]byte) *MockSysfsDevice {
+ return &MockSysfsDevice{
+ DeviceName: name,
+ DevicePath: path,
+ DeviceSubsystem: subsystem,
+ DeviceAttributeVals: attributeVals,
+ }
+}
+
+// WithSysfsDevices adds the supplied devices, keyed by class, to the [MockHostEnvironment].
+func WithSysfsDevices(devices map[string][]internal_efi.SysfsDevice) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ env.Devices = devices
+ }
+}
+
+// WithAMD64Environment adds a [github.com/snapcore/secboot/efi/internal.HostEnvironmentAMD64] to the [MockHostEnvironment].
+// Whilst this supports mocking MSRs, it only supports the same value for every CPU.
+func WithAMD64Environment(cpuVendorIdentificator string, cpuidFeatures []uint64, cpus uint32, msrs map[uint32]uint64) MockHostEnvironmentOption {
+ return func(env *MockHostEnvironment) {
+ features := make(map[uint64]struct{})
+ for _, feature := range cpuidFeatures {
+ features[feature] = struct{}{}
+ }
+ env.AMD64Env = &mockHostEnvironmentAMD64{
+ vendorIdentificator: cpuVendorIdentificator,
+ features: features,
+ msrs: msrs,
+ }
+ }
+}
+
+// NewMockHostEnvironmentWithOpts returns a new MockHostEnvironment.
+func NewMockHostEnvironmentWithOpts(options ...MockHostEnvironmentOption) *MockHostEnvironment {
+ env := &MockHostEnvironment{
+ VirtMode: internal_efi.VirtModeNone,
+ }
+ for _, opt := range options {
+ opt(env)
+ }
+ return env
+}
+
+// VarContext implements [github.com/snapcore/secboot/internal/efi.HostEnvironmentEFI.VarContext].
func (e *MockHostEnvironment) VarContext(parent context.Context) context.Context {
return context.WithValue(parent, efi.VarsBackendKey{}, e.Vars)
}
-// ReadEventLog implements [github.com/snapcore/secboot/efi.HostEnvironment.ReadEventLog].
+// ReadEventLog implements [github.com/snapcore/secboot/internal/efi.HostEnvironmentEFI.ReadEventLog].
func (e *MockHostEnvironment) ReadEventLog() (*tcglog.Log, error) {
if e.Log == nil {
return nil, errors.New("nil log")
}
return e.Log, nil
}
+
+// TPMDevice implements [github.com/snapcore/secboot/internal/efi.HostEnvironment.TPMDevice].
+func (e *MockHostEnvironment) TPMDevice() (tpm2.TPMDevice, error) {
+ if e.TPM2Device == nil {
+ return nil, internal_efi.ErrNoTPM2Device
+ }
+ return e.TPM2Device, nil
+}
+
+// DetectVirtMode implements [github.com/snapcore/secboot/internal/efi.HostEnvironment.DetectVirtMode].
+func (e *MockHostEnvironment) DetectVirtMode(mode internal_efi.DetectVirtMode) (string, error) {
+ if len(e.DelayedVirtModeOptions) > 0 {
+ opt := e.DelayedVirtModeOptions[0]
+ e.DelayedVirtModeOptions = e.DelayedVirtModeOptions[1:]
+ opt(e)
+ }
+
+ if e.VirtModeErr != nil {
+ return "", e.VirtModeErr
+ }
+
+ switch mode {
+ case internal_efi.DetectVirtModeAll:
+ return e.VirtMode, nil
+ case internal_efi.DetectVirtModeContainer, internal_efi.DetectVirtModeVM:
+ if e.VirtModeType == mode {
+ return e.VirtMode, nil
+ }
+ }
+ return internal_efi.VirtModeNone, nil
+}
+
+// DevicesForClass implements [github.com/snapcore/secboot/internal/efi.HostEnvironment.DevicesForClass].
+func (e *MockHostEnvironment) DevicesForClass(class string) ([]internal_efi.SysfsDevice, error) {
+ if e.Devices == nil {
+ return nil, errors.New("nil devices")
+ }
+ devices, exists := e.Devices[class]
+ if !exists {
+ return nil, nil
+ }
+ return devices, nil
+}
+
+// AMD64 implements [github.com/snapcore/secboot/internal/efi.HostEnvironment.AMD64].
+func (e *MockHostEnvironment) AMD64() (internal_efi.HostEnvironmentAMD64, error) {
+ if e.AMD64Env == nil {
+ return nil, internal_efi.ErrNotAMD64Host
+ }
+ return e.AMD64Env, nil
+}
diff --git a/tpm2/tpm_test.go b/tpm2/tpm_test.go
index 65eed426..76389f12 100644
--- a/tpm2/tpm_test.go
+++ b/tpm2/tpm_test.go
@@ -20,6 +20,7 @@
package tpm2_test
import (
+ "bytes"
"crypto/x509"
"io"
"os"
@@ -165,27 +166,41 @@ func (s *tpmSuiteNoTPM) TestConnectToDefaultTPMNoTPM(c *C) {
// We don't have a TPM1.2 simulator, so create a mock TCTI that just returns
// a TPM_BAD_ORDINAL error
-type mockTPM12Tcti struct{}
+type mockTPM12Transport struct {
+ rsp io.Reader
+}
+
+func (t *mockTPM12Transport) Read(data []byte) (int, error) {
+ for {
+ n, err := t.rsp.Read(data)
+ if err == io.EOF {
+ t.rsp = nil
+ err = nil
+ if n == 0 {
+ continue
+ }
+ }
+ return n, err
+ }
+}
-func (t *mockTPM12Tcti) Read(data []byte) (int, error) {
+func (t *mockTPM12Transport) Write(data []byte) (int, error) {
+ buf := new(bytes.Buffer)
// tag = TPM_TAG_RSP_COMMAND (0xc4)
// paramSize = 10
// returnCode = TPM_BAD_ORDINAL (10)
- b := mu.MustMarshalToBytes(tpm2.TagRspCommand, uint32(10), tpm2.ResponseBadTag)
- return copy(data, b), io.EOF
-}
-
-func (t *mockTPM12Tcti) Write(data []byte) (int, error) {
+ mu.MustMarshalToWriter(buf, tpm2.TagRspCommand, uint32(10), tpm2.ResponseBadTag)
+ t.rsp = buf
return len(data), nil
}
-func (t *mockTPM12Tcti) Close() error {
+func (t *mockTPM12Transport) Close() error {
return nil
}
func (s *tpmSuiteNoTPM) TestConnectToDefaultTPM12(c *C) {
restore := tpm2test.MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) {
- return &mockTPM12Tcti{}, nil
+ return &mockTPM12Transport{}, nil
})
s.AddCleanup(restore)