Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b3a2e17
Add structure of `tpm` package
strideynet Apr 9, 2024
e7bc5c9
Add proto conversion methods
strideynet Apr 9, 2024
fa66c8c
Add tests for proto conversions
strideynet Apr 9, 2024
b1df57d
Add startup stuff for tpm sim based tests
strideynet Apr 9, 2024
861f82c
try and fail to write a fake ekcert to the tpm
strideynet Apr 9, 2024
2cede39
Working ability to write to a TPM ekcert index
strideynet Apr 9, 2024
8642262
Tidy up
strideynet Apr 9, 2024
60d344e
Add finishing touches to test and add godocs
strideynet Apr 10, 2024
28c70e2
Go mod tidy
strideynet Apr 10, 2024
d7776a8
Appease linter
strideynet Apr 10, 2024
0b63cce
Remove incorrectly copied comment
strideynet Apr 10, 2024
ebcd519
Tidy up line wrapping
strideynet Apr 10, 2024
5493ada
Add license header
strideynet Apr 10, 2024
283b450
Update lib/tpm/tpm.go
strideynet Apr 10, 2024
f0bf32a
Update lib/tpm/tpm_simulator_test.go
strideynet Apr 10, 2024
a0d9bb8
Update lib/tpm/validate.go
strideynet Apr 10, 2024
6d6669d
Update lib/tpm/tpm.go
strideynet Apr 11, 2024
5a465cd
Update lib/tpm/tpm_simulator_test.go
strideynet Apr 11, 2024
fd3c86b
Update lib/tpm/tpm_simulator_test.go
strideynet Apr 11, 2024
b92e60d
Update lib/tpm/tpm_simulator_test.go
strideynet Apr 11, 2024
8b76ae7
Avoid managing closure in the attestWithTPM func
strideynet Apr 11, 2024
d2f821f
Use ekCertSerialHex const
strideynet Apr 11, 2024
481369d
Simpler JoinAuditAttributes method
strideynet Apr 11, 2024
d9f8ebf
Add missing err return
strideynet Apr 11, 2024
e996c1e
Add remark on the nvram rsa ekcert index
strideynet Apr 11, 2024
dc9bace
Update lib/tpm/tpm_simulator_test.go
strideynet Apr 11, 2024
018d4e0
Add subtests
strideynet Apr 11, 2024
6ba1881
Clarify in hex
strideynet Apr 11, 2024
0eab104
Switch to testing exported iface
strideynet Apr 11, 2024
e3ae0a2
Use x509.CertPool and switch to testing public APi
strideynet Apr 11, 2024
5641b58
Remove overly cautious check
strideynet Apr 11, 2024
45889aa
Validate Validate params
strideynet Apr 11, 2024
4e77afd
Reuse strings builder when handling an odd number of hex digits
strideynet Apr 11, 2024
45e598c
Switch to gocmp and struct for ekcert
strideynet Apr 11, 2024
7bf262b
Use return struct for Attest
strideynet Apr 11, 2024
2f196f7
Avoid marshalling PKIX key twice
strideynet Apr 11, 2024
158ab14
Update lib/tpm/validate.go
strideynet Apr 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we name the file and methods here in a way that reflects that it works with api/client/proto protos?

If we were to do similar conversions from Device Trust, would the conversions live here or inside the Device Trust Service?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hm - do you have a suggestion ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be honest - not really. :)

Do you think that's something we would do? Refactor device trust to use this package? (We don't have to call all the shots now either.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Potentially - the protos are basically identical for the basic credential activation. The difference is that the Device Trust also requires a platform attestation of the PCRs which we don't require for tpm joining.

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 {
Comment thread
codingllama marked this conversation as resolved.
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)
Comment thread
codingllama marked this conversation as resolved.
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)
Comment thread
codingllama marked this conversation as resolved.
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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would a package-level logger work here? I'm not super fond of the logger parameters.

Same for others.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤔 tough one - I'm very much in the camp of treating loggers like all other dependencies and injecting them rather than using global loggers. It makes it way easier to add fields which should be included in all logs emitted by child functions. Another thought is I'd love to fix how badly garbled our parallel test log output is one day, and the only way to do that is to avoid global loggers and to be able to inject a logger which properly attributes logs to the test they belong to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The peeve I have with this is that it "messes up" the function signature. Taking to an extreme then almost every standalone func suddenly takes a logger.

I suppose this is a reflection of us not having a good way to "inject" orthogonal concerns like loggers. A slightly better way to solve this is tying the methods to a struct and pushing the logger to it.

Feel free to resolve this thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

almost every standalone func suddenly takes a logger.

Perhaps I'm a bit of an extremist here - but I do unironically believe that this is how things should be done, logger injected as a parameter or as a field struct. In my experience with Go so far, Globals have repeatedly caused large amounts of pain, whereas DI is more of a minor annoyance 😓

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 {
Comment thread
strideynet marked this conversation as resolved.
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