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)