diff --git a/go.mod b/go.mod
index cc338f10b1e79..90363919b4658 100644
--- a/go.mod
+++ b/go.mod
@@ -101,7 +101,8 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.17.0
github.com/google/go-querystring v1.1.0
- github.com/google/go-tpm-tools v0.4.2
+ 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
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@@ -351,7 +352,7 @@ require (
github.com/google/certificate-transparency-go v1.1.7 // indirect
github.com/google/flatbuffers v23.1.21+incompatible // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
- github.com/google/go-tpm v0.9.0 // indirect
+ github.com/google/go-configfs-tsm v0.2.2 // 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
diff --git a/go.sum b/go.sum
index 17bb7d431b274..cef5a1a81dd85 100644
--- a/go.sum
+++ b/go.sum
@@ -731,6 +731,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98=
+github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo=
github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
@@ -740,12 +742,12 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-sev-guest v0.9.3 h1:GOJ+EipURdeWFl/YYdgcCxyPeMgQUWlI056iFkBD8UU=
github.com/google/go-sev-guest v0.9.3/go.mod h1:hc1R4R6f8+NcJwITs0L90fYWTsBpd1Ix+Gur15sqHDs=
-github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33 h1:lRlUusuieEuqljjihCXb+Mr73VNitOYPJYWXzJKtBWs=
-github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33/go.mod h1:84ut3oago/BqPXD4ppiGXdkZNW3WFPkcyAO4my2hXdY=
+github.com/google/go-tdx-guest v0.3.1 h1:gl0KvjdsD4RrJzyLefDOvFOUH3NAJri/3qvaL5m83Iw=
+github.com/google/go-tdx-guest v0.3.1/go.mod h1:/rc3d7rnPykOPuY8U9saMyEps0PZDThLk/RygXm04nE=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
-github.com/google/go-tpm-tools v0.4.2 h1:iyaCPKt2N5Rd0yz0G8ANa022SgCNZkMpp+db6QELtvI=
-github.com/google/go-tpm-tools v0.4.2/go.mod h1:fGUDZu4tw3V4hUVuFHmiYgRd0c58/IXivn9v3Ea/ck4=
+github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
+github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/lib/tpm/proto.go b/lib/tpm/proto.go
new file mode 100644
index 0000000000000..d543c24f23316
--- /dev/null
+++ b/lib/tpm/proto.go
@@ -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 .
+ */
+
+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,
+ }
+}
diff --git a/lib/tpm/proto_test.go b/lib/tpm/proto_test.go
new file mode 100644
index 0000000000000..e0a9eca31333f
--- /dev/null
+++ b/lib/tpm/proto_test.go
@@ -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 .
+ */
+
+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)
+}
diff --git a/lib/tpm/tpm.go b/lib/tpm/tpm.go
new file mode 100644
index 0000000000000..e83000535bf53
--- /dev/null
+++ b/lib/tpm/tpm.go
@@ -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 .
+ */
+
+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
+}
diff --git a/lib/tpm/tpm_simulator_test.go b/lib/tpm/tpm_simulator_test.go
new file mode 100644
index 0000000000000..a71fce746569d
--- /dev/null
+++ b/lib/tpm/tpm_simulator_test.go
@@ -0,0 +1,244 @@
+//go:build tpmsimulator
+
+/*
+ * 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 .
+ */
+
+package tpm_test
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "io"
+ "log/slog"
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/google/go-attestation/attest"
+ gocmp "github.com/google/go-cmp/cmp"
+ tpmsimulator "github.com/google/go-tpm-tools/simulator"
+ "github.com/google/go-tpm/legacy/tpm2"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/api/fixtures"
+ "github.com/gravitational/teleport/lib/tpm"
+)
+
+// fakeCmdChannel is used to inject the TPM simulator into `go-attestation`'s
+// TPM wrapper.
+type fakeCmdChannel struct {
+ io.ReadWriteCloser
+}
+
+// MeasurementLog implements CommandChannelTPM20.
+func (cc *fakeCmdChannel) MeasurementLog() ([]byte, error) {
+ // Nothing to do here - we don't use the measurement log for tpm joining.
+ return nil, nil
+}
+
+func writeEKCertToTPM(t *testing.T, sim *tpmsimulator.Simulator, data []byte) {
+ // As per TCG Credential Profile EK for TPM 2.0, 2.2.1.4, the RSA 2048
+ // EK certificate is stored in the TPM's NV index 0x1c00002.
+ const nvramRSAEKCertIndex = 0x1c00002
+ err := tpm2.NVDefineSpace(
+ sim,
+ tpm2.HandlePlatform, // Using Platform Authorization.
+ nvramRSAEKCertIndex,
+ "", // As this is the simulator, there isn't a password for Platform Authorization.
+ "", // We do not configure a password for this index. This allows it to be read using the NV index as the auth handle.
+ nil,
+ tpm2.AttrPPWrite| // Allows this NV index to be written with platform authorization.
+ tpm2.AttrPPRead| // Allows this NV index to be read with platform authorization.
+ tpm2.AttrPlatformCreate| // Marks this index as created by the Platform
+ tpm2.AttrAuthRead, // Allows the nv index to be used as an auth handle to read itself.
+
+ uint16(len(data)),
+ )
+ require.NoError(t, err)
+
+ err = tpm2.NVWrite(
+ sim,
+ tpm2.HandlePlatform,
+ nvramRSAEKCertIndex,
+ "",
+ data,
+ 0,
+ )
+ require.NoError(t, err)
+}
+
+// To include tests based on this simulator, use the `tpmsimulator` build tag.
+// This requires openssl libraries to be installed on the machine and findable
+// by the compiler. On macOS:
+// brew install openssl
+// export C_INCLUDE_PATH="$(brew --prefix openssl)/include"
+// export LIBRARY_PATH="$(brew --prefix openssl)/lib"
+// go test ./lib/tpm -run TestWithSimulator -tags tpmsimulator
+
+func TestWithSimulator(t *testing.T) {
+ t.Parallel()
+ ctx := context.Background()
+ log := slog.Default()
+
+ ca := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ BasicConstraintsValid: true,
+ IsCA: true,
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(time.Hour),
+ }
+ caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
+ require.NoError(t, err)
+ caBytes, err := x509.CreateCertificate(
+ rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey,
+ )
+ require.NoError(t, err)
+ caPEM := pem.EncodeToMemory(
+ &pem.Block{Type: "CERTIFICATE", Bytes: caBytes},
+ )
+ caPool := x509.NewCertPool()
+ require.True(t, caPool.AppendCertsFromPEM(caPEM))
+ wrongCAPool := x509.NewCertPool()
+ require.True(t, wrongCAPool.AppendCertsFromPEM([]byte(fixtures.TLSCACertPEM)))
+
+ sim, err := tpmsimulator.GetWithFixedSeedInsecure(0)
+ require.NoError(t, err)
+ // This is the EKPubHash that results from the EK generated with the seed 0.
+ const wantEKPubHash = "1b5bbe2e96054f7bc34ebe7ba9a4a9eac5611c6879285ceff6094fa556af485c"
+
+ attestTPM, err := attest.OpenTPM(&attest.OpenConfig{
+ TPMVersion: attest.TPMVersion20,
+ CommandChannel: &fakeCmdChannel{ReadWriteCloser: sim},
+ })
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ assert.NoError(t, attestTPM.Close(), "TPM simulator close errored")
+ })
+
+ tpmEKs, err := attestTPM.EKs()
+ require.NoError(t, err)
+ wantEKPub, err := x509.MarshalPKIXPublicKey(tpmEKs[0].Public)
+ require.NoError(t, err)
+
+ t.Run("Without EKCert", func(t *testing.T) {
+ att, err := tpm.AttestWithTPM(ctx, log, attestTPM)
+ require.NoError(t, err)
+
+ // Check QueryRes looks right.
+ assert.Empty(t, gocmp.Diff(tpm.QueryRes{
+ EKPubHash: wantEKPubHash,
+ EKPub: wantEKPub,
+ }, att.Data))
+
+ t.Run("Success", func(t *testing.T) {
+ validated, err := tpm.Validate(ctx, log, tpm.ValidateParams{
+ EKKey: att.Data.EKPub,
+ AttestParams: att.AttestParams,
+ Solve: att.Solve,
+ })
+ require.NoError(t, err)
+ assert.Empty(t, gocmp.Diff(&tpm.ValidatedTPM{
+ EKPubHash: wantEKPubHash,
+ EKCertVerified: false,
+ }, validated))
+ })
+ t.Run("Failure due to missing EKCert", func(t *testing.T) {
+ _, err = tpm.Validate(ctx, log, tpm.ValidateParams{
+ EKKey: att.Data.EKPub,
+ AttestParams: att.AttestParams,
+ Solve: att.Solve,
+ AllowedCAs: caPool,
+ })
+ assert.ErrorContains(t, err, "tpm did not provide an EKCert to validate against allowed CAs")
+ })
+ })
+
+ // Write fake EKCert to the TPM
+ const ekCertSerialNum = 1337133713371337
+ const ekCertSerialHex = "04:c0:1d:b4:00:b0:c9"
+ fakeEKCert := &x509.Certificate{
+ SerialNumber: big.NewInt(ekCertSerialNum),
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(time.Hour),
+ }
+ fakeEKBytes, err := x509.CreateCertificate(
+ rand.Reader, fakeEKCert, ca, tpmEKs[0].Public, caPrivKey,
+ )
+ require.NoError(t, err)
+ writeEKCertToTPM(t, sim, fakeEKBytes)
+
+ t.Run("With EKCert", func(t *testing.T) {
+ att, err := tpm.AttestWithTPM(ctx, log, attestTPM)
+ require.NoError(t, err)
+
+ // Check queryRes looks right.
+ assert.Empty(t, gocmp.Diff(tpm.QueryRes{
+ EKPubHash: wantEKPubHash,
+ EKPub: wantEKPub,
+ EKCert: &tpm.QueryEKCert{
+ Raw: fakeEKBytes,
+ SerialNumber: ekCertSerialHex,
+ },
+ }, att.Data))
+
+ t.Run("Success without CAs", func(t *testing.T) {
+ validated, err := tpm.Validate(ctx, log, tpm.ValidateParams{
+ EKKey: att.Data.EKPub,
+ EKCert: att.Data.EKCert.Raw,
+ AttestParams: att.AttestParams,
+ Solve: att.Solve,
+ })
+ require.NoError(t, err)
+ assert.Empty(t, gocmp.Diff(&tpm.ValidatedTPM{
+ EKPubHash: wantEKPubHash,
+ EKCertVerified: false,
+ EKCertSerial: ekCertSerialHex,
+ }, validated))
+ })
+ t.Run("Success with CAs", func(t *testing.T) {
+ validated, err := tpm.Validate(ctx, log, tpm.ValidateParams{
+ EKKey: att.Data.EKPub,
+ EKCert: att.Data.EKCert.Raw,
+ AttestParams: att.AttestParams,
+ Solve: att.Solve,
+ AllowedCAs: caPool,
+ })
+ require.NoError(t, err)
+ assert.Empty(t, gocmp.Diff(&tpm.ValidatedTPM{
+ EKPubHash: wantEKPubHash,
+ EKCertVerified: true,
+ EKCertSerial: ekCertSerialHex,
+ }, validated))
+ })
+ t.Run("Failure with wrong CA", func(t *testing.T) {
+ _, err := tpm.Validate(ctx, log, tpm.ValidateParams{
+ EKKey: att.Data.EKPub,
+ EKCert: att.Data.EKCert.Raw,
+ AttestParams: att.AttestParams,
+ Solve: att.Solve,
+ // Some random CA that won't match the EKCert.
+ AllowedCAs: wrongCAPool,
+ })
+ assert.ErrorContains(t, err, "certificate signed by unknown authority")
+ })
+ })
+}
diff --git a/lib/tpm/validate.go b/lib/tpm/validate.go
new file mode 100644
index 0000000000000..d0c9b74736342
--- /dev/null
+++ b/lib/tpm/validate.go
@@ -0,0 +1,201 @@
+/*
+ * 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 .
+ */
+
+package tpm
+
+import (
+ "context"
+ "crypto"
+ "crypto/subtle"
+ "crypto/x509"
+ "log/slog"
+
+ "github.com/google/go-attestation/attest"
+ "github.com/gravitational/trace"
+)
+
+// ValidateParams are the parameters required to validate a TPM.
+type ValidateParams struct {
+ // EKCert should be the EK certificate in ASN.1 DER format. At least one of
+ // EKCert or EKKey must be provided.
+ EKCert []byte
+ // EKKey should be the public part of the EK key in PKIX format. At least
+ // one of EKCert or EKKey must be provided. If EKCert is provided, EKKey
+ // will be ignored.
+ EKKey []byte
+ // AttestParams are the parameters required to attest the TPM provided by
+ // the client. These relate to an AK that has been generated for this
+ // ceremony.
+ AttestParams attest.AttestationParameters
+ // Solve is the function that Validate should call when it has prepared the
+ // challenge and needs the remote TPM to solve it.
+ Solve func(ec *attest.EncryptedCredential) ([]byte, error)
+ // AllowedCAs is a pool of PEM encoded CAs that are allowed to sign the
+ // EKCert. If this value is nil, the EKCert will not be verified.
+ AllowedCAs *x509.CertPool
+}
+
+// ValidatedTPM is returned by Validate and contains the validated information
+// about the remote TPM.
+type ValidatedTPM struct {
+ // EKPubHash is the SHA256 hash of the PKIX marshaled EKPub in hex format.
+ EKPubHash string `json:"ek_pub_hash"`
+ // EKCertSerial is the serial number of the EK cert represented as a colon
+ // delimited hex string. If there is no EKCert, this field will be empty.
+ EKCertSerial string `json:"ek_cert_serial,omitempty"`
+ // EKCertVerified is true if the EKCert was verified against the allowed
+ // CAs.
+ EKCertVerified bool `json:"ek_cert_verified"`
+}
+
+// JoinAuditAttributes returns a series of attributes that can be inserted into
+// audit events related to a specific join.
+func (c *ValidatedTPM) JoinAuditAttributes() (map[string]interface{}, error) {
+ return map[string]interface{}{
+ "ek_pub_hash": c.EKPubHash,
+ "ek_cert_serial": c.EKCertSerial,
+ "ek_cert_verified": c.EKCertVerified,
+ }, nil
+}
+
+// Validate takes the parameters from a remote TPM and performs the necessary
+// initial checks before then generating an encrypted credential challenge for
+// the client to solve in a credential activation ceremony. This allows us to
+// verify that the client possesses the TPM corresponding to the EK public key
+// or certificate presented by the client.
+func Validate(
+ ctx context.Context, log *slog.Logger, params ValidateParams,
+) (*ValidatedTPM, error) {
+ ctx, span := tracer.Start(ctx, "Validate")
+ defer span.End()
+
+ // Validate params
+ switch {
+ case params.Solve == nil:
+ return nil, trace.BadParameter("solve must be non-nil")
+ case params.EKCert == nil && params.EKKey == nil:
+ return nil, trace.BadParameter("at least one of EKCert or EKKey must be provided")
+ }
+
+ ekCert, ekPub, err := parseEK(ctx, params)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ validated := &ValidatedTPM{}
+ ekPubPKIX, err := x509.MarshalPKIXPublicKey(ekPub)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ validated.EKPubHash, err = hashEKPub(ekPubPKIX)
+ if err != nil {
+ return validated, trace.Wrap(err, "hashing EK public key")
+ }
+ if ekCert != nil {
+ validated.EKCertSerial = serialString(ekCert.SerialNumber)
+ }
+
+ if params.AllowedCAs != nil {
+ if err := verifyEKCert(ctx, params.AllowedCAs, ekCert); err != nil {
+ return validated, trace.Wrap(err, "verifying EK cert")
+ }
+ validated.EKCertVerified = true
+ }
+
+ activationParameters := attest.ActivationParameters{
+ TPMVersion: attest.TPMVersion20,
+ AK: params.AttestParams,
+ EK: ekPub,
+ }
+ // The generate method completes initial validation that provides the
+ // following assurances:
+ // - The attestation key is of a secure length
+ // - The attestation key is marked as created within a TPM
+ // - The attestation key is marked as restricted (e.g cannot be used to
+ // sign or decrypt external data)
+ // When the returned challenge is solved by the TPM using ActivateCredential
+ // the following additional assurance is given:
+ // - The attestation key resides in the same TPM as the endorsement key
+ solution, encryptedCredential, err := activationParameters.Generate()
+ if err != nil {
+ return validated, trace.Wrap(err, "generating credential activation challenge")
+ }
+ clientSolution, err := params.Solve(encryptedCredential)
+ if err != nil {
+ return validated, trace.Wrap(err, "asking client to perform credential activation")
+ }
+ if subtle.ConstantTimeCompare(clientSolution, solution) != 1 {
+ return validated, trace.BadParameter("invalid credential activation solution")
+ }
+
+ return validated, nil
+}
+
+func parseEK(
+ ctx context.Context, params ValidateParams,
+) (*x509.Certificate, crypto.PublicKey, error) {
+ _, span := tracer.Start(ctx, "parseEK")
+ defer span.End()
+
+ ekCertPresent := len(params.EKCert) > 0
+ ekKeyPresent := len(params.EKKey) > 0
+ switch {
+ case ekCertPresent:
+ ekCert, err := attest.ParseEKCertificate(params.EKCert)
+ if err != nil {
+ return nil, nil, trace.Wrap(err, "parsing EK cert")
+ }
+ return ekCert, ekCert.PublicKey, nil
+ case ekKeyPresent:
+ ekPub, err := x509.ParsePKIXPublicKey(params.EKKey)
+ if err != nil {
+ return nil, nil, trace.Wrap(err, "parsing EK key")
+ }
+ return nil, ekPub, nil
+ default:
+ return nil, nil, trace.BadParameter("either EK cert or EK key must be provided")
+ }
+}
+
+func verifyEKCert(
+ ctx context.Context,
+ allowedCAs *x509.CertPool,
+ ekCert *x509.Certificate,
+) error {
+ _, span := tracer.Start(ctx, "verifyEKCert")
+ defer span.End()
+
+ if ekCert == nil {
+ return trace.BadParameter("tpm did not provide an EKCert to validate against allowed CAs")
+ }
+
+ // Validate EKCert against CA pool
+ _, err := ekCert.Verify(x509.VerifyOptions{
+ Roots: allowedCAs,
+ KeyUsages: []x509.ExtKeyUsage{
+ // Go's x509 Verification doesn't support the EK certificate
+ // ExtKeyUsage (http://oid-info.com/get/2.23.133.8.1), so we
+ // allow any.
+ x509.ExtKeyUsageAny,
+ },
+ })
+ if err != nil {
+ return trace.Wrap(err, "verifying EK cert")
+ }
+ return nil
+}