diff --git a/go.mod b/go.mod index 274a5677..b134b9b8 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/google/go-tpm v0.9.0 github.com/google/go-tpm-tools v0.4.4 github.com/googleapis/gax-go/v2 v2.12.4 + github.com/gopacket/gopacket v1.2.0 github.com/peterbourgon/diskv/v3 v3.0.1 github.com/pkg/errors v0.9.1 github.com/schollz/jsonstore v1.1.0 diff --git a/go.sum b/go.sum index dba0cf2a..30db09d8 100644 --- a/go.sum +++ b/go.sum @@ -442,6 +442,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= +github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= +github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= @@ -798,6 +800,8 @@ github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= diff --git a/tpm/debug/bin.go b/tpm/debug/bin.go new file mode 100644 index 00000000..2106a017 --- /dev/null +++ b/tpm/debug/bin.go @@ -0,0 +1,36 @@ +package debug + +import ( + "io" + "sync" +) + +// NewBinTap creates a new TPM tap that writes the +// the outgoing and incoming TPM communication verbatim +// to the (single) underlying [io.Writer]. +func NewBinTap(w io.Writer) Tap { + return &binTap{w: w} +} + +type binTap struct { + sync.Mutex + w io.Writer +} + +func (t *binTap) Rx() io.Writer { + return t +} + +func (t *binTap) Tx() io.Writer { + return t +} + +// Write implements [io.Writer] and writes the provided data +// to the underlying [io.Writer] verbatim for both TPM commands +// and responses. +func (t *binTap) Write(data []byte) (int, error) { + t.Lock() + defer t.Unlock() + + return t.w.Write(data) +} diff --git a/tpm/debug/bin_test.go b/tpm/debug/bin_test.go new file mode 100644 index 00000000..27e5ae68 --- /dev/null +++ b/tpm/debug/bin_test.go @@ -0,0 +1,20 @@ +package debug + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_binTapWrites(t *testing.T) { + var buf bytes.Buffer + bt := NewBinTap(&buf) + n, err := bt.Tx().Write([]byte{1, 2, 3, 4}) + require.NoError(t, err) + require.Equal(t, 4, n) + n, err = bt.Rx().Write([]byte{5, 6, 7, 8}) + require.NoError(t, err) + require.Equal(t, 4, n) + require.Equal(t, []byte{1, 2, 3, 4, 5, 6, 7, 8}, buf.Bytes()) +} diff --git a/tpm/debug/pcapng.go b/tpm/debug/pcapng.go new file mode 100644 index 00000000..cbba6bbd --- /dev/null +++ b/tpm/debug/pcapng.go @@ -0,0 +1,188 @@ +package debug + +import ( + "fmt" + "io" + "net" + "runtime" + "sync" + "time" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/gopacket/gopacket/pcapgo" +) + +var ngSectionInfo = pcapgo.NgSectionInfo{ + Application: "com.smallstep.crypto.tpmtap", + Hardware: runtime.GOARCH, + OS: runtime.GOOS, +} + +var ngInterface = pcapgo.NgInterface{ + Name: "tpm", + Description: "TPM Command Channel", + OS: runtime.GOOS, + SnapLength: 0, //unlimited + TimestampResolution: 9, +} + +// FlushFunc is the type of function returned when creating +// a new PcapNG tap. The underlying PcapNG writer must be flushed +// before the [io.Writer] it's writing to is closed. +type FlushFunc func() error + +// NewPcapngTap creates a new TPM tap that writes the +// the outgoing and incoming TPM communication to the +// provided [io.Writer] in PcapNG format. +func NewPcapngTap(w io.Writer) (Tap, FlushFunc, error) { + // record current defaults, so that they can be restored after + // creating a PcapNG writer. + currentSectionInfo := pcapgo.DefaultNgWriterOptions.SectionInfo + defer func() { + pcapgo.DefaultNgWriterOptions.SectionInfo = currentSectionInfo + }() + currentInterface := pcapgo.DefaultNgInterface + defer func() { + pcapgo.DefaultNgInterface = currentInterface + }() + + // override some default properties of the PcapNG writer + pcapgo.DefaultNgWriterOptions.SectionInfo = ngSectionInfo + pcapgo.DefaultNgInterface = ngInterface + + // create a new PcapNG writer + pw, err := pcapgo.NewNgWriter(w, layers.LinkTypeEthernet) + if err != nil { + return nil, nil, fmt.Errorf("failed creating PcapNG writer: %w", err) + } + finish := func() error { + if err := pw.Flush(); err != nil { + return fmt.Errorf("failed to flush PcapNG writer: %w", err) + } + return nil + } + + // the single PcapNG writer is used for both TPM commands and responses. + // It's wrapped, so that the direction of the data is know at the time of + // writing a new packet to the PcapNG writer. + return &pcapTap{ + in: &pcapWriter{ + in: true, + writer: pw, + }, + out: &pcapWriter{ + writer: pw, + }, + }, finish, nil +} + +type pcapTap struct { + in *pcapWriter + out *pcapWriter +} + +func (t *pcapTap) Rx() io.Writer { + return t.in +} + +func (t *pcapTap) Tx() io.Writer { + return t.out +} + +type pcapWriter struct { + in bool + writer *pcapgo.NgWriter +} + +var ( + outSeq uint32 + inSeq uint32 + mu sync.Mutex +) + +// Write implements [io.Writer] and writes the provided data +// to the underlying [io.Writer] in PcapNG format. +func (w *pcapWriter) Write(data []byte) (int, error) { + mu.Lock() + defer mu.Unlock() + + err := write(w.writer, data, w.in, inSeq, outSeq) + + if w.in { + inSeq += uint32(len(data)) + } else { + outSeq += uint32(len(data)) + } + + return len(data), err +} + +var ethernetLayer = &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + DstMAC: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + EthernetType: layers.EthernetTypeIPv4, +} + +var ipLayer = &layers.IPv4{ + Version: 4, + TTL: 64, + Flags: layers.IPv4DontFragment, + Protocol: layers.IPProtocolTCP, + SrcIP: net.IP{127, 0, 0, 1}, + DstIP: net.IP{127, 0, 0, 1}, +} + +var serializeOptions = gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, +} + +var decodeOptions = gopacket.DecodeOptions{ + Lazy: true, + NoCopy: true, +} + +func write(w *pcapgo.NgWriter, data []byte, in bool, inSeq, outSeq uint32) error { + var tcpLayer = &layers.TCP{Window: 16} + if in { + tcpLayer.SrcPort = layers.TCPPort(2321) + tcpLayer.DstPort = layers.TCPPort(50001) + tcpLayer.ACK = true + tcpLayer.Seq = inSeq + tcpLayer.Ack = outSeq + tcpLayer.PSH = true + } else { + tcpLayer.SrcPort = layers.TCPPort(50001) + tcpLayer.DstPort = layers.TCPPort(2321) + tcpLayer.ACK = false + tcpLayer.Seq = outSeq + } + + if err := tcpLayer.SetNetworkLayerForChecksum(ipLayer); err != nil { + return fmt.Errorf("failed setting network layer: %w", err) + } + buffer := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(buffer, serializeOptions, + ethernetLayer, + ipLayer, + tcpLayer, + gopacket.Payload(data), + ); err != nil { + return fmt.Errorf("failed serializing layers: %w", err) + } + + p := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeEthernet, decodeOptions) + + // construct the capture info based on what's in the new packet + ci := p.Metadata().CaptureInfo + ci.CaptureLength = len(p.Data()) + ci.Length = ci.CaptureLength + ci.Timestamp = time.Now() + + if err := w.WritePacket(ci, p.Data()); err != nil { + return fmt.Errorf("failed writing packet data: %w", err) + } + + return nil +} diff --git a/tpm/debug/pcapng_test.go b/tpm/debug/pcapng_test.go new file mode 100644 index 00000000..ae13c582 --- /dev/null +++ b/tpm/debug/pcapng_test.go @@ -0,0 +1,76 @@ +package debug + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/gopacket/gopacket/pcapgo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_pcapNGTapWrites(t *testing.T) { + var buf bytes.Buffer + tr, flush, err := NewPcapngTap(&buf) + require.NoError(t, err) + + b, err := hex.DecodeString("8001000000160000017a000000060000010600000001") + require.NoError(t, err) + n, err := tr.Tx().Write(b) + require.NoError(t, err) + + assert.Equal(t, 22, n) + + b, err = hex.DecodeString("80010000001b000000000100000006000000010000010678434720") + require.NoError(t, err) + n, err = tr.Rx().Write(b) + require.NoError(t, err) + assert.Equal(t, 27, n) + + b, err = hex.DecodeString("8001000000160000017a000000060000010700000001") + require.NoError(t, err) + n, err = tr.Tx().Write(b) + require.NoError(t, err) + assert.Equal(t, 22, n) + + b, err = hex.DecodeString("80010000001b00000000010000000600000001000001076654504d") + require.NoError(t, err) + n, err = tr.Rx().Write(b) + require.NoError(t, err) + assert.Equal(t, 27, n) + + // finish writing to the PcapNG tap + flush() + + r, err := pcapgo.NewNgReader(&buf, pcapgo.DefaultNgReaderOptions) + require.NoError(t, err) + + info := r.SectionInfo() + assert.Equal(t, "com.smallstep.crypto.tpmtap", info.Application) + + iface, err := r.Interface(0) + require.NoError(t, err) + assert.Equal(t, "tpm", iface.Name) + assert.Equal(t, "TPM Command Channel", iface.Description) + + count := 0 + packetSource := gopacket.NewZeroCopyPacketSource(r, r.LinkType()) + for p := range packetSource.Packets() { + t.Log(p.Dump()) + + if assert.IsType(t, &layers.TCP{}, p.TransportLayer().(*layers.TCP)) { + tcp := p.TransportLayer().(*layers.TCP) + tcp.SetNetworkLayerForChecksum(p.NetworkLayer()) + + err, errs := p.VerifyChecksums() + assert.NoError(t, err) + assert.Empty(t, errs) + } + count += 1 + } + + require.Equal(t, 4, count) // 4 packets expected +} diff --git a/tpm/debug/tap.go b/tpm/debug/tap.go new file mode 100644 index 00000000..9979db1b --- /dev/null +++ b/tpm/debug/tap.go @@ -0,0 +1,14 @@ +package debug + +import ( + "io" +) + +// Tap is an interface providing TPM communication tapping +// capabilities. [Tx] and [Rx] provide access to [io.Writer]s +// that correspond to all (serialized) transmitted and received +// TPM commands and responses, respectively. +type Tap interface { + Tx() io.Writer + Rx() io.Writer +} diff --git a/tpm/debug/text.go b/tpm/debug/text.go new file mode 100644 index 00000000..a8b86350 --- /dev/null +++ b/tpm/debug/text.go @@ -0,0 +1,45 @@ +package debug + +import ( + "fmt" + "io" +) + +// NewTextTap creates a new TPM tap that writes the +// the outgoing and incoming TPM communication in hex +// format. +func NewTextTap(reads, writes io.Writer) Tap { + return &tap{ + in: &directionalWrapper{reads, true}, + out: &directionalWrapper{writes, false}, + } +} + +type tap struct { + in io.Writer + out io.Writer +} + +func (t *tap) Rx() io.Writer { + return t.in +} + +func (t *tap) Tx() io.Writer { + return t.out +} + +type directionalWrapper struct { + w io.Writer + in bool +} + +// Write implements [io.Writer] and writes the provided data +// to the underlying [io.Writer], formatted as hex, and prefixed +// with the direction of the data. +func (w directionalWrapper) Write(data []byte) (int, error) { + if w.in { + return fmt.Fprintf(w.w, "<- %x\n", data) + } + + return fmt.Fprintf(w.w, "-> %x\n", data) +} diff --git a/tpm/debug/text_test.go b/tpm/debug/text_test.go new file mode 100644 index 00000000..3b183555 --- /dev/null +++ b/tpm/debug/text_test.go @@ -0,0 +1,23 @@ +package debug + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_directionalWrapper_Write(t *testing.T) { + var reads bytes.Buffer + var writes bytes.Buffer + tt := NewTextTap(&reads, &writes) + n, err := tt.Tx().Write([]byte{1, 2, 3, 4}) + require.NoError(t, err) + require.Equal(t, 12, n) + require.Equal(t, "-> 01020304\n", writes.String()) + + n, err = tt.Rx().Write([]byte{5, 6, 7, 8}) + require.NoError(t, err) + require.Equal(t, 12, n) + require.Equal(t, "<- 05060708\n", reads.String()) +} diff --git a/tpm/internal/close/close_others.go b/tpm/internal/close/close_others.go index 395c23be..c837d3ff 100644 --- a/tpm/internal/close/close_others.go +++ b/tpm/internal/close/close_others.go @@ -9,10 +9,14 @@ import ( "github.com/google/go-tpm/tpmutil" "github.com/smallstep/go-attestation/attest" + "go.step.sm/crypto/tpm/internal/interceptor" "go.step.sm/crypto/tpm/internal/socket" ) func closeRWC(rwc io.ReadWriteCloser) error { + if ic, ok := rwc.(*interceptor.RWC); ok { + rwc = ic.Unwrap() + } if _, ok := rwc.(*tpmutil.EmulatorReadWriteCloser); ok { return nil // EmulatorReadWriteCloser automatically closes on every write/read cycle } @@ -20,8 +24,13 @@ func closeRWC(rwc io.ReadWriteCloser) error { } func attestTPM(t *attest.TPM, c *attest.OpenConfig) error { - if _, ok := c.CommandChannel.(*socket.CommandChannelWithoutMeasurementLog); ok { + cc := c.CommandChannel + if ic, ok := cc.(*interceptor.CommandChannel); ok { + cc = ic.Unwrap() + } + if _, ok := cc.(*socket.CommandChannelWithoutMeasurementLog); ok { return nil // backed by tpmutil.EmulatorReadWriteCloser; already closed } + return t.Close() } diff --git a/tpm/internal/interceptor/interceptor.go b/tpm/internal/interceptor/interceptor.go new file mode 100644 index 00000000..503b8c0f --- /dev/null +++ b/tpm/internal/interceptor/interceptor.go @@ -0,0 +1,111 @@ +package interceptor + +import ( + "io" + + "github.com/smallstep/go-attestation/attest" + + "go.step.sm/crypto/tpm/debug" +) + +type CommandChannel struct { + in io.Writer + out io.Writer + wrapped attest.CommandChannelTPM20 +} + +func CommandChannelFromTap(tap debug.Tap) *CommandChannel { + return &CommandChannel{ + in: tap.Rx(), + out: tap.Tx(), + } +} + +func (c *CommandChannel) Wrap(cc attest.CommandChannelTPM20) *CommandChannel { + c.wrapped = cc + return c +} + +func (c *CommandChannel) Unwrap() attest.CommandChannelTPM20 { + return c.wrapped +} + +func (c *CommandChannel) Close() error { + return c.wrapped.Close() +} + +func (c *CommandChannel) MeasurementLog() ([]byte, error) { + return c.wrapped.MeasurementLog() +} + +func (c *CommandChannel) Read(data []byte) (int, error) { + n, err := c.wrapped.Read(data) + if err != nil { + return n, err + } + + _, _ = c.in.Write(data[:n]) + + return n, nil +} + +func (c *CommandChannel) Write(data []byte) (int, error) { + n, err := c.wrapped.Write(data) + if err != nil { + return n, err + } + + _, _ = c.out.Write(data[:n]) + return n, nil +} + +var _ attest.CommandChannelTPM20 = (*CommandChannel)(nil) + +type RWC struct { + in io.Writer + out io.Writer + wrapped io.ReadWriteCloser +} + +func RWCFromTap(tap debug.Tap) *RWC { + return &RWC{ + in: tap.Rx(), + out: tap.Tx(), + } +} + +func (c *RWC) Wrap(rwc io.ReadWriteCloser) *RWC { + c.wrapped = rwc + return c +} + +func (c *RWC) Unwrap() io.ReadWriteCloser { + return c.wrapped +} + +func (c *RWC) Close() error { + return c.wrapped.Close() +} + +func (c *RWC) Read(data []byte) (int, error) { + n, err := c.wrapped.Read(data) + if err != nil { + return n, err + } + + _, _ = c.in.Write(data[:n]) + + return n, nil +} + +func (c *RWC) Write(data []byte) (int, error) { + n, err := c.wrapped.Write(data) + if err != nil { + return n, err + } + + _, _ = c.out.Write(data[:n]) + return n, nil +} + +var _ io.ReadWriteCloser = (*RWC)(nil) diff --git a/tpm/tpm.go b/tpm/tpm.go index 2f998635..1edc635f 100644 --- a/tpm/tpm.go +++ b/tpm/tpm.go @@ -6,11 +6,15 @@ import ( "fmt" "io" "net/http" + "os" + "runtime" "sync" "github.com/smallstep/go-attestation/attest" + "go.step.sm/crypto/tpm/debug" closer "go.step.sm/crypto/tpm/internal/close" + "go.step.sm/crypto/tpm/internal/interceptor" "go.step.sm/crypto/tpm/internal/open" "go.step.sm/crypto/tpm/internal/socket" "go.step.sm/crypto/tpm/simulator" @@ -31,6 +35,8 @@ type TPM struct { store storage.TPMStore simulator simulator.Simulator commandChannel CommandChannel + tap debug.Tap + shouldRetap bool downloader *downloader options *options initCommandChannelOnce sync.Once @@ -86,6 +92,8 @@ func WithSimulator(sim simulator.Simulator) NewTPMOption { type CommandChannel attest.CommandChannelTPM20 +// WithCommandChannel is used to configure a preconfigured and +// preinitialized TPM command channel. func WithCommandChannel(commandChannel CommandChannel) NewTPMOption { return func(o *options) error { o.commandChannel = commandChannel @@ -93,6 +101,18 @@ func WithCommandChannel(commandChannel CommandChannel) NewTPMOption { } } +// WithTap is used to configure a tap. The tap taps into all of +// the communication to and from the TPM. This can be used to +// inspect and debug the commands and responses. Due to how TPM +// operations (that go through go-attestation) work, the tap option +// is ignored on Windows. +func WithTap(tap debug.Tap) NewTPMOption { + return func(o *options) error { + o.tap = tap + return nil + } +} + type options struct { deviceName string attestConfig *attest.OpenConfig @@ -100,6 +120,7 @@ type options struct { commandChannel CommandChannel store storage.TPMStore downloader *downloader + tap debug.Tap } func (o *options) validate() error { @@ -133,6 +154,8 @@ func New(opts ...NewTPMOption) (*TPM, error) { downloader: tpmOptions.downloader, simulator: tpmOptions.simulator, commandChannel: tpmOptions.commandChannel, + tap: tpmOptions.tap, + shouldRetap: false, options: &tpmOptions, }, nil } @@ -171,6 +194,12 @@ func (t *TPM) open(ctx context.Context) (err error) { // The simulator is currently only used for testing. if t.simulator != nil { if t.attestTPM == nil { + // enable tapping into TPM communication. This does not work on Windows, because go-attestation + // relies on calling into the Windows Platform Crypto Provider libraries instead of interacting + // with a TPM through binary commands directly. + if err := t.tapCommandChannel(); err != nil { + return fmt.Errorf("failed tapping TPM command channel: %w", err) + } at, err := attest.OpenTPM(t.attestConfig) if err != nil { return fmt.Errorf("failed opening attest.TPM: %w", err) @@ -178,6 +207,7 @@ func (t *TPM) open(ctx context.Context) (err error) { t.attestTPM = at } t.rwc = t.simulator + t.tapRWC() } else { // TODO(hs): when an internal call to open is performed, but when // switching the "TPM implementation" to use between the two types, @@ -190,7 +220,15 @@ func (t *TPM) open(ctx context.Context) (err error) { return fmt.Errorf("failed opening TPM: %w", err) } t.rwc = rwc + t.tapRWC() } else { + // enable tapping into TPM communication. This does not work on Windows, because go-attestation + // relies on calling into the Windows Platform Crypto Provider libraries instead of interacting + // with a TPM through binary commands directly. + if err := t.tapCommandChannel(); err != nil { + return fmt.Errorf("failed tapping TPM command channel: %w", err) + } + // TODO(hs): attest.OpenTPM doesn't currently take into account the // device name provided. This doesn't seem to be an available option // to filter on currently? @@ -198,6 +236,7 @@ func (t *TPM) open(ctx context.Context) (err error) { if err != nil { return fmt.Errorf("failed opening TPM: %w", err) } + t.attestTPM = at } } @@ -205,6 +244,43 @@ func (t *TPM) open(ctx context.Context) (err error) { return nil } +func (t *TPM) tapRWC() { + if t.tap == nil || runtime.GOOS == "windows" { + return + } + + t.rwc = interceptor.RWCFromTap(t.tap).Wrap(t.rwc) +} + +func (t *TPM) tapCommandChannel() error { + if t.tap == nil || runtime.GOOS == "windows" { + return nil + } + + if t.attestConfig.CommandChannel == nil || t.shouldRetap { + rwc, err := open.TPM(t.deviceName) + if err != nil { + return fmt.Errorf("failed opening TPM: %w", err) + } + cc := &linuxCmdChannel{rwc} + t.attestConfig.CommandChannel = interceptor.CommandChannelFromTap(t.tap).Wrap(cc) + t.shouldRetap = true // retap required; the wrapped command channel is closed and thus needs to be tapped again + } else { + t.attestConfig.CommandChannel = interceptor.CommandChannelFromTap(t.tap).Wrap(t.commandChannel) + } + + return nil +} + +type linuxCmdChannel struct { + io.ReadWriteCloser +} + +// MeasurementLog implements CommandChannelTPM20. +func (cc *linuxCmdChannel) MeasurementLog() ([]byte, error) { + return os.ReadFile("/sys/kernel/security/tpm0/binary_bios_measurements") +} + // initializeCommandChannel initializes the TPM's command channel based on // configuration provided when creating the TPM instance. The method is // primarily used to be able to use a TPM simulator in lieu of a real TPM diff --git a/tpm/tpm_simulator_test.go b/tpm/tpm_simulator_test.go index c277df8f..7df36e6b 100644 --- a/tpm/tpm_simulator_test.go +++ b/tpm/tpm_simulator_test.go @@ -4,6 +4,7 @@ package tpm import ( + "bytes" "context" "crypto" "crypto/ecdsa" @@ -26,16 +27,19 @@ import ( "go.step.sm/crypto/keyutil" "go.step.sm/crypto/minica" + "go.step.sm/crypto/tpm/debug" "go.step.sm/crypto/tpm/simulator" "go.step.sm/crypto/tpm/storage" "go.step.sm/crypto/tpm/tss2" "go.step.sm/crypto/x509util" ) -func newSimulatedTPM(t *testing.T) *TPM { +func newSimulatedTPM(t *testing.T, opts ...NewTPMOption) *TPM { t.Helper() tmpDir := t.TempDir() - tpm, err := New(withSimulator(t), WithStore(storage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead + allOpts := append([]NewTPMOption{}, withSimulator(t), WithStore(storage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead + allOpts = append(allOpts, opts...) + tpm, err := New(allOpts...) require.NoError(t, err) return tpm } @@ -77,6 +81,31 @@ func TestTPM_Info(t *testing.T) { require.Equal(t, expected, info) } +func TestTPM_InfoWithTap(t *testing.T) { + var reads bytes.Buffer + var writes bytes.Buffer + tpm := newSimulatedTPM(t, WithTap(debug.NewTextTap(&reads, &writes))) + info, err := tpm.Info(context.Background()) + require.NoError(t, err) + + // expected TPM info for the Microsoft TPM simulator + expected := &Info{ + Version: Version(2), + Interface: Interface(3), + Manufacturer: GetManufacturerByID(1297303124), + VendorInfo: "xCG fTPM", + FirmwareVersion: FirmwareVersion{ + Major: 8215, + Minor: 1561, + }, + } + + require.Equal(t, expected, info) + + assert.Equal(t, "<- 80010000001b000000000100000006000000010000010678434720\n<- 80010000001b00000000010000000600000001000001076654504d\n<- 80010000001b000000000100000006000000010000010800000000\n<- 80010000001b000000000100000006000000010000010900000000\n<- 80010000001b00000000010000000600000001000001054d534654\n<- 80010000001b000000000100000006000000010000010b20170619\n", reads.String()) + assert.Equal(t, "-> 8001000000160000017a000000060000010600000001\n-> 8001000000160000017a000000060000010700000001\n-> 8001000000160000017a000000060000010800000001\n-> 8001000000160000017a000000060000010900000001\n-> 8001000000160000017a000000060000010500000001\n-> 8001000000160000017a000000060000010b00000001\n", writes.String()) +} + func TestTPM_GenerateRandom(t *testing.T) { tpm := newSimulatedTPM(t) b, err := tpm.GenerateRandom(context.Background(), 16) @@ -88,6 +117,22 @@ func TestTPM_GenerateRandom(t *testing.T) { require.Len(t, b, 10) } +func TestTPM_GenerateRandomWithTap(t *testing.T) { + var reads bytes.Buffer + var writes bytes.Buffer + tpm := newSimulatedTPM(t, WithTap(debug.NewTextTap(&reads, &writes))) + b, err := tpm.GenerateRandom(context.Background(), 16) + require.NoError(t, err) + require.Len(t, b, 16) + + b, err = tpm.GenerateRandom(context.Background(), 10) + require.NoError(t, err) + require.Len(t, b, 10) + + assert.Len(t, reads.Bytes(), 108) + assert.Equal(t, "-> 80010000000c0000017b0010\n-> 80010000000c0000017b000a\n", writes.String()) +} + func newErrorTPM(t *testing.T) *TPM { t.Helper() tmpDir := t.TempDir()