Skip to content

Commit

Permalink
Create individual packages for Windows and Linux TPM transport (#369)
Browse files Browse the repository at this point in the history
* Create individual packages for Windows and Linux TPM transport

#364 called to attention some
long-standing technical debt around TPM transport. In particular, the
stack looks like:

(Linux or Windows) `OpenTPM` function
calls the legacy `OpenTPM` function
calls the tpmutil `OpenTPM` function

At the bottom of the stack, tpmutil does some runtime introspection to
see what type of TPM it wants to open (e.g., on Linux, the device could
be either a device file or a socket). This runtime support is
convenient, but also breaks dead-code elimination (for example, tinygo
will fail to compile the UDS support code, and users have no way of
leaving that out without patches).

In principle, we've found within Google that "open my TPM" should be as
un-smart as possible, to avoid awkward edge cases (for example, what
happens if the logic finds two different TPMs on the system; which
should it prefer; should it invisibly succeed and surprise the user?).
Instead, the preferred pattern is to require the user to explicitly say
which TPM they are trying to open.

This change introduces 3 packages as a replacement for
`transport.OpenTPM` (which this change marks as now Deprecated):

`transport/linuxtpm.Open(path)` opens Linux device TPMs (e.g., /dev/tpm0 or
/dev/tpmrm0)
`transport/linuxudstpm.Open(path)` opens Linux Unix Domain Socket TPMs
`transport/windowstpm.Open()` opens the TPM from TBS.dll

Intentionally, the now-deprecated `transport.OpenTPM` is not touched.
This would create an import cycle.

* Add small tests for each of the openers

* fix lint

* fix linuxudstpm and test

* fix the test in the case that the UDS simulator is not running

* remove extraneous test for windows
  • Loading branch information
chrisfenner authored Sep 19, 2024
1 parent ec70209 commit d96ccf7
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 20 deletions.
36 changes: 36 additions & 0 deletions tpm2/transport/linuxtpm/linuxtpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build !windows

// Package linuxtpm provides access to a physical TPM device via the device file.
package linuxtpm

import (
"errors"
"fmt"
"os"

"github.com/google/go-tpm/tpm2/transport"
)

var (
// ErrFileIsNotDevice indicates that the TPM file mode was not a device.
ErrFileIsNotDevice = errors.New("TPM file is not a device")
)

// Open opens the TPM device file at the given path.
func Open(path string) (transport.TPMCloser, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
}

if fi.Mode()&os.ModeDevice == 0 {
return nil, fmt.Errorf("%w: %s (%s)", ErrFileIsNotDevice, fi.Mode().String(), path)
}
var f *os.File
f, err = os.OpenFile(path, os.O_RDWR, 0600)
if err != nil {
return nil, err
}

return transport.FromReadWriteCloser(f), nil
}
25 changes: 25 additions & 0 deletions tpm2/transport/linuxtpm/linuxtpm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build !windows

package linuxtpm

import (
"os"
"testing"

"github.com/google/go-tpm/tpm2/transport"
testhelper "github.com/google/go-tpm/tpm2/transport/test"
)

func open(path string) func() (transport.TPMCloser, error) {
return func() (transport.TPMCloser, error) {
return Open(path)
}
}

func TestLocalTPM(t *testing.T) {
testhelper.RunTest(t, []error{os.ErrNotExist, os.ErrPermission, ErrFileIsNotDevice}, open("/dev/tpm0"))
}

func TestLocalResourceManagedTPM(t *testing.T) {
testhelper.RunTest(t, []error{os.ErrNotExist, os.ErrPermission, ErrFileIsNotDevice}, open("/dev/tpmrm0"))
}
94 changes: 94 additions & 0 deletions tpm2/transport/linuxudstpm/linuxudstpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//go:build !windows

// Package linuxudstpm provides access to a TPM device via a Unix domain socket.
package linuxudstpm

import (
"errors"
"fmt"
"net"
"os"

"github.com/google/go-tpm/tpm2/transport"
)

var (
// ErrFileIsNotSocket indicates that the TPM file is not a socket.
ErrFileIsNotSocket = errors.New("TPM file is not a socket")
// ErrMustCallWriteThenRead indicates that the file was not written-then-read in the expected pattern.
ErrMustCallWriteThenRead = errors.New("must call Write then Read in an alternating sequence")
// ErrNotOpen indicates that the TPM file is not currently open.
ErrNotOpen = errors.New("no connection is open")
)

// Open opens the TPM socket at the given path.
func Open(path string) (transport.TPMCloser, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
}

if fi.Mode()&os.ModeSocket == 0 {
return nil, fmt.Errorf("%w: %s (%s)", ErrFileIsNotSocket, fi.Mode().String(), path)
}
return transport.FromReadWriteCloser(newEmulatorReadWriteCloser(path)), nil
}

// dialer abstracts the net.Dial call so test code can provide its own net.Conn
// implementation.
type dialer func(network, path string) (net.Conn, error)

// emulatorReadWriteCloser manages connections with a TPM emulator over a Unix
// domain socket. These emulators often operate in a write/read/disconnect
// sequence, so the Write method always connects, and the Read method always
// closes. emulatorReadWriteCloser is not thread safe.
type emulatorReadWriteCloser struct {
path string
conn net.Conn
dialer dialer
}

// newEmulatorReadWriteCloser stores information about a Unix domain socket to
// write to and read from.
func newEmulatorReadWriteCloser(path string) *emulatorReadWriteCloser {
return &emulatorReadWriteCloser{
path: path,
dialer: net.Dial,
}
}

// Read implements the io.Reader interface.
func (erw *emulatorReadWriteCloser) Read(p []byte) (int, error) {
// Read is always the second operation in a Write/Read sequence.
if erw.conn == nil {
return 0, ErrMustCallWriteThenRead
}
n, err := erw.conn.Read(p)
erw.conn.Close()
erw.conn = nil
return n, err
}

// Write implements the io.Writer interface.
func (erw *emulatorReadWriteCloser) Write(p []byte) (int, error) {
if erw.conn != nil {
return 0, ErrMustCallWriteThenRead
}
var err error
erw.conn, err = erw.dialer("unix", erw.path)
if err != nil {
return 0, err
}
return erw.conn.Write(p)
}

// Close implements the io.Closer interface.
func (erw *emulatorReadWriteCloser) Close() error {
if erw.conn == nil {
// This is an expected possible state, e.g., if someone sent the TPM a command and didn't read the response.
return nil
}
err := erw.conn.Close()
erw.conn = nil
return err
}
30 changes: 30 additions & 0 deletions tpm2/transport/linuxudstpm/linuxudstpm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build !windows

package linuxudstpm

import (
"flag"
"os"
"syscall"
"testing"

"github.com/google/go-tpm/tpm2/transport"
testhelper "github.com/google/go-tpm/tpm2/transport/test"
)

var tpmSocket = flag.String("tpm_socket", "/dev/tpm0", "path to the TPM simulator UDS")

func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}

func open() func() (transport.TPMCloser, error) {
return func() (transport.TPMCloser, error) {
return Open(*tpmSocket)
}
}

func TestLocalUDSTPM(t *testing.T) {
testhelper.RunTest(t, []error{os.ErrNotExist, os.ErrPermission, ErrFileIsNotSocket, syscall.ECONNREFUSED}, open())
}
14 changes: 4 additions & 10 deletions tpm2/transport/open_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@ import (
legacy "github.com/google/go-tpm/legacy/tpm2"
)

// Wrap the legacy OpenTPM function so callers don't have to import both the
// legacy and the new TPM 2.0 API.
// TODO: When we delete the legacy API, we can make this the only copy of
// OpenTPM.

// OpenTPM opens a channel to the TPM at the given path. If the file is a
// device, then it treats it like a normal TPM device, and if the file is a
// Unix domain socket, then it opens a connection to the socket.
// OpenTPM opens the TPM at the given path. If no path is provided, it will
// attempt to use reasonable defaults.
//
// This function may also be invoked with no paths, as tpm2.OpenTPM(). In this
// case, the default paths on Linux (/dev/tpmrm0 then /dev/tpm0), will be used.
// Deprecated: Please use the individual transport packages (e.g.,
// go-tpm/tpm2/transport/linuxtpm).
func OpenTPM(path ...string) (TPMCloser, error) {
rwc, err := legacy.OpenTPM(path...)
if err != nil {
Expand Down
13 changes: 3 additions & 10 deletions tpm2/transport/open_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@ import (
legacy "github.com/google/go-tpm/legacy/tpm2"
)

// Wrap the legacy OpenTPM function so callers don't have to import both the
// legacy and the new TPM 2.0 API.
// TODO: When we delete the legacy API, we can make this the only copy of
// OpenTPM.

// OpenTPM opens a channel to the TPM at the given path. If the file is a
// device, then it treats it like a normal TPM device, and if the file is a
// Unix domain socket, then it opens a connection to the socket.
// OpenTPM opens the local system TPM.
//
// This function may also be invoked with no paths, as tpm2.OpenTPM(). In this
// case, the default paths on Linux (/dev/tpmrm0 then /dev/tpm0), will be used.
// Deprecated: Please use the individual transport packages (e.g.,
// go-tpm/tpm2/transport/windowstpm).
func OpenTPM() (TPMCloser, error) {
rwc, err := legacy.OpenTPM()
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions tpm2/transport/test/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Package testhelper provides some helper code for TPM transport tests.
package testhelper

import (
"bytes"
"encoding/binary"
"errors"
"testing"

"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/tpm2/transport"
)

// RunTest checks that the connection to the given TPM seems to be working.
func RunTest(t *testing.T, skipErrs []error, tpmOpener func() (transport.TPMCloser, error)) {
tpm, err := tpmOpener()
for _, skipErr := range skipErrs {
if errors.Is(err, skipErr) {
t.Skipf("%v", err)
}
}
if err != nil {
t.Fatalf("Failed to open TPM: %v", err)
}
defer func(tpm transport.TPMCloser) {
if err := tpm.Close(); err != nil {
t.Fatalf("tpm.Close() = %v", err)
}
}(tpm)

// Ping the TPM to ask it what the manufacturer is, as a basic consistency check.
cap, err := tpm2.GetCapability{
Capability: tpm2.TPMCapTPMProperties,
Property: uint32(tpm2.TPMPTManufacturer),
PropertyCount: 1,
}.Execute(tpm)

// We might run into one of the known "skip if this error" cases.
for _, skipErr := range skipErrs {
if errors.Is(err, skipErr) {
t.Skipf("%v", err)
}
}
if err != nil {
t.Fatalf("GetCapability() = %v", err)
}
props, err := cap.CapabilityData.Data.TPMProperties()
if err != nil {
t.Fatalf("cap.TPMProperties() = %v", err)
}
if len(props.TPMProperty) != 1 {
t.Fatalf("GetCapability() = %v properties, want 1", len(props.TPMProperty))
}

var idBuf bytes.Buffer
idBuf.Grow(4)
binary.Write(&idBuf, binary.BigEndian, props.TPMProperty[0].Value)
t.Logf("Manufacturer ID: %q", idBuf.String())
}
6 changes: 6 additions & 0 deletions tpm2/transport/tpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func FromReadWriter(rw io.ReadWriter) TPM {
return &wrappedRW{transport: rw}
}

// FromReadWriteCloser takes in a io.ReadWriteCloser and returns a
// transport.TPMCloser wrapping the io.ReadWriteCloser.
func FromReadWriteCloser(rwc io.ReadWriteCloser) TPMCloser {
return &wrappedRWC{transport: rwc}
}

// ToReadWriter takes in a transport TPM and returns an
// io.ReadWriter wrapping the transport TPM.
func ToReadWriter(tpm TPM) io.ReadWriter {
Expand Down
89 changes: 89 additions & 0 deletions tpm2/transport/windowstpm/windowstpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build windows

// Package windowstpm implements the TPM transport on Windows using tbs.dll.
package windowstpm

import (
"errors"
"fmt"
"io"

"github.com/google/go-tpm/tpm2/transport"
"github.com/google/go-tpm/tpmutil/tbs"
)

var (
// ErrNotTPM20 indicates that a TPM 2.0 was not found.
ErrNotTPM20 = errors.New("device is not a TPM 2.0")
)

const (
maxTPMResponse = 4096
)

// Open opens a channel to the TPM via TBS.
func Open() (transport.TPMCloser, error) {
info, err := tbs.GetDeviceInfo()
if err != nil {
return nil, err
}

if info.TPMVersion != tbs.TPMVersion20 {
return nil, fmt.Errorf("%w: %v", ErrNotTPM20, info.TPMVersion)
}

tpmContext, err := tbs.CreateContext(tbs.TPMVersion20, tbs.IncludeTPM20)
rwc := &winTPMBuffer{
context: tpmContext,
outBuffer: make([]byte, 0, maxTPMResponse),
}
return transport.FromReadWriteCloser(rwc), err
}

// winTPMBuffer is a ReadWriteCloser to access the TPM in Windows.
type winTPMBuffer struct {
context tbs.Context
outBuffer []byte
}

// Write implements the io.Writer interface.
//
// Executes the TPM command specified by commandBuffer (at Normal Priority), returning the number
// of bytes in the command and any error code returned by executing the TPM command. Command
// response can be read by calling Read().
func (rwc *winTPMBuffer) Write(commandBuffer []byte) (int, error) {
// TPM spec defines longest possible response to be maxTPMResponse.
rwc.outBuffer = rwc.outBuffer[:maxTPMResponse]

outBufferLen, err := rwc.context.SubmitCommand(
tbs.NormalPriority,
commandBuffer,
rwc.outBuffer,
)

if err != nil {
rwc.outBuffer = rwc.outBuffer[:0]
return 0, err
}
// Shrink outBuffer so it is length of response.
rwc.outBuffer = rwc.outBuffer[:outBufferLen]
return len(commandBuffer), nil
}

// Read implements the io.Reader interface.
//
// Provides TPM response from the command called in the last Write call.
func (rwc *winTPMBuffer) Read(responseBuffer []byte) (int, error) {
if len(rwc.outBuffer) == 0 {
return 0, io.EOF
}
lenCopied := copy(responseBuffer, rwc.outBuffer)
// Cut out the piece of slice which was just read out, maintaining original slice capacity.
rwc.outBuffer = append(rwc.outBuffer[:0], rwc.outBuffer[lenCopied:]...)
return lenCopied, nil
}

// Close implements the io.Closer interface.
func (rwc *winTPMBuffer) Close() error {
return rwc.context.Close()
}
Loading

0 comments on commit d96ccf7

Please sign in to comment.