diff --git a/go.mod b/go.mod index c5774ef5..9cb64703 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/canonical/go-efilib v1.4.1 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.7.6 + github.com/canonical/go-tpm2 v1.8.1-0.20250106220815-bd8d7cc9ba17 github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981 github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85 golang.org/x/crypto v0.21.0 @@ -19,6 +19,7 @@ require ( ) require ( + github.com/canonical/go-kbkdf v0.0.0-20250104172618-3b1308f9acf9 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect github.com/kr/text v0.1.0 // indirect diff --git a/go.sum b/go.sum index 5deb1484..22d2de59 100644 --- a/go.sum +++ b/go.sum @@ -3,14 +3,16 @@ github.com/canonical/cpuid v0.0.0-20220614022739-219e067757cb/go.mod h1:6j8Sw3dw github.com/canonical/go-efilib v0.0.0-20210909101908-41435fa545d4/go.mod h1:9Sr9kd7IhQPYqaU5nut8Ky97/CtlhHDzQncQnrULgDM= github.com/canonical/go-efilib v1.4.1 h1:/VMNCypz+iVmnNuMcsm7WvmDMI1ObkEP2W1h8Ls7OyM= github.com/canonical/go-efilib v1.4.1/go.mod h1:n0Ttsy1JuHAvqaFbZBs6PAzoiiJdfkHsAmDOEbexYEQ= +github.com/canonical/go-kbkdf v0.0.0-20250104172618-3b1308f9acf9 h1:Twk1ZSTWRClfGShP16ePf2JIiayqWS4ix1rkAR6baag= +github.com/canonical/go-kbkdf v0.0.0-20250104172618-3b1308f9acf9/go.mod h1:IneQ5/yQcfPXrGekEXpR6yeea55ZD24N5+kHzeDseOM= github.com/canonical/go-sp800.108-kdf v0.0.0-20210314145419-a3359f2d21b9/go.mod h1:Zrs3YjJr+w51u0R/dyLh/oWt/EcBVdLPCVFYC4daW5s= github.com/canonical/go-sp800.108-kdf v0.0.0-20210315104021-ead800bbf9a0 h1:ZE2XMRFHcwlib3uU9is37+pKkkMloVoEPWmgQ6GK1yo= github.com/canonical/go-sp800.108-kdf v0.0.0-20210315104021-ead800bbf9a0/go.mod h1:Zrs3YjJr+w51u0R/dyLh/oWt/EcBVdLPCVFYC4daW5s= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= github.com/canonical/go-tpm2 v0.0.0-20210827151749-f80ff5afff61/go.mod h1:vG41hdbBjV4+/fkubTT1ENBBqSkLwLr7mCeW9Y6kpZY= -github.com/canonical/go-tpm2 v1.7.6 h1:9k9OAEEp9xKp4h2WJwfTUNivblJi4L5Wjx7Q/LkSTSQ= -github.com/canonical/go-tpm2 v1.7.6/go.mod h1:Dz0PQRmoYrmk/4BLILjRA+SFzuqEo1etAvYeAJiMhYU= +github.com/canonical/go-tpm2 v1.8.1-0.20250106220815-bd8d7cc9ba17 h1:bzX4uvym3OoyKAQd2fTUOP4tAuHbQktwsezk6KTXAgc= +github.com/canonical/go-tpm2 v1.8.1-0.20250106220815-bd8d7cc9ba17/go.mod h1:zK+qESVwu78XyX+NPhiBdN+zwPPDoKk4rYlQ7VUsRp4= github.com/canonical/tcglog-parser v0.0.0-20210824131805-69fa1e9f0ad2/go.mod h1:QoW2apR2tBl6T/4czdND/EHjL1Ia9cCmQnIj9Xe0Kt8= github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981 h1:vrUzSfbhl8mzdXPzjxq4jXZPCCNLv18jy6S7aVTS2tI= github.com/canonical/tcglog-parser v0.0.0-20240924110432-d15eaf652981/go.mod h1:ywdPBqUGkuuiitPpVWCfilf2/gq+frhq4CNiNs9KyHU= diff --git a/internal/tpm2_device/device.go b/internal/tpm2_device/device.go new file mode 100644 index 00000000..f0b33ffd --- /dev/null +++ b/internal/tpm2_device/device.go @@ -0,0 +1,97 @@ +// -*- 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 tpm2_device + +import ( + "errors" + + "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/ppi" +) + +// DeviceMode describes the mode to select the default device. +type DeviceMode int + +const ( + // DeviceModeDirect requests the most direct TPM2 device, without + // the use of a resource manager. These devices cannot be opened more + // than once and don't permit the TPM to be shared. + DeviceModeDirect DeviceMode = iota + + // DeviceModeResourceManaged requests a resource managed TPM2 device. + // These devices can be opened more than once and sharedi, relying on + // the resource manager to handle context switching between users + // (although they can't be shared with a direct device). + DeviceModeResourceManaged + + // DeviceModeTryResourceManaged is like DeviceModeResourceManaged except + // it will return a direct device if a resource managed device is not + // available. Some older linux kernels do not support an in-kernel resource + // manager. + DeviceModeTryResourceManaged +) + +var ( + // ErrNoTPM2Device indicates that no TPM2 device is available. + ErrNoTPM2Device = errors.New("no TPM2 device is available") + + // ErrNoResourceManagedTPM2Device indicates that there is no resource + // managed TPM2 device option available. + ErrNoResourceManagedTPM2Device = errors.New("no resource managed TPM2 device available") + + // ErrNoPPI indicates that no physical presence interface is available. + ErrNoPPI = errors.New("no physical presence interface available") +) + +type tpmDevice struct { + tpm2.TPMDevice + + mode DeviceMode + + ppi ppi.PPI + ppiErr error +} + +func (d *tpmDevice) Mode() DeviceMode { + return d.mode +} + +func (d *tpmDevice) PPI() (ppi.PPI, error) { + if d.ppiErr != nil { + return nil, d.ppiErr + } + if d.ppi == nil { + return nil, ErrNoPPI + } + return d.ppi, nil +} + +// TPMDevice corresponds to a [tpm2.TPMDevice] with some extra features. +type TPMDevice interface { + tpm2.TPMDevice + Mode() DeviceMode // either DeviceModeDirect or DeviceModeResourceManaged + PPI() (ppi.PPI, error) // provide access to the physical presence interface +} + +// DefaultDevice returns the default TPM device. The specified mode controls what kind +// of device to return, if available. +var DefaultDevice = func(DeviceMode) (TPMDevice, error) { + return nil, ErrNoTPM2Device +} diff --git a/internal/tpm2_device/device_linux.go b/internal/tpm2_device/device_linux.go new file mode 100644 index 00000000..39785b93 --- /dev/null +++ b/internal/tpm2_device/device_linux.go @@ -0,0 +1,92 @@ +// -*- 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 tpm2_device + +import ( + "errors" + + "github.com/canonical/go-tpm2/linux" +) + +var ( + linuxDefaultTPM2Device = linux.DefaultTPM2Device + linuxRawDeviceResourceManagedDevice = (*linux.RawDevice).ResourceManagedDevice + linuxRawDevicePhysicalPresenceInterface = (*linux.RawDevice).PhysicalPresenceInterface + linuxRMDeviceRawDevice = (*linux.RMDevice).RawDevice +) + +func newTpmDeviceDirect(dev *linux.RawDevice) TPMDevice { + ppi, err := linuxRawDevicePhysicalPresenceInterface(dev) + if errors.Is(err, linux.ErrNoPhysicalPresenceInterface) { + err = nil + } + return &tpmDevice{ + TPMDevice: dev, + mode: DeviceModeDirect, + ppi: ppi, + ppiErr: err, + } +} + +func newTpmDeviceRM(dev *linux.RMDevice) TPMDevice { + ppi, err := linuxRawDevicePhysicalPresenceInterface(linuxRMDeviceRawDevice(dev)) + if errors.Is(err, linux.ErrNoPhysicalPresenceInterface) { + err = nil + } + return &tpmDevice{ + TPMDevice: dev, + mode: DeviceModeResourceManaged, + ppi: ppi, + ppiErr: err, + } +} + +func init() { + DefaultDevice = func(mode DeviceMode) (TPMDevice, error) { + rawDev, err := linuxDefaultTPM2Device() + switch { + case errors.Is(err, linux.ErrDefaultNotTPM2Device) || errors.Is(err, linux.ErrNoTPMDevices): + // Either there are no TPM devices or the default device is a TPM1.2 device + return nil, ErrNoTPM2Device + case err != nil: + return nil, err + } + + if mode == DeviceModeDirect { + // Return the direct device + return newTpmDeviceDirect(rawDev), nil + } + + rmDev, err := linuxRawDeviceResourceManagedDevice(rawDev) + switch { + case errors.Is(err, linux.ErrNoResourceManagedDevice) && mode == DeviceModeTryResourceManaged: + // No in-kernel resource manager, but the mode allows us to return the direct device + return newTpmDeviceDirect(rawDev), nil + case errors.Is(err, linux.ErrNoResourceManagedDevice): + // No in-kernel resource manager, return an error + return nil, ErrNoResourceManagedTPM2Device + case err != nil: + return nil, err + } + + // Return the resource managed device + return newTpmDeviceRM(rmDev), nil + } +} diff --git a/internal/tpm2_device/device_linux_test.go b/internal/tpm2_device/device_linux_test.go new file mode 100644 index 00000000..5a3f4c1b --- /dev/null +++ b/internal/tpm2_device/device_linux_test.go @@ -0,0 +1,345 @@ +//go:build linux + +// -*- 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 tpm2_device_test + +import ( + "errors" + + "github.com/canonical/go-tpm2/linux" + "github.com/canonical/go-tpm2/ppi" + . "github.com/snapcore/secboot/internal/tpm2_device" + . "gopkg.in/check.v1" +) + +type deviceLinuxSuite struct{} + +var _ = Suite(&deviceLinuxSuite{}) + +func (s *deviceLinuxSuite) TestDefaultDeviceDefaultNotTPM2Device(c *C) { + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return nil, linux.ErrDefaultNotTPM2Device + }) + defer restore() + + _, err := DefaultDevice(DeviceModeDirect) + c.Check(err, Equals, ErrNoTPM2Device) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceDefaultNoTPMDevices(c *C) { + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return nil, linux.ErrNoTPMDevices + }) + defer restore() + + _, err := DefaultDevice(DeviceModeDirect) + c.Check(err, Equals, ErrNoTPM2Device) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceDefaultOtherError(c *C) { + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return nil, errors.New("some error") + }) + defer restore() + + _, err := DefaultDevice(DeviceModeDirect) + c.Check(err, ErrorMatches, `some error`) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceDirect(c *C) { + expectedRaw := new(linux.RawDevice) + expectedPPI := new(mockPPI) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return expectedPPI, nil + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeDirect) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRaw, DeviceModeDirect, expectedPPI, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceDirectNoPPI(c *C) { + expectedRaw := new(linux.RawDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return nil, linux.ErrNoPhysicalPresenceInterface + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeDirect) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRaw, DeviceModeDirect, nil, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceDirectPPIError(c *C) { + expectedRaw := new(linux.RawDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return nil, errors.New("some error") + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeDirect) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRaw, DeviceModeDirect, nil, errors.New("some error"))) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceTryResourceManaged(c *C) { + expectedRaw := new(linux.RawDevice) + expectedPPI := new(mockPPI) + expectedRM := new(linux.RMDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return expectedPPI, nil + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return expectedRM, nil + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Check(rm, Equals, expectedRM) + return expectedRaw + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeTryResourceManaged) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRM, DeviceModeResourceManaged, expectedPPI, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceTryResourceManagedNoPPI(c *C) { + expectedRaw := new(linux.RawDevice) + expectedRM := new(linux.RMDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return nil, linux.ErrNoPhysicalPresenceInterface + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return expectedRM, nil + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Check(rm, Equals, expectedRM) + return expectedRaw + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeTryResourceManaged) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRM, DeviceModeResourceManaged, nil, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceTryResourceManagedPPIErr(c *C) { + expectedRaw := new(linux.RawDevice) + expectedRM := new(linux.RMDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return nil, errors.New("some error") + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return expectedRM, nil + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Check(rm, Equals, expectedRM) + return expectedRaw + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeTryResourceManaged) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRM, DeviceModeResourceManaged, nil, errors.New("some error"))) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceTryResourceManagedNoRM(c *C) { + expectedRaw := new(linux.RawDevice) + expectedPPI := new(mockPPI) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return expectedPPI, nil + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return nil, linux.ErrNoResourceManagedDevice + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Errorf("unexpected call to linux.RMDevice.RawDevice()") + return nil + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeTryResourceManaged) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRaw, DeviceModeDirect, expectedPPI, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceTryResourceManagedErr(c *C) { + expectedRaw := new(linux.RawDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return nil, linux.ErrNoPhysicalPresenceInterface + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return nil, errors.New("some error") + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Errorf("unexpected call to linux.RMDevice.RawDevice()") + return nil + }) + defer restore() + + _, err := DefaultDevice(DeviceModeTryResourceManaged) + c.Check(err, ErrorMatches, `some error`) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceResourceManaged(c *C) { + expectedRaw := new(linux.RawDevice) + expectedPPI := new(mockPPI) + expectedRM := new(linux.RMDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Check(raw, Equals, expectedRaw) + return expectedPPI, nil + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return expectedRM, nil + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Check(rm, Equals, expectedRM) + return expectedRaw + }) + defer restore() + + dev, err := DefaultDevice(DeviceModeResourceManaged) + c.Check(err, IsNil) + c.Check(dev, DeepEquals, NewTPMDevice(expectedRM, DeviceModeResourceManaged, expectedPPI, nil)) +} + +func (s *deviceLinuxSuite) TestDefaultDeviceResourceManagedNoRM(c *C) { + expectedRaw := new(linux.RawDevice) + + restore := MockLinuxDefaultTPM2Device(func() (*linux.RawDevice, error) { + return expectedRaw, nil + }) + defer restore() + + restore = MockLinuxRawDevicePhysicalPresenceInterface(func(raw *linux.RawDevice) (ppi.PPI, error) { + c.Errorf("unexpected call to linux.RawDevice.PhysicalPresenceInterface()") + return nil, nil + }) + defer restore() + + restore = MockLinuxRawDeviceResourceManagedDevice(func(raw *linux.RawDevice) (*linux.RMDevice, error) { + c.Check(raw, Equals, expectedRaw) + return nil, linux.ErrNoResourceManagedDevice + }) + defer restore() + + restore = MockLinuxRMDeviceRawDevice(func(rm *linux.RMDevice) *linux.RawDevice { + c.Errorf("unexpected call to linux.RMDevice.RawDevice()") + return nil + }) + defer restore() + + _, err := DefaultDevice(DeviceModeResourceManaged) + c.Check(err, Equals, ErrNoResourceManagedTPM2Device) +} diff --git a/internal/tpm2_device/device_test.go b/internal/tpm2_device/device_test.go new file mode 100644 index 00000000..e6ba8cc6 --- /dev/null +++ b/internal/tpm2_device/device_test.go @@ -0,0 +1,88 @@ +// -*- 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 tpm2_device_test + +import ( + "errors" + "testing" + + "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/ppi" + . "github.com/snapcore/secboot/internal/tpm2_device" + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type mockTpmDevice struct{} + +func (mockTpmDevice) Open() (tpm2.Transport, error) { + return nil, errors.New("cannot open mock transport") +} + +func (mockTpmDevice) String() string { + return "mock device" +} + +type mockPPI struct { + ppi.PPI +} + +type deviceSuite struct{} + +var _ = Suite(&deviceSuite{}) + +func (s *deviceSuite) TestTPMDeviceDevice(c *C) { + dev := NewTPMDevice(new(mockTpmDevice), DeviceModeDirect, nil, ErrNoPPI) + c.Check(dev.String(), Equals, "mock device") + _, err := dev.Open() + c.Check(err, ErrorMatches, `cannot open mock transport`) +} + +func (s *deviceSuite) TestTPMDeviceMode(c *C) { + dev := NewTPMDevice(new(mockTpmDevice), DeviceModeDirect, nil, ErrNoPPI) + c.Check(dev.Mode(), Equals, DeviceModeDirect) + + dev = NewTPMDevice(new(mockTpmDevice), DeviceModeResourceManaged, nil, ErrNoPPI) + c.Check(dev.Mode(), Equals, DeviceModeResourceManaged) +} + +func (s *deviceSuite) TestTPMDevicePPINone(c *C) { + dev := NewTPMDevice(new(mockTpmDevice), DeviceModeDirect, nil, nil) + ppi, err := dev.PPI() + c.Check(err, Equals, ErrNoPPI) + c.Check(ppi, IsNil) +} + +func (s *deviceSuite) TestTPMDevicePPIErr(c *C) { + expected := errors.New("some error") + dev := NewTPMDevice(new(mockTpmDevice), DeviceModeDirect, nil, expected) + ppi, err := dev.PPI() + c.Check(err, Equals, expected) + c.Check(ppi, IsNil) +} + +func (s *deviceSuite) TestTPMDevicePPI(c *C) { + expected := new(mockPPI) + dev := NewTPMDevice(new(mockTpmDevice), DeviceModeDirect, expected, nil) + ppi, err := dev.PPI() + c.Check(err, IsNil) + c.Check(ppi, Equals, expected) +} diff --git a/internal/tpm2_device/export_test.go b/internal/tpm2_device/export_test.go new file mode 100644 index 00000000..b6326873 --- /dev/null +++ b/internal/tpm2_device/export_test.go @@ -0,0 +1,67 @@ +// -*- 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 tpm2_device + +import ( + "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/linux" + "github.com/canonical/go-tpm2/ppi" +) + +func MockLinuxDefaultTPM2Device(fn func() (*linux.RawDevice, error)) (restore func()) { + orig := linuxDefaultTPM2Device + linuxDefaultTPM2Device = fn + return func() { + linuxDefaultTPM2Device = orig + } +} + +func MockLinuxRawDeviceResourceManagedDevice(fn func(*linux.RawDevice) (*linux.RMDevice, error)) (restore func()) { + orig := linuxRawDeviceResourceManagedDevice + linuxRawDeviceResourceManagedDevice = fn + return func() { + linuxRawDeviceResourceManagedDevice = orig + } +} + +func MockLinuxRawDevicePhysicalPresenceInterface(fn func(*linux.RawDevice) (ppi.PPI, error)) (restore func()) { + orig := linuxRawDevicePhysicalPresenceInterface + linuxRawDevicePhysicalPresenceInterface = fn + return func() { + linuxRawDevicePhysicalPresenceInterface = orig + } +} + +func MockLinuxRMDeviceRawDevice(fn func(*linux.RMDevice) *linux.RawDevice) (restore func()) { + orig := linuxRMDeviceRawDevice + linuxRMDeviceRawDevice = fn + return func() { + linuxRMDeviceRawDevice = orig + } +} + +func NewTPMDevice(dev tpm2.TPMDevice, mode DeviceMode, ppi ppi.PPI, ppiErr error) TPMDevice { + return &tpmDevice{ + TPMDevice: dev, + mode: mode, + ppi: ppi, + ppiErr: ppiErr, + } +} diff --git a/internal/tpm2test/suites.go b/internal/tpm2test/suites.go index 3e1083ea..87cc1b27 100644 --- a/internal/tpm2test/suites.go +++ b/internal/tpm2test/suites.go @@ -20,57 +20,80 @@ package tpm2test import ( - "github.com/canonical/go-tpm2" tpm2_testutil "github.com/canonical/go-tpm2/testutil" . "gopkg.in/check.v1" + "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2_device" secboot_tpm2 "github.com/snapcore/secboot/tpm2" ) type tpmTestMixin struct { - TPM *secboot_tpm2.Connection - TCTI *TCTI + TPM *secboot_tpm2.Connection + Transport *Transport } -func (m *tpmTestMixin) setUpTest(c *C, open func() (*secboot_tpm2.Connection, *TCTI)) (cleanup func(*C)) { - // Some tests execute code which calls secboot_tpm2.ConnectToTPM. - // Allow this code to get a new secboot_tpm2.Connection using the - // tests existing underlying connection, but don't allow the code - // to fully close the connection - leave this to the test fixture. - restoreOpenDefaultTcti := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - tcti := WrapTCTI(m.TCTI.Unwrap().(*tpm2_testutil.TCTI)) - tcti.SetKeepOpen(true) - return tcti, nil - }) - +func (m *tpmTestMixin) setUpTest(c *C, suite *tpm2_testutil.TPMTest, open func() (*secboot_tpm2.Connection, *Transport)) (cleanup func(*C)) { switch { case m.TPM != nil: - // Allow the test to supply a secboot_tpm2.Connection - c.Assert(m.TCTI, NotNil) - case m.TCTI != nil: - // Allow the test to supply an existing connection - tpm, tcti, err := newTPMConnectionFromExisting(nil, m.TCTI) + // Allow the test to supply a secboot_tpm2.Connection via SetConnection + c.Assert(m.Transport, NotNil) + suite.TPM = m.TPM.TPMContext // Copy the tpm2.TPMContext to the TPMTest suite + suite.Transport = m.Transport.Unwrap().(*tpm2_testutil.Transport) // Copy the tpm2_testutil.Transport to the TPMTest suite + case m.Transport != nil: + // Allow the test to supply an existing transport, and create a connection from it + tpm, _, err := newTPMConnectionFromExistingTransport(nil, m.Transport) c.Assert(err, IsNil) m.TPM = tpm - m.TCTI = tcti + suite.TPM = m.TPM.TPMContext // Copy the tpm2.TPMContext to the TPMTest suite + suite.Transport = m.Transport.Unwrap().(*tpm2_testutil.Transport) // Copy the tpm2_testutil.Transport to the TPMTest suite + case suite.Device != nil: + // Allow the test to supply a device, and create a connection from it + tpm, transport := OpenTPMDevice(c, suite.Device, nil, nil) + m.TPM = tpm + m.Transport = transport + suite.TPM = m.TPM.TPMContext // Copy the tpm2.TPMContext to the TPMTest suite + suite.Transport = m.Transport.Unwrap().(*tpm2_testutil.Transport) // Copy the tpm2_testutil.Transport to the TPMTest suite default: - m.TPM, m.TCTI = open() + // The default case - open a default connection + tpm, transport := open() + m.TPM = tpm + m.Transport = transport + suite.TPM = m.TPM.TPMContext // Copy the tpm2.TPMContext to the TPMTest suite + suite.Transport = m.Transport.Unwrap().(*tpm2_testutil.Transport) // Copy the tpm2_testutil.Transport to the TPMTest suite } + // Some tests execute code which calls secboot_tpm2.ConnectToDefaultTPM. + // Allow this code to get a new secboot_tpm2.Connection using the + // tests existing underlying connection, but don't allow the code + // to fully close the connection - leave this to the test fixture. + // TODO: Support resource managed device concepts in tests. + // XXX: Set the maximum numbner of open connections to 1 when + // https://github.com/canonical/secboot/issues/353 is fixed. + internalDev := newTpmDevice(tpm2_testutil.NewTransportBackedDevice(suite.Transport, false, -1), tpm2_device.DeviceModeDirect, nil, tpm2_device.ErrNoPPI) + restoreDefaultDeviceFn := MockDefaultDeviceFn(func(mode tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + c.Assert(mode, Equals, tpm2_device.DeviceModeDirect) + return internalDev, nil + }) + return func(c *C) { - restoreOpenDefaultTcti() + restoreDefaultDeviceFn() + c.Check(internalDev.TPMDevice.(*tpm2_testutil.TransportBackedDevice).NumberOpen(), Equals, 0) c.Check(m.TPM.Close(), IsNil) - m.TCTI = nil m.TPM = nil + m.Transport = nil } } -func (m *tpmTestMixin) reinitTPMConnectionFromExisting(c *C) { - tpm, tcti, err := newTPMConnectionFromExisting(m.TPM, m.TCTI) +func (m *tpmTestMixin) reinitTPMConnectionFromExisting(c *C, suite *tpm2_testutil.TPMTest) { + tpm, transport, err := newTPMConnectionFromExistingTransport(m.TPM, m.Transport) c.Assert(err, IsNil) m.TPM = tpm - m.TCTI = tcti + m.Transport = transport + suite.TPM = m.TPM.TPMContext // Copy the tpm2.TPMContext to the TPMTest suite + suite.Transport = m.Transport.Unwrap().(*tpm2_testutil.Transport) // Copy the tpm2_testutil.Transport to the TPMTest suite + suite.TCTI = suite.Transport // Fill the legacy member (this normally happens in tpm2_testutil.TPMTest.SetUpTest) } // TPMTest is a base test suite for all tests that require a TPM and are able to @@ -83,60 +106,66 @@ type TPMTest struct { } // SetUpTest is called to set up the test fixture before each test. If -// SetConnection has not been called before this is called, a TPM connection -// will be created automatically. In this case, the TPMFeatures member should -// be set prior to calling SetUpTest in order to declare the features that -// the test will require. If the test requires any features that are not included -// in tpm2_testutil.PermittedTPMFeatures, the test will be skipped. If -// tpm2_testutil.TPMBackend is TPMBackendNone, then the test will be skipped. +// SetConnection has not been called before this is called, or the Transport or +// Device members have not been set, a TPM connection will be created automatically. +// In this case, the TPMFeatures member should be set prior to calling SetUpTest in +// order to declare the features that the test will require. If the test requires +// any features that are not included in tpm2_testutil.PermittedTPMFeatures, the +// test will be skipped. If tpm2_testutil.TPMBackend is TPMBackendNone, then the +// test will be skipped. +// +// If SetConnection is called prior to calling SetUpTest, the supplied TPM connection +// will be used for the test. +// +// If the Transport member is set prior to calling SetUpTest, a TPMContext is created +// using this connection if necessary. // -// If SetConnection has been called with a test provided TCTI, then a connection -// will be created from this. +// If the Device member is set prior to calling SetUpTest, a TPM connection and +// TPMContext is created using this. // // The TPM connection closed automatically when TearDownTest is called. func (b *TPMTest) SetUpTest(c *C) { // Don't support setting these directly as it makes things // complicated. c.Assert(b.TPMTest.TPM, IsNil) + c.Assert(b.TPMTest.Transport, IsNil) c.Assert(b.TPMTest.TCTI, IsNil) - cleanup := b.setUpTest(c, func() (*secboot_tpm2.Connection, *TCTI) { - return OpenTPMConnection(c, b.TPMFeatures) + cleanup := b.setUpTest(c, &b.TPMTest, func() (*secboot_tpm2.Connection, *Transport) { + return OpenDefaultTPMConnection(c, b.TPMFeatures) }) - b.TPMTest.TPM = b.tpmTestMixin.TPM.TPMContext - b.TPMTest.TCTI = b.tpmTestMixin.TCTI.Unwrap().(*tpm2_testutil.TCTI) - b.TPMTest.SetUpTest(c) b.AddFixtureCleanup(func(c *C) { cleanup(c) b.TPMTest.TPM = nil b.TPMTest.TCTI = nil + b.TPMTest.Transport = nil + b.TPMTest.Device = nil }) } // SetConnection can be called prior to SetUpTest in order to supply a // TPM connection rather than having the fixture create one. -func (b *TPMTest) SetConnection(tpm *secboot_tpm2.Connection, tcti *TCTI) { +func (b *TPMTest) SetConnection(c *C, tpm *secboot_tpm2.Connection) { b.tpmTestMixin.TPM = tpm - b.tpmTestMixin.TCTI = tcti + c.Assert(tpm.Transport(), testutil.ConvertibleTo, &Transport{}) + b.tpmTestMixin.Transport = tpm.Transport().(*Transport) } func (b *TPMTest) TPM() *secboot_tpm2.Connection { return b.tpmTestMixin.TPM } -func (b *TPMTest) TCTI() *TCTI { - return b.tpmTestMixin.TCTI +func (b *TPMTest) Transport() *Transport { + return b.tpmTestMixin.Transport } // ReinitTPMConnectionFromExisting recreates a new connection and TCTI // from the existing ones. This is useful in scenarios where the fixture // setup and test code should use a different connection. func (b *TPMTest) ReinitTPMConnectionFromExisting(c *C) { - b.reinitTPMConnectionFromExisting(c) - b.TPMTest.TPM = b.tpmTestMixin.TPM.TPMContext - b.TPMTest.TCTI = b.tpmTestMixin.TCTI.Unwrap().(*tpm2_testutil.TCTI) + b.reinitTPMConnectionFromExisting(c, &b.TPMTest) } // TPMSimulatorTest is a base test suite for all tests that require a TPM simulator. @@ -148,12 +177,19 @@ type TPMSimulatorTest struct { } // SetUpTest is called to set up the test fixture before each test. If -// SetConnection has not been called before this is called, a TPM simulator -// connection will be created automatically. If tpm2_testutil.TPMBackend is -// not TPMBackendMssim, then the test will be skipped. +// SetConnection has not been called before this is called, or the Transport or +// Device members have not been set, a TPM simulator connection will be created +// automatically. If tpm2_testutil.TPMBackend is not TPMBackendMssim, then the test +// will be skipped. +// +// If SetConnection is called prior to calling SetUpTest, the supplied TPM connection +// will be used for the test. // -// If SetConnection has been called with a test provided TCTI, then a connection -// will be created from this. +// If the Transport member is set prior to calling SetUpTest, a TPMContext is created +// using this connection if necessary. +// +// If the Device member is set prior to calling SetUpTest, a TPM connection and +// TPMContext is created using this. // // When TearDownTest is called, the TPM simulator is reset and cleared // and the connection is closed. @@ -161,46 +197,45 @@ func (b *TPMSimulatorTest) SetUpTest(c *C) { // Don't support setting these directly as it makes things // complicated. c.Assert(b.TPMTest.TPM, IsNil) + c.Assert(b.TPMTest.Transport, IsNil) c.Assert(b.TPMTest.TCTI, IsNil) - cleanup := b.setUpTest(c, func() (*secboot_tpm2.Connection, *TCTI) { - return OpenTPMSimulatorConnection(c) + cleanup := b.setUpTest(c, &b.TPMTest, func() (*secboot_tpm2.Connection, *Transport) { + return OpenDefaultTPMSimulatorConnection(c) }) - b.TPMTest.TPM = b.tpmTestMixin.TPM.TPMContext - b.TPMTest.TCTI = b.tpmTestMixin.TCTI.Unwrap().(*tpm2_testutil.TCTI) - b.TPMSimulatorTest.SetUpTest(c) b.AddFixtureCleanup(func(c *C) { b.ResetAndClearTPMSimulatorUsingPlatformHierarchy(c) cleanup(c) b.TPMTest.TPM = nil b.TPMTest.TCTI = nil + b.TPMTest.Transport = nil + b.TPMTest.Device = nil }) } // SetConnection can be called prior to SetUpTest in order to supply a // TPM connection rather than having the fixture create one. -func (b *TPMSimulatorTest) SetConnection(tpm *secboot_tpm2.Connection, tcti *TCTI) { +func (b *TPMSimulatorTest) SetConnection(c *C, tpm *secboot_tpm2.Connection) { b.tpmTestMixin.TPM = tpm - b.tpmTestMixin.TCTI = tcti + c.Assert(tpm.Transport(), testutil.ConvertibleTo, &Transport{}) + b.tpmTestMixin.Transport = tpm.Transport().(*Transport) } func (b *TPMSimulatorTest) TPM() *secboot_tpm2.Connection { return b.tpmTestMixin.TPM } -func (b *TPMSimulatorTest) TCTI() *TCTI { - return b.tpmTestMixin.TCTI +func (b *TPMSimulatorTest) Transport() *Transport { + return b.tpmTestMixin.Transport } // ReinitTPMConnectionFromExisting recreates a new connection and TCTI // from the existing ones. This is useful in scenarios where the fixture // setup and test code should use a different connection. func (b *TPMSimulatorTest) ReinitTPMConnectionFromExisting(c *C) { - b.reinitTPMConnectionFromExisting(c) - b.TPMTest.TPM = b.tpmTestMixin.TPM.TPMContext - b.TPMTest.TCTI = b.tpmTestMixin.TCTI.Unwrap().(*tpm2_testutil.TCTI) + b.reinitTPMConnectionFromExisting(c, &b.TPMTest) } // ResetTPMSimulator issues a Shutdown -> Reset -> Startup cycle of the TPM simulator and diff --git a/internal/tpm2test/tpm.go b/internal/tpm2test/tpm.go index da0bf42a..6dc77ad9 100644 --- a/internal/tpm2test/tpm.go +++ b/internal/tpm2test/tpm.go @@ -20,15 +20,18 @@ package tpm2test import ( + "errors" "testing" "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/ppi" tpm2_testutil "github.com/canonical/go-tpm2/testutil" . "gopkg.in/check.v1" "github.com/snapcore/secboot/internal/tcg" - "github.com/snapcore/secboot/internal/tcti" + "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2_device" secboot_tpm2 "github.com/snapcore/secboot/tpm2" ) @@ -81,13 +84,13 @@ const ( //TPMFeaturePersistent = tpm2_testutil.TPMFeaturePersistent ) -// MockOpenDefaultTctiFn overrides the tcti.OpenDefault function, used -// to create a connection to the default TPM. -func MockOpenDefaultTctiFn(fn func() (tpm2.TCTI, error)) (restore func()) { - origFn := tcti.OpenDefault - tcti.OpenDefault = fn +// MockDefaultDeviceFn overrides the tpm2_device.DefaultDevice function, used +// to obtain the default TPM device. +func MockDefaultDeviceFn(fn func(tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error)) (restore func()) { + orig := tpm2_device.DefaultDevice + tpm2_device.DefaultDevice = fn return func() { - tcti.OpenDefault = origFn + tpm2_device.DefaultDevice = orig } } @@ -101,53 +104,126 @@ func MockEKTemplate(mock *tpm2.Public) (restore func()) { } } -// OpenTPMSimulatorConnection returns a new TPM connection to the TPM simulator on -// the port specified by tpm2_testutil.MssimPort. If tpm2_testutil.TPMBackend is -// not TPMBackendMssim then the test will be skipped. -// -// The returned connection must be closed when it is no longer required. -func OpenTPMSimulatorConnection(c *C) (tpm *secboot_tpm2.Connection, tcti *TCTI) { - tcti = WrapTCTI(tpm2_testutil.NewSimulatorTCTI(c)) +type testTpmDevice struct { + tpm2.TPMDevice +} - restore := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return tcti, nil - }) - defer restore() +func newTestTpmDevice(dev tpm2.TPMDevice) *testTpmDevice { + return &testTpmDevice{TPMDevice: dev} +} - tpm, err := secboot_tpm2.ConnectToTPM() - c.Assert(err, IsNil) +func (d *testTpmDevice) Open() (tpm2.Transport, error) { + transport, err := d.TPMDevice.Open() + if err != nil { + return nil, err + } + testTransport, ok := transport.(*tpm2_testutil.Transport) + if !ok { + return nil, errors.New("expected a tpm2_testutil.Transport") + } + return WrapTransport(testTransport), nil +} - return tpm, tcti +type tpmDevice struct { + tpm2.TPMDevice + mode tpm2_device.DeviceMode + ppi ppi.PPI + ppiErr error } -// OpenTPMSimulatorConnectionT returns a new TPM connection to the TPM simulator -// on the port specified by tpm2_testutil.MssimPort. If tpm2_testutil.TPMBackend is -// not TPMBackendMssim then the test will be skipped. -// -// The returned connection must be closed when it is no longer required. This can -// be done with the returned close callback, which will cause the test to fail if -// closing doesn't succeed. -func OpenTPMSimulatorConnectionT(t *testing.T) (tpm *secboot_tpm2.Connection, tcti *TCTI, close func()) { - tcti = WrapTCTI(tpm2_testutil.NewSimulatorTCTIT(t)) +func newTpmDevice(dev tpm2.TPMDevice, mode tpm2_device.DeviceMode, ppi ppi.PPI, ppiErr error) *tpmDevice { + return &tpmDevice{ + TPMDevice: dev, + mode: mode, + ppi: ppi, + ppiErr: ppiErr, + } +} + +func (d *tpmDevice) Mode() tpm2_device.DeviceMode { + return d.mode +} + +func (d *tpmDevice) PPI() (ppi.PPI, error) { + return d.ppi, d.ppiErr +} - restore := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return tcti, nil +func openTPMDevice(dev tpm2.TPMDevice, ppi ppi.PPI, ppiErr error) (tpm *secboot_tpm2.Connection, err error) { + restore := MockDefaultDeviceFn(func(mode tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + if mode != tpm2_device.DeviceModeDirect { + // TODO: Support other modes here + return nil, errors.New("unexpected mode") + } + if ppi == nil && ppiErr == nil { + ppiErr = tpm2_device.ErrNoPPI + } + return newTpmDevice(newTestTpmDevice(dev), mode, ppi, ppiErr), nil }) defer restore() - tpm, err := secboot_tpm2.ConnectToTPM() + tpm, err = secboot_tpm2.ConnectToDefaultTPM() + if err != nil { + return nil, err + } + + return tpm, nil +} + +func OpenTPMDevice(c *C, dev tpm2.TPMDevice, ppi ppi.PPI, ppiErr error) (tpm *secboot_tpm2.Connection, transport *Transport) { + tpm, err := openTPMDevice(dev, ppi, ppiErr) + if errors.Is(err, tpm2_testutil.ErrSkipNoTPM) { + c.Skip("no TPM available for the test") + } + c.Assert(err, IsNil) + c.Assert(tpm.Transport(), testutil.ConvertibleTo, &Transport{}) + + return tpm, tpm.Transport().(*Transport) +} + +func OpenTPMDeviceT(t *testing.T, dev tpm2.TPMDevice, ppi ppi.PPI, ppiErr error) (tpm *secboot_tpm2.Connection, transport *Transport, close func()) { + tpm, err := openTPMDevice(dev, ppi, ppiErr) + if errors.Is(err, tpm2_testutil.ErrSkipNoTPM) { + t.SkipNow() + } if err != nil { t.Fatalf("%v", err) } - return tpm, tcti, func() { + transport, ok := tpm.Transport().(*Transport) + if !ok { + t.Fatal("unexpected transport type") + } + + return tpm, transport, func() { if err := tpm.Close(); err != nil { t.Errorf("close failed: %v", err) } } } -// OpenTPMConnection returns a new TPM connection for testing. If tpm2_testutil.TPMBackend +// OpenDefaultTPMSimulatorConnection returns a new TPM connection to the TPM simulator on +// the port specified by tpm2_testutil.MssimPort. If tpm2_testutil.TPMBackend is +// not TPMBackendMssim then the test will be skipped. +// +// The returned connection must be closed when it is no longer required. +func OpenDefaultTPMSimulatorConnection(c *C) (tpm *secboot_tpm2.Connection, transport *Transport) { + // TODO: Support supplying a ppi.PPI implementation that can be tested. + return OpenTPMDevice(c, tpm2_testutil.NewSimulatorDevice(), nil, nil) +} + +// OpenDefaultTPMSimulatorConnectionT returns a new TPM connection to the TPM simulator +// on the port specified by tpm2_testutil.MssimPort. If tpm2_testutil.TPMBackend is +// not TPMBackendMssim then the test will be skipped. +// +// The returned connection must be closed when it is no longer required. This can +// be done with the returned close callback, which will cause the test to fail if +// closing doesn't succeed. +func OpenDefaultTPMSimulatorConnectionT(t *testing.T) (tpm *secboot_tpm2.Connection, transport *Transport, close func()) { + // TODO: Support supplying a ppi.PPI implementation that can be tested. + return OpenTPMDeviceT(t, tpm2_testutil.NewSimulatorDevice(), nil, nil) +} + +// OpenDefaultTPMConnection returns a new TPM connection for testing. If tpm2_testutil.TPMBackend // is TPMBackendNone then the current test will be skipped. If tpm2_testutil.TPMBackend // is TPMBackendMssim, the returned context will correspond to a connection to the TPM // simulator on the port specified by the tpm2_testutil.MssimPort variable. If @@ -158,21 +234,12 @@ func OpenTPMSimulatorConnectionT(t *testing.T) (tpm *secboot_tpm2.Connection, tc // If the test requires features that are not permitted, the test will be skipped. // // The returned connection must be closed when it is no longer required. -func OpenTPMConnection(c *C, features tpm2_testutil.TPMFeatureFlags) (tpm *secboot_tpm2.Connection, tcti *TCTI) { - tcti = WrapTCTI(tpm2_testutil.NewTCTI(c, features)) - - restore := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return tcti, nil - }) - defer restore() - - tpm, err := secboot_tpm2.ConnectToTPM() - c.Assert(err, IsNil) - - return tpm, tcti +func OpenDefaultTPMConnection(c *C, features tpm2_testutil.TPMFeatureFlags) (tpm *secboot_tpm2.Connection, transport *Transport) { + // TODO: Support supplying a ppi.PPI implementation that can be tested. + return OpenTPMDevice(c, tpm2_testutil.NewDevice(c, features), nil, nil) } -// OpenTPMConnectionT returns a new TPM connection for testing. If tpm2_testutil.TPMBackend +// OpenDefaultTPMConnectionT returns a new TPM connection for testing. If tpm2_testutil.TPMBackend // is TPMBackendNone then the current test will be skipped. If tpm2_testutil.TPMBackend // is TPMBackendMssim, the returned context will correspond to a connection to the TPM // simulator on the port specified by the tpm2_testutil.MssimPort variable. If @@ -185,51 +252,45 @@ func OpenTPMConnection(c *C, features tpm2_testutil.TPMFeatureFlags) (tpm *secbo // The returned connection must be closed when it is no longer required. This can be // done with the returned close callback, which will cause the test to fail if closing // doesn't succeed. -func OpenTPMConnectionT(t *testing.T, features tpm2_testutil.TPMFeatureFlags) (tpm *secboot_tpm2.Connection, tcti *TCTI, close func()) { - tcti = WrapTCTI(tpm2_testutil.NewTCTIT(t, features)) - - restore := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return tcti, nil - }) - defer restore() - - tpm, err := secboot_tpm2.ConnectToTPM() - if err != nil { - t.Fatalf("%v", err) - } - - return tpm, tcti, func() { - if err := tpm.Close(); err != nil { - t.Errorf("close failed: %v", err) - } - } +func OpenDefaultTPMConnectionT(t *testing.T, features tpm2_testutil.TPMFeatureFlags) (tpm *secboot_tpm2.Connection, transport *Transport, close func()) { + // TODO: Support supplying a ppi.PPI implementation that can be tested. + return OpenTPMDeviceT(t, tpm2_testutil.NewDeviceT(t, features), nil, nil) } -func newTPMConnectionFromExisting(tpm *secboot_tpm2.Connection, tcti *TCTI) (*secboot_tpm2.Connection, *TCTI, error) { +func newTPMConnectionFromExistingTransport(tpm *secboot_tpm2.Connection, transport *Transport) (*secboot_tpm2.Connection, *Transport, error) { + // Wrap the supplied transport in a TPMDevice. + dev := tpm2_testutil.NewTransportPassthroughDevice(transport.Unwrap()) + if tpm != nil { + // A TPM Connection was supplied. Pretend to close the existing connection, + // which flushes the HMAC session associated with it. This will close the + // test transport, but we keep the underlying transport open. + if transport != tpm.Transport() { + return nil, nil, errors.New("invalid transport") + } // Pretend to close the existing connection, which flushes // the HMAC session associated with it. - tcti.SetKeepOpen(true) + transport.SetKeepOpen(true) if err := tpm.Close(); err != nil { return nil, nil, err } - } - // Create a new tcti, using the same underlyinhg connection. - tcti = WrapTCTI(tcti.Unwrap().(*tpm2_testutil.TCTI)) + // Create another device based on the same underlying transport. + dev = tpm2_testutil.NewTransportPassthroughDevice(transport.Unwrap()) + } - restore := MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return tcti, nil + restore := MockDefaultDeviceFn(func(mode tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + return newTpmDevice(newTestTpmDevice(dev), mode, nil, tpm2_device.ErrNoPPI), nil }) defer restore() - // Create a new connection. - tpm, err := secboot_tpm2.ConnectToTPM() + // Create a new connection using the existing transport. + tpm, err := secboot_tpm2.ConnectToDefaultTPM() if err != nil { - tcti.Close() + transport.Close() } - return tpm, tcti, err + return tpm, tpm.Transport().(*Transport), err } // NewTPMConnectionFromExistingT creates a new connection and TCTI from the @@ -237,13 +298,13 @@ func newTPMConnectionFromExisting(tpm *secboot_tpm2.Connection, tcti *TCTI) (*se // test execution require a different connection. The returned connection // uses the same underlying connection as the one supplied. The supplied // source connection does not need to be closed afterwards. -func NewTPMConnectionFromExistingT(t *testing.T, tpm *secboot_tpm2.Connection, tcti *TCTI) (newTpm *secboot_tpm2.Connection, newTcti *TCTI, close func()) { - tpm, tcti, err := newTPMConnectionFromExisting(tpm, tcti) +func NewTPMConnectionFromExistingTransportT(t *testing.T, tpm *secboot_tpm2.Connection, transport *Transport) (newTpm *secboot_tpm2.Connection, newTransport *Transport, close func()) { + newTpm, newTransport, err := newTPMConnectionFromExistingTransport(tpm, transport) if err != nil { t.Fatal(err) } - return tpm, tcti, func() { - if err := tpm.Close(); err != nil { + return newTpm, newTransport, func() { + if err := newTpm.Close(); err != nil { t.Errorf("close failed: %v", err) } } @@ -252,7 +313,7 @@ func NewTPMConnectionFromExistingT(t *testing.T, tpm *secboot_tpm2.Connection, t // ResetTPMSimulatorT issues a Shutdown -> Reset -> Startup cycle of the TPM // simulator and returns a newly initialized TPM connection. The supplied // source connection does not need to be closed afterwards. -func ResetTPMSimulatorT(t *testing.T, tpm *secboot_tpm2.Connection, tcti *TCTI) (newTpm *secboot_tpm2.Connection, newTcti *TCTI, close func()) { - tpm2_testutil.ResetTPMSimulatorT(t, tpm.TPMContext, tcti.Unwrap().(*tpm2_testutil.TCTI)) - return NewTPMConnectionFromExistingT(t, tpm, tcti) +func ResetTPMSimulatorT(t *testing.T, tpm *secboot_tpm2.Connection, transport *Transport) (newTpm *secboot_tpm2.Connection, newTransport *Transport, close func()) { + tpm2_testutil.ResetTPMSimulatorT(t, tpm.TPMContext, transport.Unwrap().(*tpm2_testutil.Transport)) + return NewTPMConnectionFromExistingTransportT(t, tpm, transport) } diff --git a/internal/tpm2test/tcti.go b/internal/tpm2test/transport.go similarity index 69% rename from internal/tpm2test/tcti.go rename to internal/tpm2test/transport.go index 244a3034..d15afa85 100644 --- a/internal/tpm2test/tcti.go +++ b/internal/tpm2test/transport.go @@ -20,48 +20,46 @@ package tpm2test import ( - "os" - "github.com/canonical/go-tpm2" tpm2_testutil "github.com/canonical/go-tpm2/testutil" ) -// TCTI is a wrapper around tpm2_testutil.TCTI that provides a mechanism +// Transport is a wrapper around tpm2_testutil.Transport that provides a mechanism // to keep the underlying connection open when Close is called. -type TCTI struct { - tcti *tpm2_testutil.TCTI +type Transport struct { + transport *tpm2_testutil.Transport keepOpen bool markedClosed bool closed bool } -func (t *TCTI) Read(data []byte) (int, error) { +func (t *Transport) Read(data []byte) (int, error) { if t.markedClosed { - return 0, os.ErrClosed + return 0, tpm2.ErrTransportClosed } - return t.tcti.Read(data) + return t.transport.Read(data) } -func (t *TCTI) Write(data []byte) (int, error) { +func (t *Transport) Write(data []byte) (int, error) { if t.markedClosed { - return 0, os.ErrClosed + return 0, tpm2.ErrTransportClosed } - return t.tcti.Write(data) + return t.transport.Write(data) } // Close closes the underlying connection unless SetKeepOpen has // been called with keepOpen set to true, in which case, the // interface is marked as closed without actually closing it. -func (t *TCTI) Close() error { +func (t *Transport) Close() error { if t.markedClosed { - return os.ErrClosed + return tpm2.ErrTransportClosed } t.markedClosed = true if t.keepOpen { return nil } t.closed = true - return t.tcti.Close() + return t.transport.Close() } // SetKeepOpen provides a mechanism to keep the underlying connection @@ -69,19 +67,19 @@ func (t *TCTI) Close() error { // mark the connection as closed without actually closing it. This // makes it possible to reuse the underlying connection in another // secboot_tpm2.Connection. -func (t *TCTI) SetKeepOpen(keepOpen bool) error { +func (t *Transport) SetKeepOpen(keepOpen bool) error { t.keepOpen = keepOpen if !t.keepOpen && t.markedClosed && !t.closed { t.closed = true - return t.tcti.Close() + return t.transport.Close() } return nil } -func (t *TCTI) Unwrap() tpm2.TCTI { - return t.tcti +func (t *Transport) Unwrap() tpm2.Transport { + return t.transport } -func WrapTCTI(tcti *tpm2_testutil.TCTI) *TCTI { - return &TCTI{tcti: tcti} +func WrapTransport(transport *tpm2_testutil.Transport) *Transport { + return &Transport{transport: transport} } diff --git a/tpm2/errors.go b/tpm2/errors.go index e2aecbbe..b758ac0f 100644 --- a/tpm2/errors.go +++ b/tpm2/errors.go @@ -24,6 +24,7 @@ import ( "fmt" "github.com/canonical/go-tpm2" + "github.com/snapcore/secboot/internal/tpm2_device" "golang.org/x/xerrors" ) @@ -48,8 +49,8 @@ var ( // the TPM (eg, a recovery key) ErrTPMLockout = errors.New("the TPM is in DA lockout mode") - // ErrNoTPM2Device is returned from ConnectToDefaultTPM or SecureConnectToDefaultTPM if no TPM2 device is avaiable. - ErrNoTPM2Device = errors.New("no TPM2 device is available") + // ErrNoTPM2Device is returned from ConnectToDefaultTPM if no TPM2 device is avaiable. + ErrNoTPM2Device = tpm2_device.ErrNoTPM2Device ) // TPMResourceExistsError is returned from any function that creates a persistent TPM resource if a resource already exists diff --git a/tpm2/platform.go b/tpm2/platform.go index 3f324222..b504699e 100644 --- a/tpm2/platform.go +++ b/tpm2/platform.go @@ -67,7 +67,7 @@ func (h *platformKeyDataHandler) recoverKeysCommon(data *secboot.PlatformKeyData Err: fmt.Errorf("invalid key data version: %d", k.data.Version())} } - tpm, err := ConnectToTPM() + tpm, err := ConnectToDefaultTPM() switch { case err == ErrNoTPM2Device: return nil, &secboot.PlatformHandlerError{ @@ -121,7 +121,7 @@ func (h *platformKeyDataHandler) RecoverKeysWithAuthKey(data *secboot.PlatformKe } func (h *platformKeyDataHandler) ChangeAuthKey(data *secboot.PlatformKeyData, old, new []byte) ([]byte, error) { - tpm, err := ConnectToTPM() + tpm, err := ConnectToDefaultTPM() switch { case err == ErrNoTPM2Device: return nil, &secboot.PlatformHandlerError{ diff --git a/tpm2/platform_legacy.go b/tpm2/platform_legacy.go index 2f54bd64..55e881f0 100644 --- a/tpm2/platform_legacy.go +++ b/tpm2/platform_legacy.go @@ -53,7 +53,7 @@ const legacyPlatformName = "tpm2-legacy" type legacyPlatformKeyDataHandler struct{} func (h *legacyPlatformKeyDataHandler) RecoverKeys(data *secboot.PlatformKeyData, encryptedPayload []byte) ([]byte, error) { - tpm, err := ConnectToTPM() + tpm, err := ConnectToDefaultTPM() switch { case err == ErrNoTPM2Device: return nil, &secboot.PlatformHandlerError{ diff --git a/tpm2/platform_legacy_test.go b/tpm2/platform_legacy_test.go index c39557e4..dd89ecf1 100644 --- a/tpm2/platform_legacy_test.go +++ b/tpm2/platform_legacy_test.go @@ -21,9 +21,7 @@ package tpm2_test import ( "math/rand" - "os" "path/filepath" - "syscall" "github.com/canonical/go-tpm2" @@ -31,6 +29,7 @@ import ( "github.com/snapcore/secboot" "github.com/snapcore/secboot/internal/tcg" + "github.com/snapcore/secboot/internal/tpm2_device" "github.com/snapcore/secboot/internal/tpm2test" . "github.com/snapcore/secboot/tpm2" ) @@ -84,8 +83,8 @@ func (s *platformLegacySuite) TestRecoverKeysNoTPMConnection(c *C) { PCRPolicyCounterHandle: tpm2.HandleNull}) c.Check(err, IsNil) - restore := tpm2test.MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return nil, &os.PathError{Op: "open", Path: "/dev/tpm0", Err: syscall.ENOENT} + restore := tpm2test.MockDefaultDeviceFn(func(tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + return nil, tpm2_device.ErrNoTPM2Device }) s.AddCleanup(restore) diff --git a/tpm2/platform_test.go b/tpm2/platform_test.go index 7ecfd865..84ddd66c 100644 --- a/tpm2/platform_test.go +++ b/tpm2/platform_test.go @@ -25,8 +25,6 @@ import ( gohash "hash" "io" "math/rand" - "os" - "syscall" "github.com/canonical/go-tpm2" "github.com/canonical/go-tpm2/util" @@ -39,6 +37,7 @@ import ( "github.com/snapcore/secboot" "github.com/snapcore/secboot/internal/tcg" "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2_device" "github.com/snapcore/secboot/internal/tpm2test" . "github.com/snapcore/secboot/tpm2" ) @@ -376,8 +375,8 @@ func (s *platformSuite) TestRecoverKeysNoTPMConnection(c *C) { }) c.Check(err, IsNil) - restore := tpm2test.MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return nil, &os.PathError{Op: "open", Path: "/dev/tpm0", Err: syscall.ENOENT} + restore := tpm2test.MockDefaultDeviceFn(func(tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + return nil, tpm2_device.ErrNoTPM2Device }) s.AddCleanup(restore) diff --git a/tpm2/tpm.go b/tpm2/tpm.go index a2238935..c1ce6df6 100644 --- a/tpm2/tpm.go +++ b/tpm2/tpm.go @@ -27,7 +27,7 @@ import ( "golang.org/x/xerrors" "github.com/snapcore/secboot/internal/tcg" - "github.com/snapcore/secboot/internal/tcti" + "github.com/snapcore/secboot/internal/tpm2_device" ) // Connection corresponds to a connection to a TPM device, and is a wrapper around *tpm2.TPMContext. @@ -132,15 +132,15 @@ func (t *Connection) init() (err error) { // connectToDefaultTPM opens a connection to the default TPM device. func connectToDefaultTPM() (*tpm2.TPMContext, error) { - tcti, err := tcti.OpenDefault() + dev, err := tpm2_device.DefaultDevice(tpm2_device.DeviceModeDirect) if err != nil { - if isPathError(err) { - return nil, ErrNoTPM2Device - } - return nil, xerrors.Errorf("cannot open TPM device: %w", err) + return nil, err } - tpm := tpm2.NewTPMContext(tcti) + tpm, err := tpm2.OpenTPMDevice(dev) + if err != nil { + return nil, err + } if !tpm.IsTPM2() { tpm.Close() return nil, ErrNoTPM2Device @@ -149,10 +149,7 @@ func connectToDefaultTPM() (*tpm2.TPMContext, error) { return tpm, nil } -// ConnectToDefaultTPM will attempt to connect to the default TPM. It makes no attempt to verify the authenticity of the TPM. This -// function is useful for connecting to a device that isn't correctly provisioned and for which the endorsement hierarchy -// authorization value is unknown (so that it can be cleared), or for connecting to a device in order to execute -// FetchAndSaveEKCertificateChain. It should not be used in any other scenario. +// ConnectToDefaultTPM will attempt to connect to the default TPM2 device. // // If no TPM2 device is available, then a ErrNoTPM2Device error will be returned. func ConnectToDefaultTPM() (*Connection, error) { @@ -178,10 +175,3 @@ func ConnectToDefaultTPM() (*Connection, error) { succeeded = true return t, nil } - -// ConnectToTPM will attempt to connect to a TPM using the currently -// defined connection function. This is used internally by the tpm2 -// package when a connection is required, and defaults to -// ConnectToDefaultTPM. This can be overridden with a custom connection -// function. -var ConnectToTPM func() (*Connection, error) = ConnectToDefaultTPM diff --git a/tpm2/tpm_test.go b/tpm2/tpm_test.go index 76389f12..41ac8816 100644 --- a/tpm2/tpm_test.go +++ b/tpm2/tpm_test.go @@ -20,14 +20,9 @@ package tpm2_test import ( - "bytes" "crypto/x509" - "io" - "os" - "syscall" "github.com/canonical/go-tpm2" - "github.com/canonical/go-tpm2/mu" "github.com/canonical/go-tpm2/templates" tpm2_testutil "github.com/canonical/go-tpm2/testutil" @@ -35,6 +30,7 @@ import ( "github.com/snapcore/secboot/internal/tcg" "github.com/snapcore/secboot/internal/testutil" + "github.com/snapcore/secboot/internal/tpm2_device" "github.com/snapcore/secboot/internal/tpm2test" . "github.com/snapcore/secboot/tpm2" ) @@ -154,53 +150,8 @@ func (s *tpmSuite) TestConnectToDefaultTPMInvalidEK(c *C) { } func (s *tpmSuiteNoTPM) TestConnectToDefaultTPMNoTPM(c *C) { - restore := tpm2test.MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return nil, &os.PathError{Op: "open", Path: "/dev/tpm0", Err: syscall.ENOENT} - }) - s.AddCleanup(restore) - - tpm, err := ConnectToDefaultTPM() - c.Check(err, Equals, ErrNoTPM2Device) - c.Check(tpm, IsNil) -} - -// We don't have a TPM1.2 simulator, so create a mock TCTI that just returns -// a TPM_BAD_ORDINAL error -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 *mockTPM12Transport) Write(data []byte) (int, error) { - buf := new(bytes.Buffer) - // tag = TPM_TAG_RSP_COMMAND (0xc4) - // paramSize = 10 - // returnCode = TPM_BAD_ORDINAL (10) - mu.MustMarshalToWriter(buf, tpm2.TagRspCommand, uint32(10), tpm2.ResponseBadTag) - t.rsp = buf - return len(data), nil -} - -func (t *mockTPM12Transport) Close() error { - return nil -} - -func (s *tpmSuiteNoTPM) TestConnectToDefaultTPM12(c *C) { - restore := tpm2test.MockOpenDefaultTctiFn(func() (tpm2.TCTI, error) { - return &mockTPM12Transport{}, nil + restore := tpm2test.MockDefaultDeviceFn(func(tpm2_device.DeviceMode) (tpm2_device.TPMDevice, error) { + return nil, tpm2_device.ErrNoTPM2Device }) s.AddCleanup(restore)