From 8c938365cc936a3112166ae17d03e0b177976330 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 1 Nov 2023 18:53:04 -0700 Subject: [PATCH 1/6] Add signer implementation to tss2.TPPKey This PR adds a signer implementation for TSS2 keys, it also adds new methods to marshal those keys to PEM. --- tpm/tss2/encode.go | 62 ++++++++++++ tpm/tss2/encode_test.go | 160 +++++++++++++++++++++++++++++ tpm/tss2/other_test.go | 14 +++ tpm/tss2/signer.go | 191 +++++++++++++++++++++++++++++++++++ tpm/tss2/signer_test.go | 199 +++++++++++++++++++++++++++++++++++++ tpm/tss2/simulator_test.go | 20 ++++ tpm/tss2/tpm_test.go | 19 ++++ 7 files changed, 665 insertions(+) create mode 100644 tpm/tss2/encode.go create mode 100644 tpm/tss2/encode_test.go create mode 100644 tpm/tss2/other_test.go create mode 100644 tpm/tss2/signer.go create mode 100644 tpm/tss2/signer_test.go create mode 100644 tpm/tss2/simulator_test.go create mode 100644 tpm/tss2/tpm_test.go diff --git a/tpm/tss2/encode.go b/tpm/tss2/encode.go new file mode 100644 index 00000000..df5b50e1 --- /dev/null +++ b/tpm/tss2/encode.go @@ -0,0 +1,62 @@ +package tss2 + +import ( + "encoding/pem" + + "github.com/google/go-tpm/legacy/tpm2" +) + +// TPMOption is the type used to modify a [TPMKey]. +type TPMOption func(*TPMKey) + +// New creates a new [TPMKey] with the given public and private keys. +func New(pub, priv []byte, opts ...TPMOption) *TPMKey { + key := &TPMKey{ + Type: oidLoadableKey, + EmptyAuth: true, + Parent: int(tpm2.HandleOwner), + PublicKey: integrityPrefix(pub), + PrivateKey: integrityPrefix(priv), + } + for _, fn := range opts { + fn(key) + } + return key +} + +// Encode encodes the [TPMKey] returns a [*pem.Block]. +func (k *TPMKey) Encode() (*pem.Block, error) { + b, err := MarshalPrivateKey(k) + if err != nil { + return nil, err + } + return &pem.Block{ + Type: "TSS2 PRIVATE KEY", + Bytes: b, + }, nil +} + +// EncodeToMemory encodes the [TPMKey] and returns an encoded PEM block. +func (k *TPMKey) EncodeToMemory() ([]byte, error) { + block, err := k.Encode() + if err != nil { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// Encode encodes the given public and private key and returns a [*pem.Block]. +func Encode(pub, priv []byte, opts ...TPMOption) (*pem.Block, error) { + return New(pub, priv, opts...).Encode() +} + +// EncodeToMemory encodes the given public and private key and returns an +// encoded PEM block. +func EncodeToMemory(pub, priv []byte, opts ...TPMOption) ([]byte, error) { + return New(pub, priv, opts...).EncodeToMemory() +} + +func integrityPrefix(b []byte) []byte { + s := len(b) + return append([]byte{byte(s >> 8 & 0xFF), byte(s & 0xFF)}, b...) +} diff --git a/tpm/tss2/encode_test.go b/tpm/tss2/encode_test.go new file mode 100644 index 00000000..8bdfe451 --- /dev/null +++ b/tpm/tss2/encode_test.go @@ -0,0 +1,160 @@ +package tss2 + +import ( + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + type args struct { + pub []byte + priv []byte + opts []TPMOption + } + tests := []struct { + name string + args args + want *TPMKey + }{ + {"ok", args{[]byte("public"), []byte("private"), nil}, &TPMKey{ + Type: oidLoadableKey, + EmptyAuth: true, + Parent: 0x40000001, + PublicKey: append([]byte{0, 6}, []byte("public")...), + PrivateKey: append([]byte{0, 7}, []byte("private")...), + }}, + {"ok with options", args{[]byte("public"), []byte("private"), []TPMOption{ + func(k *TPMKey) { + k.EmptyAuth = false + }, + func(k *TPMKey) { + k.Policy = append(k.Policy, TPMPolicy{CommandCode: 1, CommandPolicy: []byte("command-policy")}) + }, + }}, &TPMKey{ + Type: oidLoadableKey, + EmptyAuth: false, + Policy: []TPMPolicy{{CommandCode: 1, CommandPolicy: []byte("command-policy")}}, + Parent: 0x40000001, + PublicKey: append([]byte{0, 6}, []byte("public")...), + PrivateKey: append([]byte{0, 7}, []byte("private")...), + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, New(tt.args.pub, tt.args.priv, tt.args.opts...)) + }) + } +} + +func TestTPMKey_Encode(t *testing.T) { + tests := []struct { + name string + tpmKey *TPMKey + want *pem.Block + assertion assert.ErrorAssertionFunc + }{ + {"ok", New([]byte("public"), []byte("private")), &pem.Block{ + Type: "TSS2 PRIVATE KEY", + Bytes: []byte{ + 0x30, 0x28, + 0x6, 0x6, 0x67, 0x81, 0x5, 0xa, 0x1, 0x3, + 0xa0, 0x3, 0x1, 0x1, 0xff, + 0x2, 0x4, 0x40, 0x0, 0x0, 0x1, + 0x4, 0x8, 0x0, 0x6, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4, 0x9, 0x0, 0x7, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + }, + }, assert.NoError}, + {"fail", nil, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.tpmKey.Encode() + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestTPMKey_EncodeToMemory(t *testing.T) { + tests := []struct { + name string + tpmKey *TPMKey + want []byte + assertion assert.ErrorAssertionFunc + }{ + {"ok", New([]byte("public"), []byte("private")), []byte(`-----BEGIN TSS2 PRIVATE KEY----- +MCgGBmeBBQoBA6ADAQH/AgRAAAABBAgABnB1YmxpYwQJAAdwcml2YXRl +-----END TSS2 PRIVATE KEY----- +`), assert.NoError}, + {"fail", nil, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.tpmKey.EncodeToMemory() + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + t.Log(string(got)) + }) + } +} + +func TestEncode(t *testing.T) { + type args struct { + pub []byte + priv []byte + opts []TPMOption + } + tests := []struct { + name string + args args + want *pem.Block + assertion assert.ErrorAssertionFunc + }{ + {"ok", args{[]byte("public"), []byte("private"), nil}, &pem.Block{ + Type: "TSS2 PRIVATE KEY", + Bytes: []byte{ + 0x30, 0x28, + 0x6, 0x6, 0x67, 0x81, 0x5, 0xa, 0x1, 0x3, + 0xa0, 0x3, 0x1, 0x1, 0xff, + 0x2, 0x4, 0x40, 0x0, 0x0, 0x1, + 0x4, 0x8, 0x0, 0x6, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4, 0x9, 0x0, 0x7, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + }, + }, assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(tt.args.pub, tt.args.priv, tt.args.opts...) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestEncodeToMemory(t *testing.T) { + type args struct { + pub []byte + priv []byte + opts []TPMOption + } + tests := []struct { + name string + args args + want []byte + assertion assert.ErrorAssertionFunc + }{ + {"ok", args{[]byte("public"), []byte("private"), nil}, []byte(`-----BEGIN TSS2 PRIVATE KEY----- +MCgGBmeBBQoBA6ADAQH/AgRAAAABBAgABnB1YmxpYwQJAAdwcml2YXRl +-----END TSS2 PRIVATE KEY----- +`), assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := EncodeToMemory(tt.args.pub, tt.args.priv, tt.args.opts...) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/tpm/tss2/other_test.go b/tpm/tss2/other_test.go new file mode 100644 index 00000000..50f5492c --- /dev/null +++ b/tpm/tss2/other_test.go @@ -0,0 +1,14 @@ +//go:build !tpm && !tpmsimulator + +package tss2 + +import ( + "io" + "testing" +) + +func openTPM(t *testing.T) io.ReadWriteCloser { + t.Helper() + t.Skip("Use tags tpm or tpmsimulator") + return nil +} diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go new file mode 100644 index 00000000..4f0dce76 --- /dev/null +++ b/tpm/tss2/signer.go @@ -0,0 +1,191 @@ +package tss2 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/asn1" + "errors" + "fmt" + "io" + "math/big" + + "github.com/google/go-tpm/legacy/tpm2" + "github.com/google/go-tpm/tpmutil" +) + +var primaryParams = tpm2.Public{ + Type: tpm2.AlgECC, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagStorageDefault, + ECCParameters: &tpm2.ECCParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgNull, + }, + CurveID: tpm2.CurveNISTP256, + }, +} + +// Signer implements [crypto.Signer] using a [TPMKey]. +type Signer struct { + rw io.ReadWriter + publicKey crypto.PublicKey + tpmKey *TPMKey +} + +// CreateSigner creates a new [crypto.Signer] with the given TPM (rw) and +// [TPMKey]. The caller is responsible of opening and closing the TPM. +func CreateSigner(rw io.ReadWriter, key *TPMKey) (crypto.Signer, error) { + switch { + case !key.Type.Equal(oidLoadableKey): + return nil, fmt.Errorf("invalid TSS2 key: type %q is not valid", key.Type.String()) + case len(key.Policy) != 0: + return nil, errors.New("invalid TSS2 key: policy is not implemented") + case len(key.AuthPolicy) != 0: + return nil, errors.New("invalid TSS2 key: auth policy is not implemented") + case len(key.Secret) > 0: + return nil, errors.New("invalid TSS2 key: secret should not be set") + case !validateParent(key.Parent): + return nil, fmt.Errorf("invalid TSS2 key: parent '%d' is not valid", key.Parent) + case !validateKey(key.PublicKey): + return nil, errors.New("invalid TSS2 key: public key is invalid") + case !validateKey(key.PrivateKey): + return nil, errors.New("invalid TSS2 key: private key key is invalid") + } + + public, err := tpm2.DecodePublic(key.PublicKey[2:]) + if err != nil { + return nil, fmt.Errorf("error decoding public key: %w", err) + } + if public.Type != tpm2.AlgRSA && public.Type != tpm2.AlgECC { + return nil, fmt.Errorf("invalid TSS2 key: public key type %q is not valid", public.Type.String()) + } + publicKey, err := public.Key() + if err != nil { + return nil, fmt.Errorf("error creating public key: %w", err) + } + + return &Signer{ + rw: rw, + publicKey: publicKey, + tpmKey: key, + }, nil +} + +func (s *Signer) Public() crypto.PublicKey { + return s.publicKey +} + +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + parentHandle := tpmutil.Handle(s.tpmKey.Parent) + if !handleIsPersistent(s.tpmKey.Parent) { + parentHandle, _, err = tpm2.CreatePrimary(s.rw, parentHandle, tpm2.PCRSelection{}, "", "", primaryParams) + if err != nil { + return nil, fmt.Errorf("error creating primary: %w", err) + } + defer tpm2.FlushContext(s.rw, parentHandle) + } + + keyHandle, _, err := tpm2.Load(s.rw, parentHandle, "", s.tpmKey.PublicKey[2:], s.tpmKey.PrivateKey[2:]) + if err != nil { + return nil, fmt.Errorf("error loading key handle: %w", err) + } + defer tpm2.FlushContext(s.rw, keyHandle) + + switch p := s.publicKey.(type) { + case *ecdsa.PublicKey: + return signECDSA(s.rw, keyHandle, digest, p.Curve) + case *rsa.PublicKey: + return signRSA(s.rw, keyHandle, digest, opts) + default: + return nil, fmt.Errorf("unsupported signing key type %T", s.publicKey) + } +} + +// https://github.com/smallstep/go-attestation/blob/f5480326fb6d63859537ec89fbea7c62485bc4da/attest/wrapped_tpm20.go#L513 +func signECDSA(rw io.ReadWriter, key tpmutil.Handle, digest []byte, curve elliptic.Curve) ([]byte, error) { + scheme, err := curveSigScheme(curve) + if err != nil { + return nil, err + } + sig, err := tpm2.Sign(rw, key, "", digest, nil, scheme) + if err != nil { + return nil, fmt.Errorf("error creating ECDSA signature: %w", err) + } + if sig.ECC == nil { + return nil, fmt.Errorf("expected ECDSA signature, got: %v", sig.Alg) + } + return asn1.Marshal(struct { + R *big.Int + S *big.Int + }{sig.ECC.R, sig.ECC.S}) +} + +// https://github.com/smallstep/go-attestation/blob/f5480326fb6d63859537ec89fbea7c62485bc4da/attest/wrapped_tpm20.go#L527 +func signRSA(rw io.ReadWriter, key tpmutil.Handle, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + h, err := tpm2.HashToAlgorithm(opts.HashFunc()) + if err != nil { + return nil, fmt.Errorf("incorrect hash algorithm: %v", err) + } + + scheme := &tpm2.SigScheme{ + Alg: tpm2.AlgRSASSA, + Hash: h, + } + + if pss, ok := opts.(*rsa.PSSOptions); ok { + if pss.SaltLength != rsa.PSSSaltLengthAuto && pss.SaltLength != rsa.PSSSaltLengthEqualsHash && pss.SaltLength != len(digest) { + return nil, fmt.Errorf("invalid PSS salt length %d, expected rsa.PSSSaltLengthAuto, rsa.PSSSaltLengthEqualsHash or %d", pss.SaltLength, len(digest)) + } + scheme.Alg = tpm2.AlgRSAPSS + } + + sig, err := tpm2.Sign(rw, key, "", digest, nil, scheme) + if err != nil { + return nil, fmt.Errorf("error creating RSA signature: %v", err) + } + if sig.RSA == nil { + return nil, fmt.Errorf("expected RSA signature, got: %v", sig.Alg) + } + return sig.RSA.Signature, nil +} + +func curveSigScheme(curve elliptic.Curve) (*tpm2.SigScheme, error) { + scheme := &tpm2.SigScheme{ + Alg: tpm2.AlgECDSA, + } + switch curve { + case elliptic.P256(): + scheme.Hash = tpm2.AlgSHA256 + case elliptic.P384(): + scheme.Hash = tpm2.AlgSHA384 + case elliptic.P521(): + scheme.Hash = tpm2.AlgSHA512 + default: + return nil, fmt.Errorf("unsupported curve %s", curve.Params().Name) + } + + return scheme, nil +} + +func handleIsPersistent(h int) bool { + return (h >> 24) == int(tpm2.HandleTypePersistent) +} + +func validateParent(parent int) bool { + return handleIsPersistent(parent) || + parent == int(tpm2.HandleOwner) || + parent == int(tpm2.HandleNull) || + parent == int(tpm2.HandleEndorsement) || + parent == int(tpm2.HandlePlatform) +} + +func validateKey(b []byte) bool { + return len(b) >= 2 && len(b)-2 == (int(b[0])<<8)+int(b[1]) +} diff --git a/tpm/tss2/signer_test.go b/tpm/tss2/signer_test.go new file mode 100644 index 00000000..12f6cf70 --- /dev/null +++ b/tpm/tss2/signer_test.go @@ -0,0 +1,199 @@ +package tss2 + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "encoding/pem" + "io" + "testing" + + "github.com/google/go-tpm/legacy/tpm2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var defaultKeyParamsEC = tpm2.Public{ + Type: tpm2.AlgECC, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagSignerDefault ^ tpm2.FlagRestricted, + ECCParameters: &tpm2.ECCParams{ + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgECDSA, + Hash: tpm2.AlgSHA256, + }, + CurveID: tpm2.CurveNISTP256, + }, +} + +var defaultKeyParamsRSA = tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagSignerDefault ^ tpm2.FlagRestricted, + RSAParameters: &tpm2.RSAParams{ + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgRSASSA, + Hash: tpm2.AlgSHA256, + }, + KeyBits: 2048, + }, +} + +var defaultKeyParamsRSAPSS = tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagSignerDefault ^ tpm2.FlagRestricted, + RSAParameters: &tpm2.RSAParams{ + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgRSAPSS, + Hash: tpm2.AlgSHA256, + }, + KeyBits: 2048, + }, +} + +func TestSign(t *testing.T) { + rw := openTPM(t) + t.Cleanup(func() { + assert.NoError(t, rw.Close()) + }) + + keyHnd, _, err := tpm2.CreatePrimary(rw, tpm2.HandleOwner, tpm2.PCRSelection{}, "", "", primaryParams) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, tpm2.FlushContext(rw, keyHnd)) + }) + + tests := []struct { + name string + params tpm2.Public + opts crypto.SignerOpts + }{ + {"ok ECDSA", defaultKeyParamsEC, crypto.SHA256}, + {"ok RSA", defaultKeyParamsRSA, crypto.SHA256}, + {"ok RSAPSS", defaultKeyParamsRSAPSS, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, Hash: crypto.SHA256, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + priv, pub, _, _, _, err := tpm2.CreateKey(rw, keyHnd, tpm2.PCRSelection{}, "", "", tt.params) + require.NoError(t, err) + + signer, err := CreateSigner(rw, New(pub, priv)) + require.NoError(t, err) + + hash := crypto.SHA256.New() + hash.Write([]byte("rulingly-quailed-cloacal-indifferentist-roughhoused-self-mad")) + sum := hash.Sum(nil) + + sig, err := signer.Sign(rand.Reader, sum[:], tt.opts) + require.NoError(t, err) + + switch pub := signer.Public().(type) { + case *ecdsa.PublicKey: + assert.Equal(t, tpm2.AlgECC, tt.params.Type) + assert.True(t, ecdsa.VerifyASN1(pub, sum[:], sig)) + case *rsa.PublicKey: + assert.Equal(t, tpm2.AlgRSA, tt.params.Type) + switch tt.params.RSAParameters.Sign.Alg { + case tpm2.AlgRSASSA: + assert.NoError(t, rsa.VerifyPKCS1v15(pub, tt.opts.HashFunc(), sum[:], sig)) + case tpm2.AlgRSAPSS: + assert.NoError(t, rsa.VerifyPSS(pub, crypto.SHA256, sum[:], sig, nil)) + default: + t.Errorf("unexpected RSAParameters.Sign.Alg %v", tt.params.RSAParameters.Sign.Alg) + } + default: + t.Errorf("unexpected PublicKey type %T", pub) + } + }) + } +} + +func TestCreateSigner(t *testing.T) { + parsePEM := func(s string) []byte { + block, _ := pem.Decode([]byte(s)) + return block.Bytes + } + + var rw bytes.Buffer + key, err := ParsePrivateKey(parsePEM(p256TSS2PEM)) + require.NoError(t, err) + + pub, err := tpm2.DecodePublic(key.PublicKey[2:]) + require.NoError(t, err) + publicKey, err := pub.Key() + require.NoError(t, err) + + modKey := func(fn TPMOption) *TPMKey { + return New(key.PublicKey[2:], key.PrivateKey[2:], fn) + } + + type args struct { + rw io.ReadWriter + key *TPMKey + } + tests := []struct { + name string + args args + want crypto.Signer + assertion assert.ErrorAssertionFunc + }{ + {"ok", args{&rw, key}, &Signer{ + rw: &rw, publicKey: publicKey, tpmKey: key, + }, assert.NoError}, + {"fail type", args{&rw, modKey(func(k *TPMKey) { + k.Type = oidSealedKey + })}, nil, assert.Error}, + {"fail policy", args{&rw, modKey(func(k *TPMKey) { + k.Policy = []TPMPolicy{{CommandCode: 1, CommandPolicy: []byte("command-policy")}} + })}, nil, assert.Error}, + {"fail authPolicy", args{&rw, modKey(func(k *TPMKey) { + k.AuthPolicy = []TPMAuthPolicy{{Name: "auth", Policy: []TPMPolicy{{CommandCode: 1, CommandPolicy: []byte("command-policy")}}}} + })}, nil, assert.Error}, + {"fail secret", args{&rw, modKey(func(k *TPMKey) { + k.Secret = []byte("secret") + })}, nil, assert.Error}, + {"fail parent", args{&rw, modKey(func(k *TPMKey) { + k.Parent = 0 + })}, nil, assert.Error}, + {"fail publicKey", args{&rw, modKey(func(k *TPMKey) { + k.PublicKey = key.PublicKey[2:] + })}, nil, assert.Error}, + {"fail privateKey", args{&rw, modKey(func(k *TPMKey) { + k.PrivateKey = key.PrivateKey[2:] + })}, nil, assert.Error}, + {"fail decodePublic", args{&rw, modKey(func(k *TPMKey) { + k.PublicKey = append([]byte{0, 6}, []byte("public")...) + })}, nil, assert.Error}, + {"fail type", args{&rw, modKey(func(k *TPMKey) { + p := tpm2.Public{ + Type: tpm2.AlgSymCipher, + NameAlg: tpm2.AlgAES, + Attributes: tpm2.FlagSignerDefault ^ tpm2.FlagRestricted, + SymCipherParameters: &tpm2.SymCipherParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + Unique: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, + }, + } + b, err := p.Encode() + if assert.NoError(t, err) { + k.PublicKey = integrityPrefix(b) + } + })}, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateSigner(tt.args.rw, tt.args.key) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/tpm/tss2/simulator_test.go b/tpm/tss2/simulator_test.go new file mode 100644 index 00000000..72fb697d --- /dev/null +++ b/tpm/tss2/simulator_test.go @@ -0,0 +1,20 @@ +//go:build tpmsimulator + +package tss2 + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" + "go.step.sm/crypto/tpm/simulator" +) + +func openTPM(t *testing.T) io.ReadWriteCloser { + t.Helper() + + sim, err := simulator.New() + require.NoError(t, err) + require.NoError(t, sim.Open()) + return sim +} diff --git a/tpm/tss2/tpm_test.go b/tpm/tss2/tpm_test.go new file mode 100644 index 00000000..a16024f1 --- /dev/null +++ b/tpm/tss2/tpm_test.go @@ -0,0 +1,19 @@ +//go:build tpm + +package tss2 + +import ( + "io" + "testing" + + "github.com/google/go-tpm/legacy/tpm2" + "github.com/stretchr/testify/require" +) + +func openTPM(t *testing.T) io.ReadWriteCloser { + t.Helper() + + rwc, err := tpm2.OpenTPM() + require.NoError(t, err) + return rwc +} From 95c70ebf460fbf0a10f1e51bbe5003c489b82bca Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 1 Nov 2023 19:00:14 -0700 Subject: [PATCH 2/6] Add copyright notice to signer.go --- tpm/tss2/signer.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go index 4f0dce76..f4d099da 100644 --- a/tpm/tss2/signer.go +++ b/tpm/tss2/signer.go @@ -1,3 +1,19 @@ +// Copyright 2023 Smallstep Labs, Inc +// Copyright 2023 David Woodhouse, @dwmw2 +// Copyright 2023 @google/go-tpm-admin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package tss2 import ( From 1bca3975e98f30b06cd1adc51ac906496ce1db5f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 1 Nov 2023 19:04:50 -0700 Subject: [PATCH 3/6] Fix linter errors --- tpm/tss2/signer.go | 8 ++++---- tpm/tss2/signer_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go index f4d099da..037b8f4a 100644 --- a/tpm/tss2/signer.go +++ b/tpm/tss2/signer.go @@ -98,7 +98,7 @@ func (s *Signer) Public() crypto.PublicKey { return s.publicKey } -func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { parentHandle := tpmutil.Handle(s.tpmKey.Parent) if !handleIsPersistent(s.tpmKey.Parent) { parentHandle, _, err = tpm2.CreatePrimary(s.rw, parentHandle, tpm2.PCRSelection{}, "", "", primaryParams) @@ -147,7 +147,7 @@ func signECDSA(rw io.ReadWriter, key tpmutil.Handle, digest []byte, curve ellipt func signRSA(rw io.ReadWriter, key tpmutil.Handle, digest []byte, opts crypto.SignerOpts) ([]byte, error) { h, err := tpm2.HashToAlgorithm(opts.HashFunc()) if err != nil { - return nil, fmt.Errorf("incorrect hash algorithm: %v", err) + return nil, fmt.Errorf("error getting algorithm: %w", err) } scheme := &tpm2.SigScheme{ @@ -164,10 +164,10 @@ func signRSA(rw io.ReadWriter, key tpmutil.Handle, digest []byte, opts crypto.Si sig, err := tpm2.Sign(rw, key, "", digest, nil, scheme) if err != nil { - return nil, fmt.Errorf("error creating RSA signature: %v", err) + return nil, fmt.Errorf("error creating RSA signature: %w", err) } if sig.RSA == nil { - return nil, fmt.Errorf("expected RSA signature, got: %v", sig.Alg) + return nil, fmt.Errorf("unexpected signature scheme %v", sig.Alg) } return sig.RSA.Signature, nil } diff --git a/tpm/tss2/signer_test.go b/tpm/tss2/signer_test.go index 12f6cf70..df41e710 100644 --- a/tpm/tss2/signer_test.go +++ b/tpm/tss2/signer_test.go @@ -89,20 +89,20 @@ func TestSign(t *testing.T) { hash.Write([]byte("rulingly-quailed-cloacal-indifferentist-roughhoused-self-mad")) sum := hash.Sum(nil) - sig, err := signer.Sign(rand.Reader, sum[:], tt.opts) + sig, err := signer.Sign(rand.Reader, sum, tt.opts) require.NoError(t, err) switch pub := signer.Public().(type) { case *ecdsa.PublicKey: assert.Equal(t, tpm2.AlgECC, tt.params.Type) - assert.True(t, ecdsa.VerifyASN1(pub, sum[:], sig)) + assert.True(t, ecdsa.VerifyASN1(pub, sum, sig)) case *rsa.PublicKey: assert.Equal(t, tpm2.AlgRSA, tt.params.Type) switch tt.params.RSAParameters.Sign.Alg { case tpm2.AlgRSASSA: - assert.NoError(t, rsa.VerifyPKCS1v15(pub, tt.opts.HashFunc(), sum[:], sig)) + assert.NoError(t, rsa.VerifyPKCS1v15(pub, tt.opts.HashFunc(), sum, sig)) case tpm2.AlgRSAPSS: - assert.NoError(t, rsa.VerifyPSS(pub, crypto.SHA256, sum[:], sig, nil)) + assert.NoError(t, rsa.VerifyPSS(pub, crypto.SHA256, sum, sig, nil)) default: t.Errorf("unexpected RSAParameters.Sign.Alg %v", tt.params.RSAParameters.Sign.Alg) } From 4ae422005a4f226beb8832ad5c7d2e403c3e0b3d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 2 Nov 2023 12:03:44 -0700 Subject: [PATCH 4/6] Make sure the tpm channel is not nil --- tpm/tss2/signer.go | 2 ++ tpm/tss2/signer_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go index 037b8f4a..90225480 100644 --- a/tpm/tss2/signer.go +++ b/tpm/tss2/signer.go @@ -59,6 +59,8 @@ type Signer struct { // [TPMKey]. The caller is responsible of opening and closing the TPM. func CreateSigner(rw io.ReadWriter, key *TPMKey) (crypto.Signer, error) { switch { + case rw == nil: + return nil, fmt.Errorf("invalid TPM channel: rw cannot be nil") case !key.Type.Equal(oidLoadableKey): return nil, fmt.Errorf("invalid TSS2 key: type %q is not valid", key.Type.String()) case len(key.Policy) != 0: diff --git a/tpm/tss2/signer_test.go b/tpm/tss2/signer_test.go index df41e710..5b36d7de 100644 --- a/tpm/tss2/signer_test.go +++ b/tpm/tss2/signer_test.go @@ -145,6 +145,7 @@ func TestCreateSigner(t *testing.T) { {"ok", args{&rw, key}, &Signer{ rw: &rw, publicKey: publicKey, tpmKey: key, }, assert.NoError}, + {"fail rw", args{nil, key}, nil, assert.Error}, {"fail type", args{&rw, modKey(func(k *TPMKey) { k.Type = oidSealedKey })}, nil, assert.Error}, From 5d3c0cfcd075b1a626096e16abda2288a82935d0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 2 Nov 2023 15:21:11 -0700 Subject: [PATCH 5/6] Fix comments from code review This commit also adds a way to define the SRK template used to load the blobs into the TPM. --- tpm/tss2/encode.go | 15 ++++---- tpm/tss2/encode_test.go | 1 - tpm/tss2/signer.go | 81 ++++++++++++++++++++++++++++++----------- tpm/tss2/signer_test.go | 11 ++++-- 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/tpm/tss2/encode.go b/tpm/tss2/encode.go index df5b50e1..5888c1c2 100644 --- a/tpm/tss2/encode.go +++ b/tpm/tss2/encode.go @@ -1,10 +1,9 @@ package tss2 -import ( - "encoding/pem" +import "encoding/pem" - "github.com/google/go-tpm/legacy/tpm2" -) +// handle owner is the reserver handler TPM_RH_OWNER. +const handleOwner = 0x40000001 // TPMOption is the type used to modify a [TPMKey]. type TPMOption func(*TPMKey) @@ -14,9 +13,9 @@ func New(pub, priv []byte, opts ...TPMOption) *TPMKey { key := &TPMKey{ Type: oidLoadableKey, EmptyAuth: true, - Parent: int(tpm2.HandleOwner), - PublicKey: integrityPrefix(pub), - PrivateKey: integrityPrefix(priv), + Parent: handleOwner, + PublicKey: addPrefixLength(pub), + PrivateKey: addPrefixLength(priv), } for _, fn := range opts { fn(key) @@ -56,7 +55,7 @@ func EncodeToMemory(pub, priv []byte, opts ...TPMOption) ([]byte, error) { return New(pub, priv, opts...).EncodeToMemory() } -func integrityPrefix(b []byte) []byte { +func addPrefixLength(b []byte) []byte { s := len(b) return append([]byte{byte(s >> 8 & 0xFF), byte(s & 0xFF)}, b...) } diff --git a/tpm/tss2/encode_test.go b/tpm/tss2/encode_test.go index 8bdfe451..df486748 100644 --- a/tpm/tss2/encode_test.go +++ b/tpm/tss2/encode_test.go @@ -95,7 +95,6 @@ MCgGBmeBBQoBA6ADAQH/AgRAAAABBAgABnB1YmxpYwQJAAdwcml2YXRl got, err := tt.tpmKey.EncodeToMemory() tt.assertion(t, err) assert.Equal(t, tt.want, got) - t.Log(string(got)) }) } } diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go index 90225480..bb4e159b 100644 --- a/tpm/tss2/signer.go +++ b/tpm/tss2/signer.go @@ -31,33 +31,55 @@ import ( "github.com/google/go-tpm/tpmutil" ) -var primaryParams = tpm2.Public{ - Type: tpm2.AlgECC, - NameAlg: tpm2.AlgSHA256, - Attributes: tpm2.FlagStorageDefault, - ECCParameters: &tpm2.ECCParams{ - Symmetric: &tpm2.SymScheme{ - Alg: tpm2.AlgAES, - KeyBits: 128, - Mode: tpm2.AlgCFB, +var ( + // ECCSRKTemplate contains the TCG reference ECC-P256 SRK template. + // https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf + ECCSRKTemplate = tpm2.Public{ + Type: tpm2.AlgECC, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagStorageDefault | tpm2.FlagNoDA, + ECCParameters: &tpm2.ECCParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgNull, + }, + CurveID: tpm2.CurveNISTP256, }, - Sign: &tpm2.SigScheme{ - Alg: tpm2.AlgNull, + } + + // RSASRKTemplate contains the TCG reference RSA-2048 SRK template. + // https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf + RSASRKTemplate = tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagStorageDefault | tpm2.FlagNoDA, + RSAParameters: &tpm2.RSAParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + ModulusRaw: make([]byte, 256), + KeyBits: 2048, }, - CurveID: tpm2.CurveNISTP256, - }, -} + } +) // Signer implements [crypto.Signer] using a [TPMKey]. type Signer struct { - rw io.ReadWriter - publicKey crypto.PublicKey - tpmKey *TPMKey + rw io.ReadWriter + publicKey crypto.PublicKey + tpmKey *TPMKey + srkTemplate tpm2.Public } // CreateSigner creates a new [crypto.Signer] with the given TPM (rw) and // [TPMKey]. The caller is responsible of opening and closing the TPM. -func CreateSigner(rw io.ReadWriter, key *TPMKey) (crypto.Signer, error) { +func CreateSigner(rw io.ReadWriter, key *TPMKey) (*Signer, error) { switch { case rw == nil: return nil, fmt.Errorf("invalid TPM channel: rw cannot be nil") @@ -90,12 +112,27 @@ func CreateSigner(rw io.ReadWriter, key *TPMKey) (crypto.Signer, error) { } return &Signer{ - rw: rw, - publicKey: publicKey, - tpmKey: key, + rw: rw, + publicKey: publicKey, + tpmKey: key, + srkTemplate: RSASRKTemplate, }, nil } +// SetSRKTemplate allows to change the Storage Primary Key (SRK) template used +// to load the the public/private blobs into an object in the TPM. +// +// It currently defaults to [RSASRKTemplate], the same used ad default in the +// [go.step.sm/crypto/tpm] package. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +func (s *Signer) SetSRKTemplate(p tpm2.Public) { + s.srkTemplate = p +} + func (s *Signer) Public() crypto.PublicKey { return s.publicKey } @@ -103,7 +140,7 @@ func (s *Signer) Public() crypto.PublicKey { func (s *Signer) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { parentHandle := tpmutil.Handle(s.tpmKey.Parent) if !handleIsPersistent(s.tpmKey.Parent) { - parentHandle, _, err = tpm2.CreatePrimary(s.rw, parentHandle, tpm2.PCRSelection{}, "", "", primaryParams) + parentHandle, _, err = tpm2.CreatePrimary(s.rw, parentHandle, tpm2.PCRSelection{}, "", "", s.srkTemplate) if err != nil { return nil, fmt.Errorf("error creating primary: %w", err) } diff --git a/tpm/tss2/signer_test.go b/tpm/tss2/signer_test.go index 5b36d7de..5dce3446 100644 --- a/tpm/tss2/signer_test.go +++ b/tpm/tss2/signer_test.go @@ -60,7 +60,7 @@ func TestSign(t *testing.T) { assert.NoError(t, rw.Close()) }) - keyHnd, _, err := tpm2.CreatePrimary(rw, tpm2.HandleOwner, tpm2.PCRSelection{}, "", "", primaryParams) + keyHnd, _, err := tpm2.CreatePrimary(rw, tpm2.HandleOwner, tpm2.PCRSelection{}, "", "", ECCSRKTemplate) require.NoError(t, err) t.Cleanup(func() { assert.NoError(t, tpm2.FlushContext(rw, keyHnd)) @@ -85,6 +85,9 @@ func TestSign(t *testing.T) { signer, err := CreateSigner(rw, New(pub, priv)) require.NoError(t, err) + // Set the ECC SRK template used for testing. + signer.SetSRKTemplate(ECCSRKTemplate) + hash := crypto.SHA256.New() hash.Write([]byte("rulingly-quailed-cloacal-indifferentist-roughhoused-self-mad")) sum := hash.Sum(nil) @@ -139,11 +142,11 @@ func TestCreateSigner(t *testing.T) { tests := []struct { name string args args - want crypto.Signer + want *Signer assertion assert.ErrorAssertionFunc }{ {"ok", args{&rw, key}, &Signer{ - rw: &rw, publicKey: publicKey, tpmKey: key, + rw: &rw, publicKey: publicKey, tpmKey: key, srkTemplate: RSASRKTemplate, }, assert.NoError}, {"fail rw", args{nil, key}, nil, assert.Error}, {"fail type", args{&rw, modKey(func(k *TPMKey) { @@ -186,7 +189,7 @@ func TestCreateSigner(t *testing.T) { } b, err := p.Encode() if assert.NoError(t, err) { - k.PublicKey = integrityPrefix(b) + k.PublicKey = addPrefixLength(b) } })}, nil, assert.Error}, } From c7c90c76daa3b1d518e31f5b33cc65c57241c961 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 6 Nov 2023 09:48:13 -0800 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: Herman Slatman --- tpm/tss2/encode.go | 2 +- tpm/tss2/signer.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tpm/tss2/encode.go b/tpm/tss2/encode.go index 5888c1c2..5a84f4db 100644 --- a/tpm/tss2/encode.go +++ b/tpm/tss2/encode.go @@ -2,7 +2,7 @@ package tss2 import "encoding/pem" -// handle owner is the reserver handler TPM_RH_OWNER. +// handle owner is the reserved handle TPM_RH_OWNER. const handleOwner = 0x40000001 // TPMOption is the type used to modify a [TPMKey]. diff --git a/tpm/tss2/signer.go b/tpm/tss2/signer.go index bb4e159b..f96a6d67 100644 --- a/tpm/tss2/signer.go +++ b/tpm/tss2/signer.go @@ -78,7 +78,7 @@ type Signer struct { } // CreateSigner creates a new [crypto.Signer] with the given TPM (rw) and -// [TPMKey]. The caller is responsible of opening and closing the TPM. +// [TPMKey]. The caller is responsible for opening and closing the TPM. func CreateSigner(rw io.ReadWriter, key *TPMKey) (*Signer, error) { switch { case rw == nil: @@ -119,10 +119,10 @@ func CreateSigner(rw io.ReadWriter, key *TPMKey) (*Signer, error) { }, nil } -// SetSRKTemplate allows to change the Storage Primary Key (SRK) template used +// SetSRKTemplate allows to change the Storage Root Key (SRK) template used // to load the the public/private blobs into an object in the TPM. // -// It currently defaults to [RSASRKTemplate], the same used ad default in the +// It currently defaults to [RSASRKTemplate], the same used as the default in the // [go.step.sm/crypto/tpm] package. // // # Experimental