Skip to content

Commit

Permalink
Add tpm package with Attestation/Validation functionality (#40351)
Browse files Browse the repository at this point in the history
* Add structure of `tpm` package

* Add proto conversion methods

* Add tests for proto conversions

* Add startup stuff for tpm sim based tests

* try and fail to write a fake ekcert to the tpm

* Working ability to write to a TPM ekcert index

* Tidy up

* Add finishing touches to test and add godocs

* Go mod tidy

* Appease linter

* Remove incorrectly copied comment

* Tidy up line wrapping

* Add license header

* Update lib/tpm/tpm.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/tpm_simulator_test.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/validate.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/tpm.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/tpm_simulator_test.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/tpm_simulator_test.go

Co-authored-by: Alan Parra <[email protected]>

* Update lib/tpm/tpm_simulator_test.go

Co-authored-by: Alan Parra <[email protected]>

* Avoid managing closure in the attestWithTPM func

* Use ekCertSerialHex const

* Simpler JoinAuditAttributes method

Co-authored-by: Alan Parra <[email protected]>

* Add missing err return

* Add remark on the nvram rsa ekcert index

* Update lib/tpm/tpm_simulator_test.go

Co-authored-by: Alan Parra <[email protected]>

* Add subtests

* Clarify in hex

* Switch to testing exported iface

* Use x509.CertPool and switch to testing public APi

* Remove overly cautious check

* Validate Validate params

* Reuse strings builder when handling an odd number of hex digits

* Switch to gocmp and struct for ekcert

* Use return struct for Attest

* Avoid marshalling PKIX key twice

* Update lib/tpm/validate.go

Co-authored-by: Alan Parra <[email protected]>

---------

Co-authored-by: Alan Parra <[email protected]>
  • Loading branch information
strideynet and codingllama authored Apr 11, 2024
1 parent 65c8861 commit 521f692
Show file tree
Hide file tree
Showing 6 changed files with 803 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.19.1
github.com/google/go-querystring v1.1.0
github.com/google/go-tpm v0.9.0
github.com/google/go-tpm-tools v0.4.4
github.com/google/renameio/v2 v2.0.0
github.com/google/safetext v0.0.0-20240104143208-7a7d9b3d812f
Expand Down Expand Up @@ -352,7 +353,6 @@ require (
github.com/google/flatbuffers v24.3.7+incompatible // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-configfs-tsm v0.2.2 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
Expand Down
74 changes: 74 additions & 0 deletions lib/tpm/proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package tpm

import (
"github.com/google/go-attestation/attest"

"github.com/gravitational/teleport/api/client/proto"
)

// AttestationParametersToProto converts an attest.AttestationParameters to
// its protobuf representation.
func AttestationParametersToProto(in attest.AttestationParameters) *proto.TPMAttestationParameters {
return &proto.TPMAttestationParameters{
Public: in.Public,
CreateData: in.CreateData,
CreateAttestation: in.CreateAttestation,
CreateSignature: in.CreateSignature,
}
}

// AttestationParametersFromProto extracts an attest.AttestationParameters from
// its protobuf representation.
func AttestationParametersFromProto(in *proto.TPMAttestationParameters) attest.AttestationParameters {
if in == nil {
return attest.AttestationParameters{}
}
return attest.AttestationParameters{
Public: in.Public,
CreateData: in.CreateData,
CreateAttestation: in.CreateAttestation,
CreateSignature: in.CreateSignature,
}
}

// EncryptedCredentialToProto converts an attest.EncryptedCredential to
// its protobuf representation.
func EncryptedCredentialToProto(in *attest.EncryptedCredential) *proto.TPMEncryptedCredential {
if in == nil {
return nil
}
return &proto.TPMEncryptedCredential{
CredentialBlob: in.Credential,
Secret: in.Secret,
}
}

// EncryptedCredentialFromProto extracts an attest.EncryptedCredential from
// its protobuf representation.
func EncryptedCredentialFromProto(in *proto.TPMEncryptedCredential) *attest.EncryptedCredential {
if in == nil {
return nil
}
return &attest.EncryptedCredential{
Credential: in.CredentialBlob,
Secret: in.Secret,
}
}
52 changes: 52 additions & 0 deletions lib/tpm/proto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package tpm

import (
"testing"

"github.com/google/go-attestation/attest"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/utils"
)

func TestAttestationParametersProto(t *testing.T) {
want := attest.AttestationParameters{
Public: []byte("public"),
CreateData: []byte("create_data"),
CreateAttestation: []byte("create_attestation"),
CreateSignature: []byte("create_signature"),
}
pb := AttestationParametersToProto(want)
clonedPb := utils.CloneProtoMsg(pb)
got := AttestationParametersFromProto(clonedPb)
require.Equal(t, want, got)
}

func TestEncryptedCredentialProto(t *testing.T) {
want := &attest.EncryptedCredential{
Credential: []byte("encrypted_credential"),
Secret: []byte("secret"),
}
pb := EncryptedCredentialToProto(want)
clonedPb := utils.CloneProtoMsg(pb)
got := EncryptedCredentialFromProto(clonedPb)
require.Equal(t, want, got)
}
231 changes: 231 additions & 0 deletions lib/tpm/tpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package tpm

import (
"context"
"crypto/sha256"
"crypto/x509"
"fmt"
"log/slog"
"math/big"
"strings"

"github.com/google/go-attestation/attest"
"github.com/gravitational/trace"
"go.opentelemetry.io/otel"
)

var tracer = otel.Tracer("github.com/gravitational/teleport/lib/tpm")

// serialString converts a serial number into a readable colon-delimited hex
// string thats user-readable e.g ab:ab:ab:ff:ff:ff
func serialString(serial *big.Int) string {
hex := serial.Text(16)

out := strings.Builder{}
// Handle odd-sized strings.
if len(hex)%2 == 1 {
out.WriteRune('0')
out.WriteRune(rune(hex[0]))
if len(hex) > 1 {
out.WriteRune(':')
}
hex = hex[1:]
}
for i := 0; i < len(hex); i += 2 {
if i != 0 {
out.WriteString(":")
}
out.WriteString(hex[i : i+2])
}
return out.String()
}

// hashEKPub hashes the public part of an EK key. The key is hashed with SHA256,
// and returned as a hexadecimal string.
func hashEKPub(pkixPublicKey []byte) (string, error) {
hashed := sha256.Sum256(pkixPublicKey)
return fmt.Sprintf("%x", hashed), nil
}

// QueryRes is the result of the TPM query performed by Query.
type QueryRes struct {
// EKPub is the PKIX marshaled public part of the EK.
EKPub []byte
// EKPubHash is the SHA256 hash of the PKIX marshaled EKPub in hexadecimal
// format.
EKPubHash string
// EKCert holds the information about the EKCert if present. If nil, the
// TPM does not have an EKCert.
EKCert *QueryEKCert
}

// QueryEKCert contains the EKCert information if present.
type QueryEKCert struct {
// Raw is the ASN.1 DER encoded EKCert.
Raw []byte
// SerialNumber is the serial number of the EKCert represented as a colon
// delimited hex string.
SerialNumber string
}

// Query returns information about the TPM on a system, including the
// EKPubHash and EKCertSerial which are needed to configure TPM joining.
func Query(ctx context.Context, log *slog.Logger) (*QueryRes, error) {
ctx, span := tracer.Start(ctx, "Query")
defer span.End()

tpm, err := attest.OpenTPM(&attest.OpenConfig{
TPMVersion: attest.TPMVersion20,
})
if err != nil {
return nil, trace.Wrap(err)
}
defer func() {
if err := tpm.Close(); err != nil {
log.WarnContext(
ctx,
"Failed to close TPM",
slog.String("error", err.Error()),
)
}
}()
return QueryWithTPM(ctx, log, tpm)
}

// QueryWithTPM is similar to Query, but accepts an already opened TPM.
func QueryWithTPM(
ctx context.Context, log *slog.Logger, tpm *attest.TPM,
) (*QueryRes, error) {
ctx, span := tracer.Start(ctx, "QueryWithTPM")
defer span.End()

data := &QueryRes{}

eks, err := tpm.EKs()
if err != nil {
return nil, trace.Wrap(err, "querying EKs")
}

// The first EK returned by `go-attestation` will be an RSA based EK key or
// EK cert. On Windows, ECC certs may also be returned following this. At
// this time, we are only interested in RSA certs, so we just consider the
// first thing returned.
ekPub, err := x509.MarshalPKIXPublicKey(eks[0].Public)
if err != nil {
return nil, trace.Wrap(err)
}
data.EKPub = ekPub
data.EKPubHash, err = hashEKPub(ekPub)
if err != nil {
return nil, trace.Wrap(err, "hashing ekpub")
}

if eks[0].Certificate != nil {
data.EKCert = &QueryEKCert{
Raw: eks[0].Certificate.Raw,
SerialNumber: serialString(eks[0].Certificate.SerialNumber),
}
}
log.DebugContext(ctx, "Successfully queried TPM", "data", data)
return data, nil
}

// Attestation holds the information necessary to perform a TPM join to a
// Teleport cluster.
type Attestation struct {
// Data holds the queried information about the EK and EKCert if present.
Data QueryRes
// AttestParams holds the attestation parameters for the AK created for
// this join ceremony.
AttestParams attest.AttestationParameters
// Solve is a function that should be called when the encrypted credential
// challenge is received from the server.
Solve func(ec *attest.EncryptedCredential) ([]byte, error)
}

// Attest provides the information necessary to perform a TPM join to a Teleport
// cluster. It returns a solve function which should be called when the
// encrypted credential challenge is received from the server.
// The Close function must be called if Attest returns in a non-error state.
func Attest(ctx context.Context, log *slog.Logger) (
att *Attestation,
close func() error,
err error,
) {
ctx, span := tracer.Start(ctx, "Attest")
defer span.End()

tpm, err := attest.OpenTPM(&attest.OpenConfig{
TPMVersion: attest.TPMVersion20,
})
if err != nil {
return nil, nil, trace.Wrap(err)
}
defer func() {
if err != nil {
if err := tpm.Close(); err != nil {
log.WarnContext(
ctx,
"Failed to close TPM",
slog.String("error", err.Error()),
)
}
}
}()

att, err = AttestWithTPM(ctx, log, tpm)
if err != nil {
return nil, nil, trace.Wrap(err, "attesting with TPM")
}

return att, tpm.Close, nil

}

// AttestWithTPM is similar to Attest, but accepts an already opened TPM.
func AttestWithTPM(ctx context.Context, log *slog.Logger, tpm *attest.TPM) (
att *Attestation,
err error,
) {
ctx, span := tracer.Start(ctx, "AttestWithTPM")
defer span.End()

queryData, err := QueryWithTPM(ctx, log, tpm)
if err != nil {
return nil, trace.Wrap(err, "querying TPM")
}

// Create AK and calculate attestation parameters.
ak, err := tpm.NewAK(&attest.AKConfig{})
if err != nil {
return nil, trace.Wrap(err, "creating ak")
}
log.DebugContext(ctx, "Successfully generated AK for TPM")

return &Attestation{
Data: *queryData,
AttestParams: ak.AttestationParameters(),
Solve: func(ec *attest.EncryptedCredential) ([]byte, error) {
log.DebugContext(ctx, "Solving credential challenge")
return ak.ActivateCredential(tpm, *ec)
},
}, nil
}
Loading

0 comments on commit 521f692

Please sign in to comment.