Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tapping into TPM communication #515

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
36 changes: 36 additions & 0 deletions tpm/debug/bin.go
Original file line number Diff line number Diff line change
@@ -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)
}
20 changes: 20 additions & 0 deletions tpm/debug/bin_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
188 changes: 188 additions & 0 deletions tpm/debug/pcapng.go
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +149 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't look too much into this PR, but we probably want to move these ports customizable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but that wouldn't change much functionally. The PCAP is used just as a container in this case; we're not actually capturing traffic from/to those ports. The bytes read from and written to the TPM device are "massaged into network packets", so to say.

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
}
76 changes: 76 additions & 0 deletions tpm/debug/pcapng_test.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 14 additions & 0 deletions tpm/debug/tap.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading