-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add tpm package with Attestation/Validation functionality
#40351
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
Changes from all commits
b3a2e17
e7bc5c9
fa66c8c
b1df57d
861f82c
2cede39
8642262
60d344e
28c70e2
d7776a8
0b63cce
ebcd519
5493ada
283b450
f0bf32a
a0d9bb8
6d6669d
5a465cd
fd3c86b
b92e60d
8b76ae7
d2f821f
481369d
d9f8ebf
e996c1e
dc9bace
018d4e0
6ba1881
0eab104
e3ae0a2
5641b58
45889aa
4e77afd
45e598c
7bf262b
2f196f7
158ab14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
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, | ||
| } | ||
| } | ||
| 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) | ||
|
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) | ||
|
codingllama marked this conversation as resolved.
|
||
| require.Equal(t, want, got) | ||
| } | ||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 { | ||
|
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 | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.