From e5997ada4c631f2b174b59772f4299fad176c9e5 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 17 Feb 2022 10:10:04 +0100 Subject: [PATCH] Fixup tests, add testing pipelines and small refactors Signed-off-by: Ettore Di Giacinto --- .github/workflows/static_analisys.yaml | 37 ++++++++++++++++++++ .github/workflows/tests.yaml | 27 +++++++++++++++ backend/fake.go | 7 ++++ backend/socket.go | 1 + backend/socket_test.go | 2 +- config.go | 43 +++++++++++++++++++---- get.go | 22 +++++++----- go.mod | 2 +- go.sum | 1 - tpm.go | 25 ++++++++------ tpm_attestor.go | 46 +++++++++++++++++++++---- tpm_test.go | 47 +++++++++++++------------- 12 files changed, 200 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/static_analisys.yaml create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/static_analisys.yaml b/.github/workflows/static_analisys.yaml new file mode 100644 index 0000000..567f8c6 --- /dev/null +++ b/.github/workflows/static_analisys.yaml @@ -0,0 +1,37 @@ +name: Static Analysis +on: [push, pull_request] +concurrency: + group: static-analysis-${{ github.head_ref || github.ref }}-${{ github.repository }} + cancel-in-progress: true +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + - name: Get dependencies + run: | + # Needed for github.com/google/go-tspi/tspi + sudo apt-get install libtspi-dev + go install golang.org/x/tools/cmd/goimports@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + go install golang.org/x/lint/golint@latest + go install honnef.co/go/tools/cmd/staticcheck@v0.2.0 + + - name: Vet + run: go vet -tags ci ./... + + - name: Goimports + run: test -z $(goimports -e -d . | tee /dev/stderr) + + - name: Gocyclo + run: gocyclo -over 30 . + + - name: Golint + run: golint -set_exit_status $(go list -tags ci ./...) + + - name: Staticcheck + run: staticcheck -go 1.12 ./... \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..b74a098 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,27 @@ +name: Tests +on: [push, pull_request] +concurrency: + group: tests-${{ github.head_ref || github.ref }}-${{ github.repository }} + cancel-in-progress: true +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + - name: Get dependencies + run: | + # Needed for github.com/google/go-tspi/tspi + # in opensuse this is trousers-devel + sudo add-apt-repository ppa:smoser/swtpm + sudo apt-get update + sudo apt-get install libtspi-dev swtpm + swtpm socket --server type=unixio,path=/tmp/tpm-server --ctrl type=unixio,path=/tmp/tpm-ctrl --tpm2 -d + go get github.com/onsi/ginkgo/v2/ginkgo + go get github.com/onsi/gomega/... + - name: Run tests + run: | + TPM_SOCKET=/tmp/tpm-ctrl ginkgo -r ./... \ No newline at end of file diff --git a/backend/fake.go b/backend/fake.go index 6d4645e..022ee24 100644 --- a/backend/fake.go +++ b/backend/fake.go @@ -4,6 +4,7 @@ import ( "io" ) +// FakeTPM is a wrapper for fake TPM devices type FakeTPM struct { io.ReadWriteCloser } @@ -14,4 +15,10 @@ var fixedLog = []byte{0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x33, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, 0x2, 0x1, 0x0, 0x0, 0x0, 0xb, 0x0, 0x20, 0x0, 0x0} +// MeasurementLog returns static log data to comply to TPM interface func (*FakeTPM) MeasurementLog() ([]byte, error) { return fixedLog, nil } + +// Fake returns a fake TPM-satisfying interface from a ReadWriteCloser +func Fake(rw io.ReadWriteCloser) *FakeTPM { + return &FakeTPM{ReadWriteCloser: rw} +} diff --git a/backend/socket.go b/backend/socket.go index e7ff233..a28e899 100644 --- a/backend/socket.go +++ b/backend/socket.go @@ -2,6 +2,7 @@ package backend import "net" +// Socket returns a fake TPM interface from a unix socket func Socket(f string) (*FakeTPM, error) { conn, err := net.Dial("unix", f) if err != nil { diff --git a/backend/socket_test.go b/backend/socket_test.go index 0aa88a8..87f43b2 100644 --- a/backend/socket_test.go +++ b/backend/socket_test.go @@ -9,7 +9,7 @@ import ( ) var _ = Describe("SWTPM", func() { - socket := os.Getenv("SWTPM_SOCKET") + socket := os.Getenv("TPM_SOCKET") Context("opening socket connection", func() { It("fails on invalid files", func() { diff --git a/config.go b/config.go index bd5bbca..5c8fac6 100644 --- a/config.go +++ b/config.go @@ -1,35 +1,64 @@ package tpm -import "github.com/google/go-attestation/attest" +import ( + "net/http" -type Config struct { + "github.com/google/go-attestation/attest" +) + +type config struct { emulated bool commandChannel attest.CommandChannelTPM20 seed int64 + + cacerts []byte + header http.Header } -type Option func(c *Config) error +// Option is a generic option for TPM configuration +type Option func(c *config) error -var Emulated Option = func(c *Config) error { +// Emulated sets an emulated device in place of a real native TPM device. +// Note, the emulated device is embedded and it is unsafe. +// Should just be used for testing. +var Emulated Option = func(c *config) error { c.emulated = true return nil } +// WithCAs sets the root CAs for the request +func WithCAs(ca []byte) Option { + return func(c *config) error { + c.cacerts = ca + return nil + } +} + +// WithHeader sets a specific header for the request +func WithHeader(header http.Header) Option { + return func(c *config) error { + c.header = header + return nil + } +} + +// WithSeed sets a permanent seed. Used with TPM emulated device. func WithSeed(s int64) Option { - return func(c *Config) error { + return func(c *config) error { c.seed = s return nil } } +// WithCommandChannel overrides the TPM command channel func WithCommandChannel(cc attest.CommandChannelTPM20) Option { - return func(c *Config) error { + return func(c *config) error { c.commandChannel = cc return nil } } -func (c *Config) Apply(opts ...Option) error { +func (c *config) apply(opts ...Option) error { for _, o := range opts { if err := o(c); err != nil { return err diff --git a/get.go b/get.go index 7bb0177..89dda43 100644 --- a/get.go +++ b/get.go @@ -16,11 +16,21 @@ import ( "github.com/sirupsen/logrus" ) -func Get(cacerts []byte, url string, header http.Header, opts ...Option) ([]byte, error) { +// Get retrieves a message from a remote ws server after +// a successfully process of the TPM challenge +func Get(url string, opts ...Option) ([]byte, error) { + c := &config{} + c.apply(opts...) + + header := c.header + if c.header == nil { + header = http.Header{} + } + dialer := websocket.DefaultDialer - if len(cacerts) > 0 { + if len(c.cacerts) > 0 { pool := x509.NewCertPool() - pool.AppendCertsFromPEM(cacerts) + pool.AppendCertsFromPEM(c.cacerts) dialer = &websocket.Dialer{ Proxy: http.ProxyFromEnvironment, HandshakeTimeout: 45 * time.Second, @@ -30,9 +40,6 @@ func Get(cacerts []byte, url string, header http.Header, opts ...Option) ([]byte } } - c := &Config{} - c.Apply(opts...) - attestationData, aikBytes, err := getAttestationData(c) if err != nil { return nil, err @@ -48,9 +55,6 @@ func Get(cacerts []byte, url string, header http.Header, opts ...Option) ([]byte return nil, err } - if header == nil { - header = http.Header{} - } header.Add("Authorization", token) wsURL := strings.Replace(url, "http", "ws", 1) logrus.Infof("Using TPMHash %s to dial %s", hash, wsURL) diff --git a/go.mod b/go.mod index d87f9ad..9eb96c6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/google/certificate-transparency-go v1.1.2 github.com/google/go-attestation v0.4.3 + github.com/google/go-tpm-tools v0.3.2 github.com/gorilla/websocket v1.5.0 github.com/onsi/ginkgo/v2 v2.1.3 github.com/onsi/gomega v1.17.0 @@ -14,7 +15,6 @@ require ( require ( github.com/google/go-tpm v0.3.3 // indirect - github.com/google/go-tpm-tools v0.3.2 // indirect github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad // indirect golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b // indirect golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect diff --git a/go.sum b/go.sum index d3805c0..1b4fcee 100644 --- a/go.sum +++ b/go.sum @@ -331,7 +331,6 @@ github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51B github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0= github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= github.com/google/go-tpm-tools v0.2.1/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= -github.com/google/go-tpm-tools v0.3.1 h1:AFlmenDrIe0WU5AvpbfGFOLprTJTg/fCwmTyFdDEjbM= github.com/google/go-tpm-tools v0.3.1/go.mod h1:PSg+r5hSZI5tP3X7LBQx2sW1VSZUqZHBSrKyDqrB21U= github.com/google/go-tpm-tools v0.3.2 h1:2KbPTrwqLTJUZIZNoMSd1UlzoUvmSNbtm14WuDovZjw= github.com/google/go-tpm-tools v0.3.2/go.mod h1:FYUkglac8nSi15sPnNrP9fha3A6PRb6qzEVA+Zkh8SA= diff --git a/tpm.go b/tpm.go index f7a07fd..7718adf 100644 --- a/tpm.go +++ b/tpm.go @@ -11,6 +11,9 @@ import ( "github.com/rancher-sandbox/go-tpm/backend" ) +// ResolveToken is just syntax sugar around GetPubHash. +// If the token provided is in EK's form it just returns it, otherwise +// retrieves the pubhash func ResolveToken(token string, opts ...Option) (bool, string, error) { if !strings.HasPrefix(token, "tpm://") { return false, token, nil @@ -20,10 +23,10 @@ func ResolveToken(token string, opts ...Option) (bool, string, error) { return true, hash, err } +// GetPubHash returns the EK's pub hash func GetPubHash(opts ...Option) (string, error) { - - c := &Config{} - c.Apply(opts...) + c := &config{} + c.apply(opts...) ek, err := getEK(c) if err != nil { @@ -38,7 +41,7 @@ func GetPubHash(opts ...Option) (string, error) { return hash, nil } -func getTPM(c *Config) (*attest.TPM, error) { +func getTPM(c *config) (*attest.TPM, error) { cfg := &attest.OpenConfig{ TPMVersion: attest.TPMVersion20, @@ -52,20 +55,20 @@ func getTPM(c *Config) (*attest.TPM, error) { if err != nil { return nil, err } - cfg.CommandChannel = &backend.FakeTPM{ReadWriteCloser: sim} + cfg.CommandChannel = backend.Fake(sim) } else { sim, err := simulator.GetWithFixedSeedInsecure(c.seed) if err != nil { return nil, err } - cfg.CommandChannel = &backend.FakeTPM{ReadWriteCloser: sim} + cfg.CommandChannel = backend.Fake(sim) } return attest.OpenTPM(cfg) } -func getEK(c *Config) (*attest.EK, error) { +func getEK(c *config) (*attest.EK, error) { var err error tpm, err := getTPM(c) @@ -86,7 +89,7 @@ func getEK(c *Config) (*attest.EK, error) { return &eks[0], nil } -func getToken(data *AttestationData) (string, error) { +func getToken(data *attestationData) (string, error) { bytes, err := json.Marshal(data) if err != nil { return "", fmt.Errorf("marshalling attestation data: %w", err) @@ -95,7 +98,7 @@ func getToken(data *AttestationData) (string, error) { return "Bearer TPM" + base64.StdEncoding.EncodeToString(bytes), nil } -func getAttestationData(c *Config) (*AttestationData, []byte, error) { +func getAttestationData(c *config) (*attestationData, []byte, error) { var err error tpm, err := getTPM(c) @@ -121,7 +124,7 @@ func getAttestationData(c *Config) (*AttestationData, []byte, error) { } ek := &eks[0] - ekBytes, err := EncodeEK(ek) + ekBytes, err := encodeEK(ek) if err != nil { return nil, nil, err } @@ -131,7 +134,7 @@ func getAttestationData(c *Config) (*AttestationData, []byte, error) { return nil, nil, fmt.Errorf("marshaling AK: %w", err) } - return &AttestationData{ + return &attestationData{ EK: ekBytes, AK: ¶ms, }, aikBytes, nil diff --git a/tpm_attestor.go b/tpm_attestor.go index 632241a..205698c 100644 --- a/tpm_attestor.go +++ b/tpm_attestor.go @@ -23,21 +23,22 @@ import ( "github.com/google/certificate-transparency-go/x509" "github.com/google/go-attestation/attest" + "github.com/pkg/errors" ) -type AttestationData struct { +type attestationData struct { EK []byte AK *attest.AttestationParameters } +// Challenge represent the struct returned from the ws server, +// used to resolve the TPM challenge. type Challenge struct { EC *attest.EncryptedCredential } -type KeyData struct { - Keys []string `json:"keys"` -} - +// ChallengeResponse represent the struct returned to the ws server +// as a challenge response. type ChallengeResponse struct { Secret []byte } @@ -52,7 +53,7 @@ func getPubHash(ek *attest.EK) (string, error) { return hashEncoded, nil } -func EncodeEK(ek *attest.EK) ([]byte, error) { +func encodeEK(ek *attest.EK) ([]byte, error) { if ek.Certificate != nil { return pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", @@ -78,3 +79,36 @@ func pubBytes(ek *attest.EK) ([]byte, error) { } return data, nil } + +// DecodeEK decodes EK pem bytes to attest.EK +func DecodeEK(pemBytes []byte) (*attest.EK, error) { + block, _ := pem.Decode(pemBytes) + + if block == nil { + return nil, errors.New("invalid pemBytes") + } + + switch block.Type { + case "CERTIFICATE": + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing certificate: %v", err) + } + return &attest.EK{ + Certificate: cert, + Public: cert.PublicKey, + }, nil + + case "PUBLIC KEY": + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing ecdsa public key: %v", err) + } + + return &attest.EK{ + Public: pub, + }, nil + } + + return nil, fmt.Errorf("invalid pem type: %s", block.Type) +} diff --git a/tpm_test.go b/tpm_test.go index ea9ee0f..20af075 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -9,30 +9,6 @@ import ( . "github.com/rancher-sandbox/go-tpm/backend" ) -// In order to run this suite a swtpm socket is required. e.g.: -// swtpm socket --server type=unixio,path=/tmp/tpm-server --ctrl type=unixio,path=/tmp/tpm-ctrl --tpm2 -// SWTPM_SOCKET=/tmp/tpm-ctrl ginkgo -r ./ - -var _ = Describe("TPM with SWTPM", func() { - socket := os.Getenv("SWTPM_SOCKET") - Context("opening socket connection", func() { - // Note, this doesn't work - PIt("dials in just fine", func() { - if socket == "" { - Skip("No socket file specified") - } - - b, err := Socket(socket) - Expect(err).ToNot(HaveOccurred()) - - str, err := GetPubHash(WithCommandChannel(b)) - Expect(err).ToNot(HaveOccurred()) - - Expect(str).ToNot(BeEmpty()) - }) - }) -}) - var _ = Describe("Simulated TPM", func() { Context("opening socket connection", func() { It("dials in just fine", func() { @@ -48,7 +24,30 @@ var _ = Describe("Simulated TPM", func() { Expect(err).ToNot(HaveOccurred()) str2, err := GetPubHash(Emulated, WithSeed(1)) Expect(err).ToNot(HaveOccurred()) + str3, err := GetPubHash(Emulated, WithSeed(2)) + Expect(err).ToNot(HaveOccurred()) Expect(str).To(Equal(str2)) + Expect(str).ToNot(Equal(str3)) + }) + }) + + Context("from a socket", func() { + // In order to run this test a swtpm socket is required. e.g.: + // swtpm socket --server type=unixio,path=/tmp/tpm-server --ctrl type=unixio,path=/tmp/tpm-ctrl --tpm2 + // TPM_SOCKET=/tmp/tpm-ctrl ginkgo -r ./ + + socket := os.Getenv("TPM_SOCKET") + It("gets pubhash", func() { + if socket == "" { + Skip("No socket file specified") + } + + b, err := Socket(socket) + Expect(err).ToNot(HaveOccurred()) + + str, err := GetPubHash(WithCommandChannel(b)) + Expect(err).ToNot(HaveOccurred()) + Expect(str).ToNot(BeEmpty()) }) }) })