From 258084d04eb8950326d2a62b0471f17e55c910af Mon Sep 17 00:00:00 2001 From: juanvallejo Date: Tue, 30 May 2023 18:00:02 -0700 Subject: [PATCH] Add support for generating TPM2.0 challenges using AttestedCertifyInfo Fixes: issues/320. Adds support for generating an activation challenge using CertificationParameters. Achieves symmetry with challenge-generation in AttestationParameters, in order to provide a challenge to a TPM to activate a TPM-certified key. `attest.Activation` currently supports verifying and generating a challenge given attestationData, an EK, an AK, and a signature. In the attestationData, the CreationInfo field is used to further validate and create the resulting challenge. In this change, `attest.Certification` will now support generating a challenge given attestationData, an EK, a TPM-certified public key, and a signature, in addition to an AK used to verify the certification of the provided public key we are generating an activation challenge for. --- attest/certification.go | 79 ++++++++++++++++++++++ attest/certification_test.go | 123 +++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/attest/certification.go b/attest/certification.go index 372da04..60f632d 100644 --- a/attest/certification.go +++ b/attest/certification.go @@ -17,12 +17,14 @@ package attest import ( "bytes" "crypto" + "crypto/rand" "crypto/rsa" "errors" "fmt" "io" "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpm2/credactivation" "github.com/google/go-tpm/tpmutil" ) @@ -62,6 +64,38 @@ type VerifyOpts struct { Hash crypto.Hash } +// ActivateOpts specifies options for the key certification's challenge generation. +type ActivateOpts struct { + // EK, the endorsement key, describes an asymmetric key whose + // private key is permanently bound to the TPM. + // + // Activation will verify that the provided EK is held on the same + // TPM as the key we're certifying. However, it is the caller's responsibility to + // ensure the EK they provide corresponds to the the device which + // they are trying to associate the certified key with. + EK crypto.PublicKey + // VerifierKeyNameDigest is the name digest of the public key we're using to + // verify the certification of the tpm-generated key being activated. + // The verifier key (usually the AK) that owns this digest should be the same + // key used in VerifyOpts.Public. + // Use tpm2.Public.Name() to produce the digest for a provided key. + VerifierKeyNameDigest *tpm2.HashValue +} + +// NewActivateOpts creates options for use in generating an activation challenge for a certified key. +// The computed hash is the name digest of the public key used to verify the certification of our key. +func NewActivateOpts(verifierPubKey tpm2.Public, ek crypto.PublicKey) (*ActivateOpts, error) { + pubName, err := verifierPubKey.Name() + if err != nil { + return nil, fmt.Errorf("unable to resolve a tpm2.Public Name struct from the given public key struct: %v", err) + } + + return &ActivateOpts{ + EK: ek, + VerifierKeyNameDigest: pubName.Digest, + }, nil +} + // Verify verifies the TPM2-produced certification parameters checking whether: // - the key length is secure // - the attestation parameters matched the attested key @@ -157,6 +191,51 @@ func (p *CertificationParameters) Verify(opts VerifyOpts) error { return nil } +// Generate returns a credential activation challenge, which can be provided +// to the TPM to verify the AK parameters given are authentic & the AK +// is present on the same TPM as the EK. +// +// The caller is expected to verify the secret returned from the TPM as +// as result of calling ActivateCredential() matches the secret returned here. +// The caller should use subtle.ConstantTimeCompare to avoid potential +// timing attack vectors. +func (p *CertificationParameters) Generate(rnd io.Reader, verifyOpts VerifyOpts, activateOpts ActivateOpts) (secret []byte, ec *EncryptedCredential, err error) { + if err := p.Verify(verifyOpts); err != nil { + return nil, nil, err + } + + if activateOpts.EK == nil { + return nil, nil, errors.New("no EK provided") + } + + secret = make([]byte, activationSecretLen) + if rnd == nil { + rnd = rand.Reader + } + if _, err = io.ReadFull(rnd, secret); err != nil { + return nil, nil, fmt.Errorf("error generating activation secret: %v", err) + } + + att, err := tpm2.DecodeAttestationData(p.CreateAttestation) + if err != nil { + return nil, nil, fmt.Errorf("DecodeAttestationData() failed: %v", err) + } + + if att.Type != tpm2.TagAttestCertify { + return nil, nil, fmt.Errorf("attestation does not apply to certify data, got %x", att.Type) + } + + cred, encSecret, err := credactivation.Generate(activateOpts.VerifierKeyNameDigest, activateOpts.EK, symBlockSize, secret) + if err != nil { + return nil, nil, fmt.Errorf("credactivation.Generate() failed: %v", err) + } + + return secret, &EncryptedCredential{ + Credential: cred, + Secret: encSecret, + }, nil +} + // certify uses AK's handle and the passed signature scheme to certify the key // with the `hnd` handle. func certify(tpm io.ReadWriteCloser, hnd, akHnd tpmutil.Handle, scheme tpm2.SigScheme) (*CertificationParameters, error) { diff --git a/attest/certification_test.go b/attest/certification_test.go index 8006eea..c0f8bab 100644 --- a/attest/certification_test.go +++ b/attest/certification_test.go @@ -20,6 +20,7 @@ package attest import ( + "bytes" "crypto" "crypto/rand" "crypto/rsa" @@ -274,3 +275,125 @@ func testKeyCertification(t *testing.T, tpm *TPM) { }) } } + +func TestKeyActivationTPM20(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + ak, err := tpm.NewAK(nil) + if err != nil { + t.Fatalf("error creating a new AK using simulated TPM: %v", err) + } + akAttestParams := ak.AttestationParameters() + pub, err := tpm2.DecodePublic(akAttestParams.Public) + if err != nil { + t.Fatalf("unable to decode public struct from AK attestation params: %v", err) + } + if pub.Type != tpm2.AlgRSA { + t.Fatal("non-RSA verifying key") + } + + eks, err := tpm.EKs() + if err != nil { + t.Fatalf("unexpected error retrieving EK from tpm: %v", err) + } + + if len(eks) == 0 { + t.Fatal("expected at least one EK from the simulated TPM") + } + + pk := &rsa.PublicKey{E: int(pub.RSAParameters.Exponent()), N: pub.RSAParameters.Modulus()} + hash, err := pub.RSAParameters.Sign.Hash.Hash() + if err != nil { + t.Fatalf("unable to compute hash signature from verifying key's RSA parameters: %v", err) + } + verifyOpts := VerifyOpts{ + Public: pk, + Hash: hash, + } + + sk, err := tpm.NewKey(ak, nil) + if err != nil { + t.Fatalf("unable to create a new TPM-backed key to certify: %v", err) + } + + skCertParams := sk.CertificationParameters() + activateOpts, err := NewActivateOpts(pub, eks[0].Public) + if err != nil { + t.Fatalf("unable to create new ActivateOpts: %v", err) + } + + wrongPub, err := tpm2.DecodePublic(skCertParams.Public) + if err != nil { + t.Fatalf("unable to decode public struct from CertificationParameters: %v", err) + } + + wrongActivateOpts, err := NewActivateOpts(wrongPub, eks[0].Public) + if err != nil { + t.Fatalf("unable to create wrong ActivateOpts: %v", err) + } + + for _, test := range []struct { + name string + p *CertificationParameters + verifyOpts VerifyOpts + activateOpts ActivateOpts + generateErr error + activateErr error + }{ + { + name: "OK", + p: &skCertParams, + verifyOpts: verifyOpts, + activateOpts: *activateOpts, + generateErr: nil, + activateErr: nil, + }, + { + name: "invalid verify opts", + p: &skCertParams, + verifyOpts: VerifyOpts{}, + activateOpts: *activateOpts, + generateErr: cmpopts.AnyError, + activateErr: nil, + }, + { + name: "invalid activate opts", + p: &skCertParams, + verifyOpts: verifyOpts, + activateOpts: *wrongActivateOpts, + generateErr: nil, + activateErr: cmpopts.AnyError, + }, + } { + t.Run(test.name, func(t *testing.T) { + expectedSecret, encryptedCredentials, err := test.p.Generate(rand.Reader, test.verifyOpts, test.activateOpts) + if test.generateErr != nil { + if got, want := err, test.generateErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("p.Generate() err = %v, want = %v", got, want) + } + + return + } else if err != nil { + t.Errorf("unexpected p.Generate() error: %v", err) + return + } + + actualSecret, err := ak.ActivateCredential(tpm, *encryptedCredentials) + if test.activateErr != nil { + if got, want := err, test.activateErr; !cmp.Equal(got, want, cmpopts.EquateErrors()) { + t.Errorf("p.ActivateCredential() err = %v, want = %v", got, want) + } + + return + } else if err != nil { + t.Errorf("unexpected p.ActivateCredential() error: %v", err) + return + } + + if !bytes.Equal(expectedSecret, actualSecret) { + t.Fatalf("Unexpected bytes decoded, expected %x, but got %x", expectedSecret, actualSecret) + } + }) + } +}