diff --git a/kms/tpmkms/testdata/ec-tss2.pem b/kms/tpmkms/testdata/ec-tss2.pem new file mode 100644 index 00000000..4542894d --- /dev/null +++ b/kms/tpmkms/testdata/ec-tss2.pem @@ -0,0 +1,8 @@ +-----BEGIN TSS2 PRIVATE KEY----- +MIHyBgZngQUKAQOgAwEB/wIEQAAAAQRaAFgAIwALAAQAcgAAABAAGAALAAMAEAAg +ebLnGlDAN5aHdkffRTqBdsQNnO60aY+Xvg5u80sIauMAIM0E3zndp5392TPBroKz +PLHEwLiUVeBmOhBG3kss/uICBIGAAH4AIIMO322TFYndManhpvTgLMiFd6Vs3HVs +OrHb15qLZTDQABBMcF+L3C2322FU060DHXv56Uq87uMu/qWE3HU+r5856+70P94I +0z3Plxwln2iGhhbKZ8gQQNKhiOdE4MYDPnN1uqvcwJwd7NZ1fqnwBKks6E1vgSne +tYeNooQ= +-----END TSS2 PRIVATE KEY----- diff --git a/kms/tpmkms/tpmkms.go b/kms/tpmkms/tpmkms.go index 70b04f36..b8d1ed4f 100644 --- a/kms/tpmkms/tpmkms.go +++ b/kms/tpmkms/tpmkms.go @@ -10,9 +10,11 @@ import ( "crypto/sha256" "crypto/x509" "encoding/base64" + "encoding/pem" "errors" "fmt" "net/url" + "os" "time" "go.step.sm/crypto/kms/apiv1" @@ -20,6 +22,7 @@ import ( "go.step.sm/crypto/tpm" "go.step.sm/crypto/tpm/attestation" "go.step.sm/crypto/tpm/storage" + "go.step.sm/crypto/tpm/tss2" ) func init() { @@ -188,6 +191,7 @@ func New(_ context.Context, opts apiv1.Options) (kms *TPMKMS, err error) { // // - name=: specify the name to identify the key with // - ak=true: if set to true, an Attestation Key (AK) will be created instead of an application key +// - tss2=true: is set to true, the PrivateKey response will contain a [tss2.TPMKey]. // - attest-by=: attest an application key at creation time with the AK identified by `akName` // - qualifying-data=: hexadecimal coded binary data that can be used to guarantee freshness when attesting creation of a key // @@ -240,6 +244,7 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } ctx := context.Background() + if properties.ak { ak, err := k.tpm.CreateAK(ctx, properties.name) // NOTE: size is never passed for AKs; it's hardcoded to 2048 in lower levels. if err != nil { @@ -248,10 +253,19 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } return nil, fmt.Errorf("failed creating AK: %w", err) } + + var tpmKey *tss2.TPMKey + if properties.tss2 { + if tpmKey, err = ak.ToTSS2(ctx); err != nil { + return nil, fmt.Errorf("failed exporting AK to TSS2: %w", err) + } + } + createdAKURI := fmt.Sprintf("tpmkms:name=%s;ak=true", ak.Name()) return &apiv1.CreateKeyResponse{ - Name: createdAKURI, - PublicKey: ak.Public(), + Name: createdAKURI, + PublicKey: ak.Public(), + PrivateKey: tpmKey, }, nil } @@ -283,6 +297,13 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } } + var tpmKey *tss2.TPMKey + if properties.tss2 { + if tpmKey, err = key.ToTSS2(ctx); err != nil { + return nil, fmt.Errorf("failed exporting key to TSS2: %w", err) + } + } + signer, err := key.Signer(ctx) if err != nil { return nil, fmt.Errorf("failed getting signer for key: %w", err) @@ -294,8 +315,9 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } return &apiv1.CreateKeyResponse{ - Name: createdKeyURI, - PublicKey: signer.Public(), + Name: createdKeyURI, + PublicKey: signer.Public(), + PrivateKey: tpmKey, CreateSignerRequest: apiv1.CreateSignerRequest{ SigningKey: createdKeyURI, Signer: signer, @@ -304,39 +326,76 @@ func (k *TPMKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons } // CreateSigner creates a signer using a key present in the TPM KMS. +// +// The `signingKey` in the [apiv1.CreateSignerRequest] can be used to specify +// some key properties. These are as follows: +// +// - name=: specify the name to identify the key with +// - path=: specify the TSS2 PEM file to use func (k *TPMKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { if req.Signer != nil { return req.Signer, nil } - if req.SigningKey == "" { - return nil, errors.New("createSignerRequest 'signingKey' cannot be empty") - } + var pemBytes []byte - properties, err := parseNameURI(req.SigningKey) - if err != nil { - return nil, fmt.Errorf("failed parsing %q: %w", req.SigningKey, err) - } + switch { + case req.SigningKey != "": + properties, err := parseNameURI(req.SigningKey) + if err != nil { + return nil, fmt.Errorf("failed parsing %q: %w", req.SigningKey, err) + } + if properties.ak { + return nil, fmt.Errorf("signing with an AK currently not supported") + } - if properties.ak { - return nil, fmt.Errorf("signing with an AK currently not supported") + switch { + case properties.name != "": + ctx := context.Background() + key, err := k.tpm.GetKey(ctx, properties.name) + if err != nil { + return nil, err + } + signer, err := key.Signer(ctx) + if err != nil { + return nil, fmt.Errorf("failed getting signer for key %q: %w", properties.name, err) + } + return signer, nil + case properties.path != "": + if pemBytes, err = os.ReadFile(properties.path); err != nil { + return nil, fmt.Errorf("failed reading key from %q: %w", properties.path, err) + } + default: + return nil, fmt.Errorf("failed parsing %q: name and path cannot be empty", req.SigningKey) + } + case len(req.SigningKeyPEM) > 0: + pemBytes = req.SigningKeyPEM + default: + return nil, errors.New("createSignerRequest 'signingKey' and 'signingKeyPEM' cannot be empty") } - ctx := context.Background() - key, err := k.tpm.GetKey(ctx, properties.name) + // Create a signer from a TSS2 PEM block + key, err := parseTSS2(pemBytes) if err != nil { return nil, err } - signer, err := key.Signer(ctx) + ctx := context.Background() + signer, err := k.tpm.GetTSS2Signer(ctx, key) if err != nil { - return nil, fmt.Errorf("failed getting signer for key %q: %w", properties.name, err) + return nil, fmt.Errorf("failed getting signer for TSS2 PEM: %w", err) } - return signer, nil } -// GetPublicKey returns the public key .... +// GetPublicKey returns the public key present in the TPM KMS. +// +// The `name` in the [apiv1.GetPublicKeyRequest] can be used to specify some key +// properties. These are as follows: +// +// - name=: specify the name to identify the key with +// - ak=true: if set to true, an Attestation Key (AK) will be read instead of an application key +// - path=: specify the TSS2 PEM file to read from func (k *TPMKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { if req.Name == "" { return nil, errors.New("getPublicKeyRequest 'name' cannot be empty") @@ -348,29 +407,48 @@ func (k *TPMKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, } ctx := context.Background() - if properties.ak { - ak, err := k.tpm.GetAK(ctx, properties.name) + switch { + case properties.name != "": + if properties.ak { + ak, err := k.tpm.GetAK(ctx, properties.name) + if err != nil { + return nil, err + } + akPub := ak.Public() + if akPub == nil { + return nil, errors.New("failed getting AK public key") + } + return akPub, nil + } + + key, err := k.tpm.GetKey(ctx, properties.name) if err != nil { return nil, err } - akPub := ak.Public() - if akPub == nil { - return nil, errors.New("failed getting AK public key") - } - return akPub, nil - } - key, err := k.tpm.GetKey(ctx, properties.name) - if err != nil { - return nil, err - } + signer, err := key.Signer(ctx) + if err != nil { + return nil, fmt.Errorf("failed getting signer for key %q: %w", properties.name, err) + } - signer, err := key.Signer(ctx) - if err != nil { - return nil, fmt.Errorf("failed getting signer for key %q: %w", properties.name, err) + return signer.Public(), nil + case properties.path != "": + pemBytes, err := os.ReadFile(properties.path) + if err != nil { + return nil, fmt.Errorf("failed reading key from %q: %w", properties.path, err) + } + key, err := parseTSS2(pemBytes) + if err != nil { + return nil, err + } + pub, err := key.Public() + if err != nil { + return nil, fmt.Errorf("error decoding public key from %q: %w", properties.path, err) + } + return pub, nil + default: + return nil, fmt.Errorf("failed parsing %q: name and path cannot be empty", req.Name) } - - return signer.Public(), nil } // LoadCertificate loads the certificate for the key identified by name from the TPMKMS. @@ -757,6 +835,26 @@ func ekURL(keyID []byte) *url.URL { } } +func parseTSS2(pemBytes []byte) (*tss2.TPMKey, error) { + var block *pem.Block + for len(pemBytes) > 0 { + block, pemBytes = pem.Decode(pemBytes) + if block == nil { + break + } + if block.Type != "TSS2 PRIVATE KEY" { + continue + } + + key, err := tss2.ParsePrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed parsing TSS2 PEM: %w", err) + } + return key, nil + } + return nil, fmt.Errorf("failed parsing TSS2 PEM: block not found") +} + var _ apiv1.KeyManager = (*TPMKMS)(nil) var _ apiv1.Attester = (*TPMKMS)(nil) var _ apiv1.CertificateManager = (*TPMKMS)(nil) diff --git a/kms/tpmkms/tpmkms_simulator_test.go b/kms/tpmkms/tpmkms_simulator_test.go index 900f82ba..0164be1a 100644 --- a/kms/tpmkms/tpmkms_simulator_test.go +++ b/kms/tpmkms/tpmkms_simulator_test.go @@ -9,6 +9,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/json" @@ -17,6 +18,8 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "testing" "time" @@ -31,6 +34,7 @@ import ( tpmp "go.step.sm/crypto/tpm" "go.step.sm/crypto/tpm/simulator" "go.step.sm/crypto/tpm/storage" + "go.step.sm/crypto/tpm/tss2" ) type newSimulatedTPMOption func(t *testing.T, tpm *tpmp.TPM) @@ -153,13 +157,13 @@ func TestTPMKMS_CreateKey(t *testing.T) { }, }, { - name: "ok/ak2", + name: "ok/ak-tss2", fields: fields{ tpm: tpmWithAK, }, args: args{ req: &apiv1.CreateKeyRequest{ - Name: "tpmkms:name=ak2;ak=true", + Name: "tpmkms:name=ak2;ak=true;tss2=true", SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 2048, }, @@ -170,6 +174,12 @@ func TestTPMKMS_CreateKey(t *testing.T) { if assert.NotNil(t, r) { assert.Equal(t, "tpmkms:name=ak2;ak=true", r.Name) assert.Equal(t, apiv1.CreateSignerRequest{}, r.CreateSignerRequest) + if assert.NotNil(t, r.PublicKey) { + assert.IsType(t, &rsa.PublicKey{}, r.PublicKey) + } + if assert.NotNil(t, r.PrivateKey) { + assert.IsType(t, &tss2.TPMKey{}, r.PrivateKey) + } return true } } @@ -177,13 +187,13 @@ func TestTPMKMS_CreateKey(t *testing.T) { }, }, { - name: "ok/ecdsa-key", + name: "ok/ecdsa-key-tss2", fields: fields{ tpm: tpmWithAK, }, args: args{ req: &apiv1.CreateKeyRequest{ - Name: "tpmkms:name=ecdsa-key", + Name: "tpmkms:name=ecdsa-key;tss2=true", SignatureAlgorithm: apiv1.ECDSAWithSHA256, }, }, @@ -196,6 +206,12 @@ func TestTPMKMS_CreateKey(t *testing.T) { if assert.NotNil(t, r.CreateSignerRequest.Signer) { assert.Implements(t, (*crypto.Signer)(nil), r.CreateSignerRequest.Signer) } + if assert.NotNil(t, r.PublicKey) { + assert.IsType(t, &ecdsa.PublicKey{}, r.PublicKey) + } + if assert.NotNil(t, r.PrivateKey) { + assert.IsType(t, &tss2.TPMKey{}, r.PrivateKey) + } return true } } @@ -221,6 +237,18 @@ func TestTPMKMS_CreateKey(t *testing.T) { }, expErr: errors.New("createKeyRequest 'name' cannot be empty"), }, + { + name: "fail/uri", + fields: fields{ + tpm: tpmWithAK, + }, + args: args{ + req: &apiv1.CreateKeyRequest{ + Name: "baduri:", + }, + }, + expErr: errors.New("failed parsing \"baduri:\": URI scheme \"baduri\" is not supported"), + }, { name: "fail/negative-bits", fields: fields{ @@ -407,8 +435,19 @@ func TestTPMKMS_CreateKey(t *testing.T) { func TestTPMKMS_CreateSigner(t *testing.T) { tpmWithKey := newSimulatedTPM(t, withKey("key1")) - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + key, err := tpmWithKey.GetKey(context.Background(), "key1") + require.NoError(t, err) + tss2Key, err := key.ToTSS2(context.Background()) + require.NoError(t, err) + pemBytes, err := tss2Key.EncodeToMemory() require.NoError(t, err) + tmp := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmp, "tss2.pem"), pemBytes, 0600)) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + type fields struct { tpm *tpmp.TPM } @@ -428,7 +467,7 @@ func TestTPMKMS_CreateSigner(t *testing.T) { }, args: args{ req: &apiv1.CreateSignerRequest{ - Signer: key, + Signer: signer, }, }, }, @@ -443,6 +482,40 @@ func TestTPMKMS_CreateSigner(t *testing.T) { }, }, }, + { + name: "ok/signer-path", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.CreateSignerRequest{ + SigningKey: "tpmkms:path=" + filepath.Join(tmp, "tss2.pem"), + }, + }, + }, + { + name: "ok/signer-pem", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.CreateSignerRequest{ + SigningKeyPEM: pemBytes, + }, + }, + }, + { + name: "fail/uri", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.CreateSignerRequest{ + SigningKey: "baduri:", + }, + }, + expErr: errors.New("failed parsing \"baduri:\": URI scheme \"baduri\" is not supported"), + }, { name: "fail/empty", fields: fields{ @@ -453,7 +526,31 @@ func TestTPMKMS_CreateSigner(t *testing.T) { SigningKey: "", }, }, - expErr: errors.New("createSignerRequest 'signingKey' cannot be empty"), + expErr: errors.New("createSignerRequest 'signingKey' and 'signingKeyPEM' cannot be empty"), + }, + { + name: "fail/empty-opaque", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.CreateSignerRequest{ + SigningKey: "tpmkms:", + }, + }, + expErr: errors.New("failed parsing \"tpmkms:\": name and path cannot be empty"), + }, + { + name: "fail/missing", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.CreateSignerRequest{ + SigningKey: "tpmkms:path=testdata/missing.pem", + }, + }, + expErr: errors.New("failed reading key from \"testdata/missing.pem\": open testdata/missing.pem: no such file or directory"), }, { name: "fail/ak", @@ -536,6 +633,17 @@ func TestTPMKMS_GetPublicKey(t *testing.T) { }, }, }, + { + name: "ok/key-path", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.GetPublicKeyRequest{ + Name: "tpmkms:path=testdata/ec-tss2.pem", + }, + }, + }, { name: "fail/empty", fields: fields{ @@ -548,6 +656,42 @@ func TestTPMKMS_GetPublicKey(t *testing.T) { }, expErr: errors.New("getPublicKeyRequest 'name' cannot be empty"), }, + { + name: "fail/empty-opaque", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.GetPublicKeyRequest{ + Name: "tpmkms:", + }, + }, + expErr: errors.New("failed parsing \"tpmkms:\": name and path cannot be empty"), + }, + { + name: "fail/uri", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.GetPublicKeyRequest{ + Name: "baduri:", + }, + }, + expErr: errors.New("failed parsing \"baduri:\": URI scheme \"baduri\" is not supported"), + }, + { + name: "fail/missing", + fields: fields{ + tpm: tpmWithKey, + }, + args: args{ + req: &apiv1.GetPublicKeyRequest{ + Name: "tpmkms:path=testdata/missing.pem", + }, + }, + expErr: errors.New("failed reading key from \"testdata/missing.pem\": open testdata/missing.pem: no such file or directory"), + }, { name: "fail/unknown-key", fields: fields{ diff --git a/kms/tpmkms/tpmkms_test.go b/kms/tpmkms/tpmkms_test.go index d544a605..351145ec 100644 --- a/kms/tpmkms/tpmkms_test.go +++ b/kms/tpmkms/tpmkms_test.go @@ -2,10 +2,14 @@ package tpmkms import ( "context" + "encoding/asn1" + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/tpm/tss2" ) func TestNew(t *testing.T) { @@ -39,3 +43,68 @@ func TestNew(t *testing.T) { }) } } + +func Test_parseTSS2(t *testing.T) { + pemBytes, err := os.ReadFile("testdata/ec-tss2.pem") + require.NoError(t, err) + + type args struct { + pemBytes []byte + } + tests := []struct { + name string + args args + want *tss2.TPMKey + assertion assert.ErrorAssertionFunc + }{ + {"ok", args{pemBytes}, &tss2.TPMKey{ + Type: asn1.ObjectIdentifier{2, 23, 133, 10, 1, 3}, + EmptyAuth: true, + Parent: 0x40000001, + PublicKey: []byte{ + 0x00, 0x58, + 0x00, 0x23, 0x00, 0x0b, 0x00, 0x04, 0x00, 0x72, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x18, 0x00, 0x0b, + 0x00, 0x03, 0x00, 0x10, 0x00, 0x20, 0x79, 0xb2, + 0xe7, 0x1a, 0x50, 0xc0, 0x37, 0x96, 0x87, 0x76, + 0x47, 0xdf, 0x45, 0x3a, 0x81, 0x76, 0xc4, 0x0d, + 0x9c, 0xee, 0xb4, 0x69, 0x8f, 0x97, 0xbe, 0x0e, + 0x6e, 0xf3, 0x4b, 0x08, 0x6a, 0xe3, 0x00, 0x20, + 0xcd, 0x04, 0xdf, 0x39, 0xdd, 0xa7, 0x9d, 0xfd, + 0xd9, 0x33, 0xc1, 0xae, 0x82, 0xb3, 0x3c, 0xb1, + 0xc4, 0xc0, 0xb8, 0x94, 0x55, 0xe0, 0x66, 0x3a, + 0x10, 0x46, 0xde, 0x4b, 0x2c, 0xfe, 0xe2, 0x02, + }, + PrivateKey: []byte{ + 0x00, 0x7e, + 0x00, 0x20, 0x83, 0x0e, 0xdf, 0x6d, 0x93, 0x15, + 0x89, 0xdd, 0x31, 0xa9, 0xe1, 0xa6, 0xf4, 0xe0, + 0x2c, 0xc8, 0x85, 0x77, 0xa5, 0x6c, 0xdc, 0x75, + 0x6c, 0x3a, 0xb1, 0xdb, 0xd7, 0x9a, 0x8b, 0x65, + 0x30, 0xd0, 0x00, 0x10, 0x4c, 0x70, 0x5f, 0x8b, + 0xdc, 0x2d, 0xb7, 0xdb, 0x61, 0x54, 0xd3, 0xad, + 0x03, 0x1d, 0x7b, 0xf9, 0xe9, 0x4a, 0xbc, 0xee, + 0xe3, 0x2e, 0xfe, 0xa5, 0x84, 0xdc, 0x75, 0x3e, + 0xaf, 0x9f, 0x39, 0xeb, 0xee, 0xf4, 0x3f, 0xde, + 0x08, 0xd3, 0x3d, 0xcf, 0x97, 0x1c, 0x25, 0x9f, + 0x68, 0x86, 0x86, 0x16, 0xca, 0x67, 0xc8, 0x10, + 0x40, 0xd2, 0xa1, 0x88, 0xe7, 0x44, 0xe0, 0xc6, + 0x03, 0x3e, 0x73, 0x75, 0xba, 0xab, 0xdc, 0xc0, + 0x9c, 0x1d, 0xec, 0xd6, 0x75, 0x7e, 0xa9, 0xf0, + 0x04, 0xa9, 0x2c, 0xe8, 0x4d, 0x6f, 0x81, 0x29, + 0xde, 0xb5, 0x87, 0x8d, 0xa2, 0x84, + }, + }, assert.NoError}, + {"fail empty", args{nil}, nil, assert.Error}, + {"fail no pem", args{[]byte("not a pem")}, nil, assert.Error}, + {"fail type", args{[]byte("-----BEGIN FOO-----\nMCgGBmeBBQoBA6ADAQH/AgRAAAABBAgABnB1YmxpYwQJAAdwcml2YXRl\n-----END FOO-----")}, nil, assert.Error}, + {"fail parse", args{[]byte("-----BEGIN TSS2 PRIVATE KEY-----\nbm90LWEta2V5Cg==\n-----END TSS2 PRIVATE KEY-----")}, nil, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseTSS2(tt.args.pemBytes) + tt.assertion(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/kms/tpmkms/uri.go b/kms/tpmkms/uri.go index 5dabd8de..76c3fb4b 100644 --- a/kms/tpmkms/uri.go +++ b/kms/tpmkms/uri.go @@ -13,8 +13,10 @@ import ( type objectProperties struct { name string ak bool + tss2 bool attestBy string qualifyingData []byte + path string } func parseNameURI(nameURI string) (o objectProperties, err error) { @@ -24,7 +26,8 @@ func parseNameURI(nameURI string) (o objectProperties, err error) { var u *uri.URI var parseErr error if u, parseErr = uri.ParseWithScheme(Scheme, nameURI); parseErr == nil { - if name := u.Get("name"); name == "" { + o.path = u.Get("path") + if name := u.Get("name"); name == "" && o.path == "" { if len(u.Values) == 1 { o.name = u.Opaque } else { @@ -39,6 +42,7 @@ func parseNameURI(nameURI string) (o objectProperties, err error) { o.name = name } o.ak = u.GetBool("ak") + o.tss2 = u.GetBool("tss2") o.attestBy = u.Get("attest-by") if qualifyingData := u.GetEncoded("qualifying-data"); qualifyingData != nil { o.qualifyingData = qualifyingData