From ab034308e280fe19a2645f0f801663d685cf2a8e Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Tue, 18 Jun 2024 00:28:07 -0700 Subject: [PATCH 1/2] CA suite support --- .github/ISSUE_TEMPLATE/testplan.md | 4 +- api/types/authentication.go | 8 ++ integration/hsm/helpers.go | 2 +- integration/hsm/hsm_test.go | 1 + lib/auth/auth.go | 106 ++++++++++++-------- lib/auth/keystore/aws_kms.go | 45 +++++++-- lib/auth/keystore/aws_kms_test.go | 68 +++++++++---- lib/auth/keystore/gcp_kms.go | 49 ++++++---- lib/auth/keystore/gcp_kms_test.go | 121 +++++++++++------------ lib/auth/keystore/keystore_test.go | 151 ++++++++++++++++++----------- lib/auth/keystore/manager.go | 44 +++++++-- lib/auth/keystore/pkcs11.go | 56 +++++++---- lib/auth/keystore/software.go | 40 +++----- lib/auth/keystore/testhelpers.go | 15 +++ lib/cryptosuites/suites.go | 51 +++++++--- lib/cryptosuites/suites_test.go | 2 +- 16 files changed, 479 insertions(+), 284 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/testplan.md b/.github/ISSUE_TEMPLATE/testplan.md index af766eeb617e6..56c2093515e9b 100644 --- a/.github/ISSUE_TEMPLATE/testplan.md +++ b/.github/ISSUE_TEMPLATE/testplan.md @@ -821,8 +821,8 @@ $ $ # test AWS KMS $ # login in to AWS locally $ AWS_ACCOUNT="$(aws sts get-caller-identity | jq -r '.Account')" -$ TELEPORT_TEST_AWS_KMS_ACCOUNT="${AWS_ACCOUNT}" TELEPORT_TEST_AWS_REGION=us-west-2 go test ./lib/auth/keystore -v --count 1 -$ TELEPORT_TEST_AWS_KMS_ACCOUNT="${AWS_ACCOUNT}" TELEPORT_TEST_AWS_REGION=us-west-2 TELEPORT_ETCD_TEST=1 go test ./integration/hsm -v --count 1 +$ TELEPORT_TEST_AWS_KMS_ACCOUNT="${AWS_ACCOUNT}" TELEPORT_TEST_AWS_KMS_REGION=us-west-2 go test ./lib/auth/keystore -v --count 1 +$ TELEPORT_TEST_AWS_KMS_ACCOUNT="${AWS_ACCOUNT}" TELEPORT_TEST_AWS_KMS_REGION=us-west-2 TELEPORT_ETCD_TEST=1 go test ./integration/hsm -v --count 1 $ $ # test AWS CloudHSM $ # set up the CloudHSM cluster and run this on an EC2 that can reach it diff --git a/api/types/authentication.go b/api/types/authentication.go index d41e1da66aeef..c7f46811730ab 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -172,6 +172,8 @@ type AuthPreference interface { // GetSignatureAlgorithmSuite gets the signature algorithm suite. GetSignatureAlgorithmSuite() SignatureAlgorithmSuite + // SetSignatureAlgorithmSuite sets the signature algorithm suite. + SetSignatureAlgorithmSuite(SignatureAlgorithmSuite) // String represents a human readable version of authentication settings. String() string @@ -543,10 +545,16 @@ func (c *AuthPreferenceV2) setStaticFields() { c.Metadata.Name = MetaNameClusterAuthPreference } +// GetSignatureAlgorithmSuite gets the signature algorithm suite. func (c *AuthPreferenceV2) GetSignatureAlgorithmSuite() SignatureAlgorithmSuite { return c.Spec.SignatureAlgorithmSuite } +// SetSignatureAlgorithmSuite sets the signature algorithm suite. +func (c *AuthPreferenceV2) SetSignatureAlgorithmSuite(suite SignatureAlgorithmSuite) { + c.Spec.SignatureAlgorithmSuite = suite +} + // CheckAndSetDefaults verifies the constraints for AuthPreference. func (c *AuthPreferenceV2) CheckAndSetDefaults() error { c.setStaticFields() diff --git a/integration/hsm/helpers.go b/integration/hsm/helpers.go index fa0f24be72bf4..0bf7631a62a87 100644 --- a/integration/hsm/helpers.go +++ b/integration/hsm/helpers.go @@ -275,7 +275,7 @@ func newAuthConfig(t *testing.T, log utils.Logger) *servicecfg.Config { } var err error config.Auth.ClusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ - ClusterName: "testcluster", + ClusterName: "test-cluster", }) require.NoError(t, err) config.SetAuthServerAddress(config.Auth.ListenAddr) diff --git a/integration/hsm/hsm_test.go b/integration/hsm/hsm_test.go index f5556ad724595..72320e149ef94 100644 --- a/integration/hsm/hsm_test.go +++ b/integration/hsm/hsm_test.go @@ -67,6 +67,7 @@ func newHSMAuthConfig(t *testing.T, storageConfig *backend.Config, log utils.Log config := newAuthConfig(t, log) config.Auth.StorageConfig = *storageConfig config.Auth.KeyStore = keystore.HSMTestConfig(t) + config.Auth.Preference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1) return config } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 855ed2e0cfc63..6e51346ee9a1d 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -90,6 +90,7 @@ import ( "github.com/gravitational/teleport/lib/cache" "github.com/gravitational/teleport/lib/circleci" "github.com/gravitational/teleport/lib/cloud" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/gcp" @@ -343,9 +344,10 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) { } keystoreOpts := &keystore.Options{ - HostUUID: cfg.HostUUID, - ClusterName: cfg.ClusterName, - CloudClients: cfg.CloudClients, + HostUUID: cfg.HostUUID, + ClusterName: cfg.ClusterName, + CloudClients: cfg.CloudClients, + AuthPreferenceGetter: cfg.ClusterConfiguration, } if cfg.KeyStoreConfig.PKCS11 != (servicecfg.PKCS11Config{}) { if !modules.GetModules().Features().HSM { @@ -6715,62 +6717,80 @@ func (a *Server) addAdditionalTrustedKeysAtomic(ctx context.Context, ca types.Ce // Keep this function in sync with lib/service/suite/suite.go:NewTestCAWithConfig(). func newKeySet(ctx context.Context, keyStore *keystore.Manager, caID types.CertAuthID) (types.CAKeySet, error) { var keySet types.CAKeySet + + // Add SSH keys if necessary. switch caID.Type { - case types.UserCA, types.HostCA: - sshKeyPair, err := keyStore.NewSSHKeyPair(ctx) - if err != nil { - return keySet, trace.Wrap(err) - } - tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName) + case types.UserCA, types.HostCA, types.OpenSSHCA: + sshKeyPair, err := keyStore.NewSSHKeyPair(ctx, sshCAKeyPurpose(caID.Type)) if err != nil { return keySet, trace.Wrap(err) } keySet.SSH = append(keySet.SSH, sshKeyPair) - keySet.TLS = append(keySet.TLS, tlsKeyPair) - case types.DatabaseCA, types.DatabaseClientCA: - // Database CA only contains TLS cert. - tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName) + } + + // Add TLS keys if necessary. + switch caID.Type { + case types.UserCA, types.HostCA, types.DatabaseCA, types.DatabaseClientCA, types.SAMLIDPCA, types.SPIFFECA: + tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName, tlsCAKeyPurpose(caID.Type)) if err != nil { return keySet, trace.Wrap(err) } keySet.TLS = append(keySet.TLS, tlsKeyPair) - case types.OpenSSHCA: - // OpenSSH CA only contains a SSH key pair. - sshKeyPair, err := keyStore.NewSSHKeyPair(ctx) - if err != nil { - return keySet, trace.Wrap(err) - } - keySet.SSH = append(keySet.SSH, sshKeyPair) - case types.JWTSigner, types.OIDCIdPCA: - jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx) + } + + // Add JWT keys if necessary. + switch caID.Type { + case types.JWTSigner, types.OIDCIdPCA, types.SPIFFECA: + jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx, jwtCAKeyPurpose(caID.Type)) if err != nil { return keySet, trace.Wrap(err) } keySet.JWT = append(keySet.JWT, jwtKeyPair) + } + + return keySet, nil +} + +func sshCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose { + switch caType { + case types.UserCA: + return cryptosuites.UserCASSH + case types.HostCA: + return cryptosuites.HostCASSH + case types.OpenSSHCA: + return cryptosuites.OpenSSHCASSH + } + return cryptosuites.KeyPurposeUnspecified +} + +func tlsCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose { + switch caType { + case types.UserCA: + return cryptosuites.UserCATLS + case types.HostCA: + return cryptosuites.HostCATLS + case types.DatabaseCA: + return cryptosuites.DatabaseCATLS + case types.DatabaseClientCA: + return cryptosuites.DatabaseClientCATLS case types.SAMLIDPCA: - // SAML IDP CA only contains TLS certs. - tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName) - if err != nil { - return keySet, trace.Wrap(err) - } - keySet.TLS = append(keySet.TLS, tlsKeyPair) + return cryptosuites.SAMLIdPCATLS case types.SPIFFECA: - tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName) - if err != nil { - return keySet, trace.Wrap(err) - } - keySet.TLS = append(keySet.TLS, tlsKeyPair) - // Whilst we don't currently support JWT-SVIDs, we will eventually. So - // generate a JWT keypair. - jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx) - if err != nil { - return keySet, trace.Wrap(err) - } - keySet.JWT = append(keySet.JWT, jwtKeyPair) - default: - return keySet, trace.BadParameter("unknown ca type: %s", caID.Type) + return cryptosuites.SPIFFECATLS } - return keySet, nil + return cryptosuites.KeyPurposeUnspecified +} + +func jwtCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose { + switch caType { + case types.JWTSigner: + return cryptosuites.JWTCAJWT + case types.OIDCIdPCA: + return cryptosuites.OIDCIdPCAJWT + case types.SPIFFECA: + return cryptosuites.SPIFFECAJWT + } + return cryptosuites.KeyPurposeUnspecified } // ensureLocalAdditionalKeys adds additional trusted keys to the CA if they are not diff --git a/lib/auth/keystore/aws_kms.go b/lib/auth/keystore/aws_kms.go index 1db86295f09ae..6fc681ca69a2b 100644 --- a/lib/auth/keystore/aws_kms.go +++ b/lib/auth/keystore/aws_kms.go @@ -19,6 +19,8 @@ package keystore import ( "context" "crypto" + "crypto/ecdsa" + "crypto/rsa" "crypto/x509" "errors" "fmt" @@ -43,6 +45,7 @@ import ( "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib/cloud" awslib "github.com/gravitational/teleport/lib/cloud/aws" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" ) @@ -108,13 +111,20 @@ func (a *awsKMSKeystore) keyTypeDescription() string { return fmt.Sprintf("AWS KMS keys in account %s and region %s", a.awsAccount, a.awsRegion) } -// generateRSA creates a new RSA private key and returns its identifier and a crypto.Signer. The returned +// generateKey creates a new private key and returns its identifier and a crypto.Signer. The returned // identifier can be passed to getSigner later to get an equivalent crypto.Signer. -func (a *awsKMSKeystore) generateRSA(ctx context.Context, _ ...rsaKeyOption) ([]byte, crypto.Signer, error) { +func (a *awsKMSKeystore) generateKey(ctx context.Context, algorithm cryptosuites.Algorithm, opts ...rsaKeyOption) ([]byte, crypto.Signer, error) { + alg, err := awsAlgorithm(algorithm) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + a.logger.InfoContext(ctx, "Creating new AWS KMS keypair.", "algorithm", alg) + output, err := a.kms.CreateKey(&kms.CreateKeyInput{ Description: aws.String("Teleport CA key"), - KeySpec: aws.String("RSA_2048"), - KeyUsage: aws.String("SIGN_VERIFY"), + KeySpec: &alg, + KeyUsage: aws.String(kms.KeyUsageTypeSignVerify), Tags: []*kms.Tag{ { TagKey: aws.String(clusterTagKey), @@ -141,6 +151,16 @@ func (a *awsKMSKeystore) generateRSA(ctx context.Context, _ ...rsaKeyOption) ([] return keyID, signer, nil } +func awsAlgorithm(alg cryptosuites.Algorithm) (string, error) { + switch alg { + case cryptosuites.RSA2048: + return kms.KeySpecRsa2048, nil + case cryptosuites.ECDSAP256: + return kms.KeySpecEccNistP256, nil + } + return "", trace.BadParameter("unsupported algorithm: %v", alg) +} + // getSigner returns a crypto.Signer for the given key identifier, if it is found. func (a *awsKMSKeystore) getSigner(ctx context.Context, rawKey []byte, publicKey crypto.PublicKey) (crypto.Signer, error) { keyID, err := parseAWSKMSKeyID(rawKey) @@ -236,16 +256,23 @@ func (a *awsKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpt var signingAlg string switch opts.HashFunc() { case crypto.SHA256: - signingAlg = "RSASSA_PKCS1_V1_5_SHA_256" + switch a.pub.(type) { + case *rsa.PublicKey: + signingAlg = kms.SigningAlgorithmSpecRsassaPkcs1V15Sha256 + case *ecdsa.PublicKey: + signingAlg = kms.SigningAlgorithmSpecEcdsaSha256 + default: + return nil, trace.BadParameter("unsupported hash func %q for AWS KMS key type %T", opts.HashFunc(), a.pub) + } case crypto.SHA512: - signingAlg = "RSASSA_PKCS1_V1_5_SHA_512" + signingAlg = kms.SigningAlgorithmSpecRsassaPkcs1V15Sha512 default: return nil, trace.BadParameter("unsupported hash func %q for AWS KMS key", opts.HashFunc()) } output, err := a.kms.Sign(&kms.SignInput{ KeyId: aws.String(a.keyARN), Message: digest, - MessageType: aws.String("DIGEST"), + MessageType: aws.String(kms.MessageTypeDigest), SigningAlgorithm: aws.String(signingAlg), }) if err != nil { @@ -344,8 +371,10 @@ func (a *awsKMSKeystore) deleteUnusedKeys(ctx context.Context, activeKeys [][]by } return trace.Wrap(err, "failed to fetch tags for AWS KMS key %q", keyARN) } + + clusterName := a.clusterName.GetClusterName() if !slices.ContainsFunc(output.Tags, func(tag *kms.Tag) bool { - return aws.StringValue(tag.TagKey) == clusterTagKey && aws.StringValue(tag.TagValue) == a.clusterName.GetClusterName() + return aws.StringValue(tag.TagKey) == clusterTagKey && aws.StringValue(tag.TagValue) == clusterName }) { // This key was not created by this Teleport cluster, never delete it. return nil diff --git a/lib/auth/keystore/aws_kms_test.go b/lib/auth/keystore/aws_kms_test.go index 31056f9cd45a4..5adaa6927facb 100644 --- a/lib/auth/keystore/aws_kms_test.go +++ b/lib/auth/keystore/aws_kms_test.go @@ -20,6 +20,7 @@ import ( "context" "crypto" "crypto/rand" + "crypto/x509" "fmt" "slices" "strconv" @@ -42,6 +43,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/cloud" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" ) @@ -67,8 +69,9 @@ func TestAWSKMS_DeleteUnusedKeys(t *testing.T) { clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ClusterName: "test-cluster"}) require.NoError(t, err) opts := &Options{ - ClusterName: clusterName, - HostUUID: "uuid", + ClusterName: clusterName, + HostUUID: "uuid", + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, CloudClients: &cloud.TestCloudClients{ KMS: fakeKMS, STS: &fakeAWSSTSClient{ @@ -82,8 +85,8 @@ func TestAWSKMS_DeleteUnusedKeys(t *testing.T) { totalKeys := pageSize * 3 for i := 0; i < totalKeys; i++ { - _, err := keyStore.NewSSHKeyPair(ctx) - require.NoError(t, err) + _, err := keyStore.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) + require.NoError(t, err, trace.DebugReport(err)) } // Newly created keys should not be deleted. @@ -104,6 +107,7 @@ func TestAWSKMS_DeleteUnusedKeys(t *testing.T) { // Insert a key created by a different Teleport cluster, it should not be // deleted by the keystore. output, err := fakeKMS.CreateKey(&kms.CreateKeyInput{ + KeySpec: aws.String(kms.KeySpecEccNistP256), Tags: []*kms.Tag{ &kms.Tag{ TagKey: aws.String(clusterTagKey), @@ -137,8 +141,9 @@ func TestAWSKMS_WrongAccount(t *testing.T) { clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ClusterName: "test-cluster"}) require.NoError(t, err) opts := &Options{ - ClusterName: clusterName, - HostUUID: "uuid", + ClusterName: clusterName, + HostUUID: "uuid", + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, CloudClients: &cloud.TestCloudClients{ KMS: newFakeAWSKMSService(t, clock, "222222222222", "us-west-2", 1000), STS: &fakeAWSSTSClient{ @@ -169,8 +174,9 @@ func TestAWSKMS_RetryWhilePending(t *testing.T) { clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ClusterName: "test-cluster"}) require.NoError(t, err) opts := &Options{ - ClusterName: clusterName, - HostUUID: "uuid", + ClusterName: clusterName, + HostUUID: "uuid", + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, CloudClients: &cloud.TestCloudClients{ KMS: kms, STS: &fakeAWSSTSClient{ @@ -188,7 +194,7 @@ func TestAWSKMS_RetryWhilePending(t *testing.T) { clock.BlockUntil(2) clock.Advance(kms.keyPendingDuration) }() - _, err = manager.NewSSHKeyPair(ctx) + _, err = manager.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) require.NoError(t, err) // Test with two retries required. @@ -199,7 +205,7 @@ func TestAWSKMS_RetryWhilePending(t *testing.T) { clock.BlockUntil(2) clock.Advance(kms.keyPendingDuration / 2) }() - _, err = manager.NewSSHKeyPair(ctx) + _, err = manager.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) require.NoError(t, err) // Test a timeout. @@ -210,7 +216,7 @@ func TestAWSKMS_RetryWhilePending(t *testing.T) { clock.BlockUntil(2) clock.Advance(pendingKeyTimeout) }() - _, err = manager.NewSSHKeyPair(ctx) + _, err = manager.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) require.Error(t, err) } @@ -236,6 +242,7 @@ func newFakeAWSKMSService(t *testing.T, clock clockwork.Clock, account string, r type fakeAWSKMSKey struct { arn string + privKeyPEM []byte tags []*kms.Tag creationDate time.Time state string @@ -254,8 +261,25 @@ func (f *fakeAWSKMSService) CreateKey(input *kms.CreateKeyInput) (*kms.CreateKey if f.keyPendingDuration > 0 { state = "Pending" } + var privKeyPEM []byte + switch aws.StringValue(input.KeySpec) { + case kms.KeySpecRsa2048: + privKeyPEM = testRSAPrivateKeyPEM + case kms.KeySpecEccNistP256: + signer, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + if err != nil { + return nil, trace.Wrap(err) + } + privKeyPEM, err = keys.MarshalPrivateKey(signer) + if err != nil { + return nil, trace.Wrap(err) + } + default: + return nil, trace.BadParameter("unsupported KeySpec %v", input.KeySpec) + } f.keys = append(f.keys, &fakeAWSKMSKey{ arn: a.String(), + privKeyPEM: privKeyPEM, tags: input.Tags, creationDate: f.clock.Now(), state: state, @@ -276,8 +300,16 @@ func (f *fakeAWSKMSService) GetPublicKeyWithContext(ctx context.Context, input * if key.state != "Enabled" { return nil, trace.NotFound("key %q is not enabled", aws.StringValue(input.KeyId)) } + privateKey, err := keys.ParsePrivateKey(key.privKeyPEM) + if err != nil { + return nil, trace.Wrap(err) + } + der, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, trace.Wrap(err) + } return &kms.GetPublicKeyOutput{ - PublicKey: testRawPublicKeyDER, + PublicKey: der, }, nil } @@ -289,19 +321,19 @@ func (f *fakeAWSKMSService) Sign(input *kms.SignInput) (*kms.SignOutput, error) if key.state != "Enabled" { return nil, trace.NotFound("key %q is not enabled", aws.StringValue(input.KeyId)) } + signer, err := keys.ParsePrivateKey(key.privKeyPEM) + if err != nil { + return nil, trace.Wrap(err) + } var opts crypto.SignerOpts switch aws.StringValue(input.SigningAlgorithm) { - case "RSASSA_PKCS1_V1_5_SHA_256": + case kms.SigningAlgorithmSpecRsassaPkcs1V15Sha256, kms.SigningAlgorithmSpecEcdsaSha256: opts = crypto.SHA256 - case "RSASSA_PKCS1_V1_5_SHA_512": + case kms.SigningAlgorithmSpecRsassaPkcs1V15Sha512: opts = crypto.SHA512 default: return nil, trace.BadParameter("unsupported SigningAlgorithm %q", aws.StringValue(input.SigningAlgorithm)) } - signer, err := keys.ParsePrivateKey(testRawPrivateKey) - if err != nil { - return nil, trace.Wrap(err) - } signature, err := signer.Sign(rand.Reader, input.Message, opts) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/keystore/gcp_kms.go b/lib/auth/keystore/gcp_kms.go index cb607913ffb6c..443fbbb42e581 100644 --- a/lib/auth/keystore/gcp_kms.go +++ b/lib/auth/keystore/gcp_kms.go @@ -38,6 +38,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/keystore/internal/faketime" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" ) @@ -105,27 +106,17 @@ func (g *gcpKMSKeyStore) keyTypeDescription() string { return fmt.Sprintf("GCP KMS keys in keyring %s", g.keyRing) } -// generateRSA creates a new RSA private key and returns its identifier and a -// crypto.Signer. The returned identifier for gcpKMSKeyStore encoded the full -// GCP KMS key version name, and can be passed to getSigner later to get the same -// crypto.Signer. -func (g *gcpKMSKeyStore) generateRSA(ctx context.Context, opts ...rsaKeyOption) ([]byte, crypto.Signer, error) { - options := &rsaKeyOptions{} - for _, opt := range opts { - opt(options) - } - - var alg kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm - switch options.digestAlgorithm { - case crypto.SHA256, 0: - alg = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256 - case crypto.SHA512: - alg = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512 - default: - return nil, nil, trace.BadParameter("unsupported digest algorithm: %v", options.digestAlgorithm) +// generateKey creates a new private key and returns its identifier and a crypto.Signer. The returned +// identifier for gcpKMSKeyStore encodes the full GCP KMS key version name, and can be passed to getSigner +// later to get an equivalent crypto.Signer. +func (g *gcpKMSKeyStore) generateKey(ctx context.Context, algorithm cryptosuites.Algorithm, opts ...rsaKeyOption) ([]byte, crypto.Signer, error) { + alg, err := gcpAlgorithm(algorithm, opts...) + if err != nil { + return nil, nil, trace.Wrap(err) } keyUUID := uuid.NewString() + g.log.InfoContext(ctx, "Creating new GCP KMS keypair.", "id", keyUUID, "algorithm", alg.String()) req := &kmspb.CreateCryptoKeyRequest{ Parent: g.keyRing, @@ -157,6 +148,28 @@ func (g *gcpKMSKeyStore) generateRSA(ctx context.Context, opts ...rsaKeyOption) return keyID.marshal(), signer, nil } +func gcpAlgorithm(alg cryptosuites.Algorithm, opts ...rsaKeyOption) (kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm, error) { + rsaOpts := &rsaKeyOptions{} + for _, opt := range opts { + opt(rsaOpts) + } + + switch alg { + case cryptosuites.RSA2048: + switch rsaOpts.digestAlgorithm { + case crypto.SHA256, 0: + return kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, nil + case crypto.SHA512: + return kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512, nil + default: + return kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, trace.BadParameter("unsupported digest algorithm: %v", rsaOpts.digestAlgorithm) + } + case cryptosuites.ECDSAP256: + return kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, nil + } + return kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED, trace.BadParameter("unsupported algorithm: %v", alg) +} + // getSigner returns a crypto.Signer for the given pem-encoded private key. func (g *gcpKMSKeyStore) getSigner(ctx context.Context, rawKey []byte, publicKey crypto.PublicKey) (crypto.Signer, error) { keyID, err := parseGCPKMSKeyID(rawKey) diff --git a/lib/auth/keystore/gcp_kms_test.go b/lib/auth/keystore/gcp_kms_test.go index 3405c7b65c550..b61af3dd5a206 100644 --- a/lib/auth/keystore/gcp_kms_test.go +++ b/lib/auth/keystore/gcp_kms_test.go @@ -48,44 +48,14 @@ import ( "github.com/gravitational/teleport/api/utils/grpc/interceptors" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/keystore/internal/faketime" - "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/cloud" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/jwt" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" ) -const ( - testPrivateKeyPem = `-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0El4Wi5gPO64E/brJ8jYxj+s4UELoMxUyVmw9wj0utmzL1OF -zTHhyr2NCfpEixYOKR81JCrfcyi5q24pgp8oefLt1wT1lyTjSwh8DFEsRK1vhHfq -O8dwG1Snx3zx9u92u7ZmbTpzVWFcSwK6C7LKjvFYuBrSt6qZ2rwg3jURqiql5i9Q -wV7Q+uC/WW5epJ3rX8SeChtq0laaLiw3bMQ23cFiFpZFGUEsYmMDPiSt/LSjwFAb -iDulz0BozR+WV+i13G6LZat+QmvHjSi4QyWUpEDF+VVz2MZ1WnvY10JX/kngE81a -qszchd4ThpjqJY17Dqs6R+wNLzKZ1iaMO5AGXQIDAQABAoIBAQCfptEjfsyxp+Fd -HDTfh+nw+7nN5we8tyJ+O8uTbz/3QQtByWmUARorRuOmtDh5y+wKxSr8kAg6wwqe -RpB22PwzjWuVFu4QbmvyhYxf/JBMDAygozHdpF9f86GvHSxytNZzx7n3G4hv93LA -5FQqx17P9lqks5q0wYWwzeb7q/3gSfINtq/aqK76W+vg9hxI1V99PP03a64q6BYg -XbOpK6p+hiONsV2nB6rYeTZ+RhGuXE97MVT1XGRVgEtzlxBAWI/SS4EaBV9MhI9/ -JF+yPyR7P8LpAqgSj0Q2XvOmn0wuW4PgWkhliBTAonxED2rHJQLWNDTqoCHZYpOZ -erhaFXPBAoGBAN0C26C/ajk3wa7bm6CmBLro1tbBK7/xeUUSgHXw7OUu2z3yRJEv -ZxroeeGvP0yW/NFjfWWqTVszDqreehegsQfqz6YBoaqXooVr6MWUOeIrCYJBUMWI -o48rc+f5BpB6c3DdvAAsn6aLGEZJHJhqNlNSGormicObpgaYpWcF/nyZAoGBAPFC -7UmmUuymBybuMYD6hfHRz6XPsbiF2zJue/bGXETmrZ/d95svWK16lUOP9AGzG7UC -5GyCaEOmOwMWagiuglZTknbrOgT8/N8+5l0T2cu3w8jy2bcAOMxow7QNhV0ZVGPB -d5F4mVbq3cownUbiY2LV5d3aYa8DOVb+R66Y7I5lAoGAKPvTsH5ue0fModlVhbfj -nql41YAi1cg4ncdtjPFtbJ6Ax376mhW5P/MmTuSJj3FcVpPleAnZqHTSXns9Fs6U -pYw0j2s0CIdv+t/k3Wa8SSWD8OSdztOkyPLc3oJ+ZiJe7+oeZ8XeoSqgCMCcDeN8 -SX0rMODJYT2mzwhVe8JPy9kCgYEAmCuWbvWxKAIwUKW8I5XgFf438mVluvTypIR7 -O9MxL2Qv7r2aBw995y2CJ/ML/GZz+1+vo6E9Ei4u2muwxXkMTFa58re7CJppBIYv -1lVG8e8eVgiWuY4yRPtvNImyrF3llGXafK6MSP4qlfTDvoncFeLD8YJkSnbGG9CW -ddGOouECgYBh3WFOnERRyviW/LTVspYOSwbOK3f17yyd13kuFJWjcULCSob2mwIk -0eHP1qt9ZxIIXJngrKz5nssgAvHKWu1q245MBZ7rChuBXJLwvY8Puh0C54JJbhlb -K5UACTho05E0hm3kAJ+pV5APw4UdBFPt90K5nx1OI8nmhxYPqR4V3w== ------END RSA PRIVATE KEY-----` -) - // fakeGCPKMSServer is a GRPC service implementation which fakes the real GCP // KMS service, to be used in unit tests. type fakeGCPKMSServer struct { @@ -119,7 +89,7 @@ func withInitialKeyState(state kmspb.CryptoKeyVersion_CryptoKeyVersionState) fak } type keyState struct { - pem string + pem []byte cryptoKey *kmspb.CryptoKey cryptoKeyVersion *kmspb.CryptoKeyVersion } @@ -138,9 +108,26 @@ func (f *fakeGCPKMSServer) CreateCryptoKey(ctx context.Context, req *kmspb.Creat Algorithm: cryptoKey.VersionTemplate.Algorithm, } + var pem []byte + switch cryptoKey.VersionTemplate.Algorithm { + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: + pem = testRSAPrivateKeyPEM + case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: + signer, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + if err != nil { + return nil, trace.Wrap(err) + } + pem, err = keys.MarshalPrivateKey(signer) + if err != nil { + return nil, trace.Wrap(err) + } + default: + return nil, trace.BadParameter("unsupported algorithm %v", cryptoKey.VersionTemplate.Algorithm) + } + f.mu.Lock() f.keyVersions[keyVersionName] = &keyState{ - pem: testPrivateKeyPem, + pem: pem, cryptoKey: cryptoKey, cryptoKeyVersion: cryptoKeyVersion, } @@ -160,24 +147,23 @@ func (f *fakeGCPKMSServer) GetPublicKey(ctx context.Context, req *kmspb.GetPubli return nil, trace.BadParameter("cannot fetch public key, state has value %s", keyState.cryptoKeyVersion.State) } - signer, err := keys.ParsePrivateKey([]byte(keyState.pem)) + signer, err := keys.ParsePrivateKey(keyState.pem) if err != nil { return nil, trace.Wrap(err) } - pubKeyBytes, err := x509.MarshalPKIXPublicKey(signer.Public()) + // Not using [keys.MarshalPublicKey] here because GCP always encodes RSA keys in PKIX format, not PKCS#1. + pubKeyDER, err := x509.MarshalPKIXPublicKey(signer.Public()) if err != nil { return nil, trace.Wrap(err) } - - block := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - } - pubKeyPem := pem.EncodeToMemory(block) + pubKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: keys.PKIXPublicKeyType, + Bytes: pubKeyDER, + }) return &kmspb.PublicKey{ - Pem: string(pubKeyPem), + Pem: string(pubKeyPEM), }, nil } @@ -192,7 +178,7 @@ func (f *fakeGCPKMSServer) AsymmetricSign(ctx context.Context, req *kmspb.Asymme return nil, trace.BadParameter("cannot fetch key, state has value %s", keyState.cryptoKeyVersion.State) } - signer, err := keys.ParsePrivateKey([]byte(keyState.pem)) + signer, err := keys.ParsePrivateKey(keyState.pem) if err != nil { return nil, trace.Wrap(err) } @@ -201,9 +187,10 @@ func (f *fakeGCPKMSServer) AsymmetricSign(ctx context.Context, req *kmspb.Asymme var alg crypto.Hash switch typedDigest := req.Digest.Digest.(type) { case *kmspb.Digest_Sha256: - if keyState.cryptoKeyVersion.Algorithm != kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256 { - return nil, trace.BadParameter( - "requested key uses algorithm %s which cannot handle a 256 bit digest", + switch keyState.cryptoKeyVersion.Algorithm { + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: + default: + return nil, trace.BadParameter("requested key uses algorithm %s which cannot handle a 256 bit digest", keyState.cryptoKeyVersion.Algorithm) } digest = typedDigest.Sha256 @@ -416,11 +403,12 @@ func TestGCPKMSKeystore(t *testing.T) { KeyRing: "test-keyring", }, }, &Options{ - ClusterName: clusterName, - HostUUID: "test-host-id", - CloudClients: &cloud.TestCloudClients{}, - kmsClient: kmsClient, - faketimeOverride: clock, + ClusterName: clusterName, + HostUUID: "test-host-id", + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, + CloudClients: &cloud.TestCloudClients{}, + kmsClient: kmsClient, + faketimeOverride: clock, }) require.NoError(t, err, "error while creating test keystore manager") @@ -484,19 +472,19 @@ func TestGCPKMSKeystore(t *testing.T) { }() // Test key creation. - sshKeyPair, err := manager.NewSSHKeyPair(clientContext) + sshKeyPair, err := manager.NewSSHKeyPair(clientContext, cryptosuites.UserCASSH) if tc.expectNewKeyPairError { require.Error(t, err, "expected to get error generating SSH keypair, got nil") return } require.NoError(t, err, "unexpected error while generating SSH keypair") - jwtKeyPair, err := manager.NewJWTKeyPair(clientContext) - require.NoError(t, err, "unexpected error creating JWT keypair") - - tlsKeyPair, err := manager.NewTLSKeyPair(clientContext, "test-cluster") + tlsKeyPair, err := manager.NewTLSKeyPair(clientContext, "test-cluster", cryptosuites.UserCATLS) require.NoError(t, err, "unexpected error creating TLS keypair") + jwtKeyPair, err := manager.NewJWTKeyPair(clientContext, cryptosuites.JWTCAJWT) + require.NoError(t, err, "unexpected error creating JWT keypair") + // Put all the keys into a "CA" so that the keystore manager can // select them and we can test the public API. ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ @@ -510,11 +498,9 @@ func TestGCPKMSKeystore(t *testing.T) { }) require.NoError(t, err, "unexpected error creating CA") - // Generate a test private key that will be the basis of test certs - // to be signed. - keygen := testauthority.New() - clientPrivKey, err := keygen.GeneratePrivateKey() - require.NoError(t, err, "unexpected error generating test private key") + // Client private key that will be the basis of test certs to be signed. + clientPrivKey, err := keys.ParsePrivateKey(testRSAPrivateKeyPEM) + require.NoError(t, err) // Test signing an SSH certificate. t.Run("ssh", func(t *testing.T) { @@ -538,7 +524,7 @@ func TestGCPKMSKeystore(t *testing.T) { require.Error(t, err, "expected to get error signing SSH cert") return } - require.NoError(t, err, "unexpected error signing SSH certificate") + require.NoError(t, err, trace.DebugReport(err)) }) // Test signing a TLS certificate. @@ -597,7 +583,7 @@ func TestGCPKMSKeystore(t *testing.T) { require.Error(t, err, "expected to get error signing JWT") return } - require.NoError(t, err, "unexpected error signing JWT") + require.NoError(t, err, "unexpected error signing JWT: %s", trace.DebugReport(err)) }) }) } @@ -695,10 +681,11 @@ func TestGCPKMSDeleteUnusedKeys(t *testing.T) { KeyRing: localKeyring, }, }, &Options{ - ClusterName: clusterName, - HostUUID: localHostID, - CloudClients: &cloud.TestCloudClients{}, - kmsClient: kmsClient, + ClusterName: clusterName, + HostUUID: localHostID, + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, + CloudClients: &cloud.TestCloudClients{}, + kmsClient: kmsClient, }) require.NoError(t, err, "error while creating test keystore manager") diff --git a/lib/auth/keystore/keystore_test.go b/lib/auth/keystore/keystore_test.go index a289b3a16942b..1276f95e3bbef 100644 --- a/lib/auth/keystore/keystore_test.go +++ b/lib/auth/keystore/keystore_test.go @@ -21,9 +21,11 @@ package keystore import ( "context" "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha256" + "errors" "testing" "time" @@ -37,13 +39,14 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/cloud" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" ) var ( - testRawPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- + testRSAPrivateKeyPEM = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAqiD2rRJ5kq7hP55eOCM9DtdkWPMI8PBKgxaAiQ9J9YF3aNur 98b8kACcTQ8ixSkHsLccVqRdt/Cnb7jtBSrwxJ9BN09fZEiyCvy7lwxNGBMQEaov 9UU722nvuWKb+EkHzcVV9ie9i8wM88xpzzYO8eda8FZjHxaaoe2lkrHiiOFQRubJ @@ -71,9 +74,8 @@ fPTgihJAeKdWbBmRMjIDe8hkz/oxR6JE2Ap+4G+KZtwVON4b+ucCYTQS+1CQp2Xc RPAMyjbzPhWQpfJnIxLcqGmvXxosABvs/b2CWaPqfCQhZIWpLeKW -----END RSA PRIVATE KEY----- `) - testRawSSHPublicKey = []byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqIPatEnmSruE/nl44Iz0O12RY8wjw8EqDFoCJD0n1gXdo26v3xvyQAJxNDyLFKQewtxxWpF238KdvuO0FKvDEn0E3T19kSLIK/LuXDE0YExARqi/1RTvbae+5Ypv4SQfNxVX2J72LzAzzzGnPNg7x51rwVmMfFpqh7aWSseKI4VBG5smodWFb5I0VA5Xo6xURNNmWDmuZaEDmsqIHobRB4sfKxIwltssw5evVVu7tGqiGarQAXoR0yCLHc4nPeov1gMpA8DOGPtWI/NPTs+//2+Hl+NdoTmJOE9Piffe5jU3Z8kCfOxxm9WanHG5I6rHBYGqRHMgl7PW+/cX7nEMv") - testRawPublicKeyDER = []byte{48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 170, 32, 246, 173, 18, 121, 146, 174, 225, 63, 158, 94, 56, 35, 61, 14, 215, 100, 88, 243, 8, 240, 240, 74, 131, 22, 128, 137, 15, 73, 245, 129, 119, 104, 219, 171, 247, 198, 252, 144, 0, 156, 77, 15, 34, 197, 41, 7, 176, 183, 28, 86, 164, 93, 183, 240, 167, 111, 184, 237, 5, 42, 240, 196, 159, 65, 55, 79, 95, 100, 72, 178, 10, 252, 187, 151, 12, 77, 24, 19, 16, 17, 170, 47, 245, 69, 59, 219, 105, 239, 185, 98, 155, 248, 73, 7, 205, 197, 85, 246, 39, 189, 139, 204, 12, 243, 204, 105, 207, 54, 14, 241, 231, 90, 240, 86, 99, 31, 22, 154, 161, 237, 165, 146, 177, 226, 136, 225, 80, 70, 230, 201, 168, 117, 97, 91, 228, 141, 21, 3, 149, 232, 235, 21, 17, 52, 217, 150, 14, 107, 153, 104, 64, 230, 178, 162, 7, 161, 180, 65, 226, 199, 202, 196, 140, 37, 182, 203, 48, 229, 235, 213, 86, 238, 237, 26, 168, 134, 106, 180, 0, 94, 132, 116, 200, 34, 199, 115, 137, 207, 122, 139, 245, 128, 202, 64, 240, 51, 134, 62, 213, 136, 252, 211, 211, 179, 239, 255, 219, 225, 229, 248, 215, 104, 78, 98, 78, 19, 211, 226, 125, 247, 185, 141, 77, 217, 242, 64, 159, 59, 28, 102, 245, 102, 167, 28, 110, 72, 234, 177, 193, 96, 106, 145, 28, 200, 37, 236, 245, 190, 253, 197, 251, 156, 67, 47, 2, 3, 1, 0, 1} - testRawPublicKeyPEM = []byte(`-----BEGIN RSA PUBLIC KEY----- + testRSASSHPublicKey = []byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqIPatEnmSruE/nl44Iz0O12RY8wjw8EqDFoCJD0n1gXdo26v3xvyQAJxNDyLFKQewtxxWpF238KdvuO0FKvDEn0E3T19kSLIK/LuXDE0YExARqi/1RTvbae+5Ypv4SQfNxVX2J72LzAzzzGnPNg7x51rwVmMfFpqh7aWSseKI4VBG5smodWFb5I0VA5Xo6xURNNmWDmuZaEDmsqIHobRB4sfKxIwltssw5evVVu7tGqiGarQAXoR0yCLHc4nPeov1gMpA8DOGPtWI/NPTs+//2+Hl+NdoTmJOE9Piffe5jU3Z8kCfOxxm9WanHG5I6rHBYGqRHMgl7PW+/cX7nEMv") + testRSAPublicKeyPEM = []byte(`-----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAqiD2rRJ5kq7hP55eOCM9DtdkWPMI8PBKgxaAiQ9J9YF3aNur98b8 kACcTQ8ixSkHsLccVqRdt/Cnb7jtBSrwxJ9BN09fZEiyCvy7lwxNGBMQEaov9UU7 22nvuWKb+EkHzcVV9ie9i8wM88xpzzYO8eda8FZjHxaaoe2lkrHiiOFQRubJqHVh @@ -81,7 +83,7 @@ W+SNFQOV6OsVETTZlg5rmWhA5rKiB6G0QeLHysSMJbbLMOXr1Vbu7Rqohmq0AF6E dMgix3OJz3qL9YDKQPAzhj7ViPzT07Pv/9vh5fjXaE5iThPT4n33uY1N2fJAnzsc ZvVmpxxuSOqxwWBqkRzIJez1vv3F+5xDLwIDAQAB -----END RSA PUBLIC KEY-----`) - testRawCert = []byte(`-----BEGIN CERTIFICATE----- + testRSACert = []byte(`-----BEGIN CERTIFICATE----- MIIDeTCCAmGgAwIBAgIRALmlBQhTQQiGIS/P0PwF97wwDQYJKoZIhvcNAQELBQAw VjEQMA4GA1UEChMHc2VydmVyMTEQMA4GA1UEAxMHc2VydmVyMTEwMC4GA1UEBRMn MjQ2NzY0MDEwMjczNTA2ODc3NjY1MDEyMTc3Mzg5MTkyODY5ODIwMB4XDTIxMDcx @@ -106,33 +108,33 @@ JhuTMEqUaAOZBoQLn+txjl3nu9WwTThJzlY0L4w= testPKCS11Key = []byte(`pkcs11:{"host_id": "server2", "key_id": "00000000-0000-0000-0000-000000000000"}`) testRawSSHKeyPair = &types.SSHKeyPair{ - PublicKey: testRawSSHPublicKey, - PrivateKey: testRawPrivateKey, + PublicKey: testRSASSHPublicKey, + PrivateKey: testRSAPrivateKeyPEM, PrivateKeyType: types.PrivateKeyType_RAW, } testRawTLSKeyPair = &types.TLSKeyPair{ - Cert: testRawCert, - Key: testRawPrivateKey, + Cert: testRSACert, + Key: testRSAPrivateKeyPEM, KeyType: types.PrivateKeyType_RAW, } testRawJWTKeyPair = &types.JWTKeyPair{ - PublicKey: testRawPublicKeyPEM, - PrivateKey: testRawPrivateKey, + PublicKey: testRSAPublicKeyPEM, + PrivateKey: testRSAPrivateKeyPEM, PrivateKeyType: types.PrivateKeyType_RAW, } testPKCS11SSHKeyPair = &types.SSHKeyPair{ - PublicKey: testRawSSHPublicKey, + PublicKey: testRSASSHPublicKey, PrivateKey: testPKCS11Key, PrivateKeyType: types.PrivateKeyType_PKCS11, } testPKCS11TLSKeyPair = &types.TLSKeyPair{ - Cert: testRawCert, + Cert: testRSACert, Key: testPKCS11Key, KeyType: types.PrivateKeyType_PKCS11, } testPKCS11JWTKeyPair = &types.JWTKeyPair{ - PublicKey: testRawPublicKeyPEM, + PublicKey: testRSAPublicKeyPEM, PrivateKey: testPKCS11Key, PrivateKeyType: types.PrivateKeyType_PKCS11, } @@ -151,28 +153,45 @@ func TestBackends(t *testing.T) { t.Run(backendDesc.name, func(t *testing.T) { backend := backendDesc.backend - // create a key - key, signer, err := backend.generateRSA(ctx) - require.NoError(t, err, trace.DebugReport(err)) - require.NotNil(t, key) - require.NotNil(t, signer) - require.Equal(t, backendDesc.expectedKeyType, keyType(key)) - - // delete the key when we're done with it - t.Cleanup(func() { require.NoError(t, backend.deleteKey(ctx, key)) }) - - // get a signer from the key - signer, err = backend.getSigner(ctx, key, signer.Public()) - require.NoError(t, err) - require.NotNil(t, signer) + for _, tc := range []struct { + alg cryptosuites.Algorithm + verify func(pubkey any, hash, signature []byte) error + }{ + { + alg: cryptosuites.RSA2048, + verify: func(pubkey any, hash, signature []byte) error { + return rsa.VerifyPKCS1v15(pubkey.(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) + }, + }, + { + alg: cryptosuites.ECDSAP256, + verify: func(pubkey any, hash, signature []byte) error { + if !ecdsa.VerifyASN1(pubkey.(*ecdsa.PublicKey), messageHash[:], signature) { + return errors.New("ECDSA signature is invalid") + } + return nil + }, + }, + } { + t.Run(tc.alg.String(), func(t *testing.T) { + // create a key + key, signer, err := backend.generateKey(ctx, tc.alg) + require.NoError(t, err, trace.DebugReport(err)) + require.Equal(t, backendDesc.expectedKeyType, keyType(key)) + + // delete the key when we're done with it + t.Cleanup(func() { require.NoError(t, backend.deleteKey(ctx, key)) }) + + // get a signer from the key + signer, err = backend.getSigner(ctx, key, signer.Public()) + require.NoError(t, err) - // try signing something - signature, err := signer.Sign(rand.Reader, messageHash[:], crypto.SHA256) - require.NoError(t, err, trace.DebugReport(err)) - require.NotEmpty(t, signature) - // make sure we can verify the signature with a "known good" rsa implementation - err = rsa.VerifyPKCS1v15(signer.Public().(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) - require.NoError(t, err) + // try signing something + signature, err := signer.Sign(rand.Reader, messageHash[:], crypto.SHA256) + require.NoError(t, err, trace.DebugReport(err)) + require.NoError(t, tc.verify(signer.Public(), messageHash[:], signature)) + }) + } }) } @@ -183,14 +202,13 @@ func TestBackends(t *testing.T) { // create some keys to test deleteUnusedKeys const numKeys = 3 rawPrivateKeys := make([][]byte, numKeys) - rawPublicKeys := make([][]byte, numKeys) + publicKeys := make([]crypto.PublicKey, numKeys) for i := 0; i < numKeys; i++ { var signer crypto.Signer var err error - rawPrivateKeys[i], signer, err = backend.generateRSA(ctx) - require.NoError(t, err) - rawPublicKeys[i], err = keys.MarshalPublicKey(signer.Public()) + rawPrivateKeys[i], signer, err = backend.generateKey(ctx, cryptosuites.ECDSAP256) require.NoError(t, err) + publicKeys[i] = signer.Public() } // AWS KMS keystore will not delete any keys created in the past 5 @@ -203,14 +221,14 @@ func TestBackends(t *testing.T) { require.NoError(t, err, trace.DebugReport(err)) // make sure the first key is still good - signer, err := backend.getSigner(ctx, rawPrivateKeys[0], rawPublicKeys[0]) + signer, err := backend.getSigner(ctx, rawPrivateKeys[0], publicKeys[0]) require.NoError(t, err) _, err = signer.Sign(rand.Reader, messageHash[:], crypto.SHA256) require.NoError(t, err) // make sure all other keys are deleted for i := 1; i < numKeys; i++ { - signer, err := backend.getSigner(ctx, rawPrivateKeys[i], rawPublicKeys[0]) + signer, err := backend.getSigner(ctx, rawPrivateKeys[i], publicKeys[0]) if err != nil { // For PKCS11 we expect to fail to get the signer, for cloud // KMS backends it won't fail until actually signing. @@ -252,7 +270,7 @@ func TestManager(t *testing.T) { for _, backendDesc := range pack.backends { t.Run(backendDesc.name, func(t *testing.T) { - manager, err := NewManager(ctx, &backendDesc.config, pack.opts) + manager, err := NewManager(ctx, &backendDesc.config, backendDesc.opts) require.NoError(t, err) // Delete all keys to clean up the test. @@ -260,15 +278,15 @@ func TestManager(t *testing.T) { require.NoError(t, manager.DeleteUnusedKeys(context.Background(), nil /*activeKeys*/)) }) - sshKeyPair, err := manager.NewSSHKeyPair(ctx) + sshKeyPair, err := manager.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) require.NoError(t, err) require.Equal(t, backendDesc.expectedKeyType, sshKeyPair.PrivateKeyType) - tlsKeyPair, err := manager.NewTLSKeyPair(ctx, clusterName) + tlsKeyPair, err := manager.NewTLSKeyPair(ctx, clusterName, cryptosuites.UserCATLS) require.NoError(t, err) require.Equal(t, backendDesc.expectedKeyType, tlsKeyPair.KeyType) - jwtKeyPair, err := manager.NewJWTKeyPair(ctx) + jwtKeyPair, err := manager.NewJWTKeyPair(ctx, cryptosuites.JWTCAJWT) require.NoError(t, err) require.Equal(t, backendDesc.expectedKeyType, jwtKeyPair.PrivateKeyType) @@ -392,7 +410,6 @@ func TestManager(t *testing.T) { } type testPack struct { - opts *Options backends []*backendDesc clock clockwork.FakeClock } @@ -400,6 +417,7 @@ type testPack struct { type backendDesc struct { name string config servicecfg.KeystoreConfig + opts *Options backend backend expectedKeyType types.PrivateKeyType unusedRawKey []byte @@ -427,10 +445,11 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { }) require.NoError(t, err) - opts := &Options{ - ClusterName: clusterName, - HostUUID: hostUUID, - Logger: logger, + baseOpts := Options{ + ClusterName: clusterName, + HostUUID: hostUUID, + Logger: logger, + AuthPreferenceGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1}, CloudClients: &cloud.TestCloudClients{ KMS: newFakeAWSKMSService(t, clock, "123456789012", "us-west-2", 100), STS: &fakeAWSSTSClient{ @@ -445,17 +464,19 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { backends = append(backends, &backendDesc{ name: "software", config: servicecfg.KeystoreConfig{}, + opts: &baseOpts, backend: softwareBackend, - unusedRawKey: testRawPrivateKey, + unusedRawKey: testRSAPrivateKeyPEM, deletionDoesNothing: true, }) if config, ok := softHSMTestConfig(t); ok { - backend, err := newPKCS11KeyStore(&config.PKCS11, opts) + backend, err := newPKCS11KeyStore(&config.PKCS11, &baseOpts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "softhsm", config: config, + opts: &baseOpts, backend: backend, expectedKeyType: types.PrivateKeyType_PKCS11, unusedRawKey: unusedPKCS11Key, @@ -463,11 +484,12 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { } if config, ok := yubiHSMTestConfig(t); ok { - backend, err := newPKCS11KeyStore(&config.PKCS11, opts) + backend, err := newPKCS11KeyStore(&config.PKCS11, &baseOpts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "yubihsm", config: config, + opts: &baseOpts, backend: backend, expectedKeyType: types.PrivateKeyType_PKCS11, unusedRawKey: unusedPKCS11Key, @@ -475,11 +497,12 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { } if config, ok := cloudHSMTestConfig(t); ok { - backend, err := newPKCS11KeyStore(&config.PKCS11, opts) + backend, err := newPKCS11KeyStore(&config.PKCS11, &baseOpts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "yubihsm", config: config, + opts: &baseOpts, backend: backend, expectedKeyType: types.PrivateKeyType_PKCS11, unusedRawKey: unusedPKCS11Key, @@ -487,11 +510,15 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { } if config, ok := gcpKMSTestConfig(t); ok { - backend, err := newGCPKMSKeyStore(ctx, &config.GCPKMS, opts) + opts := baseOpts + opts.kmsClient = nil + + backend, err := newGCPKMSKeyStore(ctx, &config.GCPKMS, &opts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "gcp_kms", config: config, + opts: &opts, backend: backend, expectedKeyType: types.PrivateKeyType_GCP_KMS, unusedRawKey: gcpKMSKeyID{ @@ -505,11 +532,12 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { KeyRing: "test-keyring", }, } - fakeGCPKMSBackend, err := newGCPKMSKeyStore(ctx, &fakeGCPKMSConfig.GCPKMS, opts) + fakeGCPKMSBackend, err := newGCPKMSKeyStore(ctx, &fakeGCPKMSConfig.GCPKMS, &baseOpts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "fake_gcp_kms", config: fakeGCPKMSConfig, + opts: &baseOpts, backend: fakeGCPKMSBackend, expectedKeyType: types.PrivateKeyType_GCP_KMS, unusedRawKey: gcpKMSKeyID{ @@ -518,11 +546,16 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { }) if config, ok := awsKMSTestConfig(t); ok { - backend, err := newAWSKMSKeystore(ctx, &config.AWSKMS, opts) + opts := baseOpts + opts.CloudClients, err = cloud.NewClients() + require.NoError(t, err) + + backend, err := newAWSKMSKeystore(ctx, &config.AWSKMS, &opts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "aws_kms", config: config, + opts: &opts, backend: backend, expectedKeyType: types.PrivateKeyType_AWS_KMS, unusedRawKey: awsKMSKeyID{ @@ -545,11 +578,12 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { AWSRegion: "us-west-2", }, } - fakeAWSKMSBackend, err := newAWSKMSKeystore(ctx, &fakeAWSKMSConfig.AWSKMS, opts) + fakeAWSKMSBackend, err := newAWSKMSKeystore(ctx, &fakeAWSKMSConfig.AWSKMS, &baseOpts) require.NoError(t, err) backends = append(backends, &backendDesc{ name: "fake_aws_kms", config: fakeAWSKMSConfig, + opts: &baseOpts, backend: fakeAWSKMSBackend, expectedKeyType: types.PrivateKeyType_AWS_KMS, unusedRawKey: awsKMSKeyID{ @@ -566,7 +600,6 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { }) return &testPack{ - opts: opts, backends: backends, clock: clock, } diff --git a/lib/auth/keystore/manager.go b/lib/auth/keystore/manager.go index 654fa0f80dfb5..07d287417b584 100644 --- a/lib/auth/keystore/manager.go +++ b/lib/auth/keystore/manager.go @@ -37,6 +37,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/keystore/internal/faketime" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/tlsca" @@ -53,6 +54,8 @@ type Manager struct { // signers from, in preference order. [backendForNewKeys] is expected to be // the first element. usableSigningBackends []backend + + authPrefGetter cryptosuites.AuthPreferenceGetter } // rsaKeyOptions configure options for RSA key generation. @@ -63,7 +66,7 @@ type rsaKeyOptions struct { // rsaKeyOption is a functional option for RSA key generation. type rsaKeyOption func(*rsaKeyOptions) -func withDigestAlgorithm(alg crypto.Hash) rsaKeyOption { +func withRSADigestAlgorithm(alg crypto.Hash) rsaKeyOption { return func(opts *rsaKeyOptions) { opts.digestAlgorithm = alg } @@ -74,7 +77,7 @@ func withDigestAlgorithm(alg crypto.Hash) rsaKeyOption { type backend interface { // generateRSA creates a new key pair and returns its identifier and a crypto.Signer. The returned // identifier can be passed to getSigner later to get an equivalent crypto.Signer. - generateRSA(context.Context, ...rsaKeyOption) (keyID []byte, signer crypto.Signer, err error) + generateKey(context.Context, cryptosuites.Algorithm, ...rsaKeyOption) (keyID []byte, signer crypto.Signer, err error) // getSigner returns a crypto.Signer for the given key identifier, if it is found. // The public key is passed as well so that it does not need to be fetched @@ -108,6 +111,8 @@ type Options struct { ClusterName types.ClusterName // Logger is a logger to be used by the keystore. Logger *slog.Logger + // AuthPreferenceGetter provides the current cluster auth preference. + AuthPreferenceGetter cryptosuites.AuthPreferenceGetter // CloudClients provides cloud clients. CloudClients CloudClientProvider @@ -125,6 +130,9 @@ func (opts *Options) CheckAndSetDefaults() error { if opts.CloudClients == nil { return trace.BadParameter("CloudClients is required") } + if opts.AuthPreferenceGetter == nil { + return trace.BadParameter("AuthPreferenceGetter is required") + } if opts.Logger == nil { opts.Logger = slog.With(teleport.ComponentKey, "Keystore") } @@ -171,6 +179,7 @@ func NewManager(ctx context.Context, cfg *servicecfg.KeystoreConfig, opts *Optio return &Manager{ backendForNewKeys: backendForNewKeys, usableSigningBackends: usableSigningBackends, + authPrefGetter: opts.AuthPreferenceGetter, }, nil } @@ -210,8 +219,11 @@ func (m *Manager) getSSHSigner(ctx context.Context, keySet types.CAKeySet) (ssh. if err != nil { return nil, trace.Wrap(err) } - // SHA-512 to match NewSSHKeyPair. - return toRSASHA512Signer(sshSigner), trace.Wrap(err) + if sshSigner.PublicKey().Type() == ssh.KeyAlgoRSA { + // SHA-512 to match NewSSHKeyPair. + sshSigner = toRSASHA512Signer(sshSigner) + } + return sshSigner, trace.Wrap(err) } } return nil, trace.NotFound("no usable SSH key pairs found") @@ -321,9 +333,13 @@ func (m *Manager) GetJWTSigner(ctx context.Context, ca types.CertAuthority) (cry } // NewSSHKeyPair generates a new SSH keypair in the keystore backend and returns it. -func (m *Manager) NewSSHKeyPair(ctx context.Context) (*types.SSHKeyPair, error) { +func (m *Manager) NewSSHKeyPair(ctx context.Context, purpose cryptosuites.KeyPurpose) (*types.SSHKeyPair, error) { + alg, err := cryptosuites.AlgorithmForKey(ctx, m.authPrefGetter, purpose) + if err != nil { + return nil, trace.Wrap(err) + } // The default hash length for SSH signers is 512 bits. - sshKey, cryptoSigner, err := m.backendForNewKeys.generateRSA(ctx, withDigestAlgorithm(crypto.SHA512)) + sshKey, cryptoSigner, err := m.backendForNewKeys.generateKey(ctx, alg, withRSADigestAlgorithm(crypto.SHA512)) if err != nil { return nil, trace.Wrap(err) } @@ -340,8 +356,12 @@ func (m *Manager) NewSSHKeyPair(ctx context.Context) (*types.SSHKeyPair, error) } // NewTLSKeyPair creates a new TLS keypair in the keystore backend and returns it. -func (m *Manager) NewTLSKeyPair(ctx context.Context, clusterName string) (*types.TLSKeyPair, error) { - tlsKey, signer, err := m.backendForNewKeys.generateRSA(ctx) +func (m *Manager) NewTLSKeyPair(ctx context.Context, clusterName string, purpose cryptosuites.KeyPurpose) (*types.TLSKeyPair, error) { + alg, err := cryptosuites.AlgorithmForKey(ctx, m.authPrefGetter, purpose) + if err != nil { + return nil, trace.Wrap(err) + } + tlsKey, signer, err := m.backendForNewKeys.generateKey(ctx, alg) if err != nil { return nil, trace.Wrap(err) } @@ -363,8 +383,12 @@ func (m *Manager) NewTLSKeyPair(ctx context.Context, clusterName string) (*types // New JWTKeyPair create a new JWT keypair in the keystore backend and returns // it. -func (m *Manager) NewJWTKeyPair(ctx context.Context) (*types.JWTKeyPair, error) { - jwtKey, signer, err := m.backendForNewKeys.generateRSA(ctx) +func (m *Manager) NewJWTKeyPair(ctx context.Context, purpose cryptosuites.KeyPurpose) (*types.JWTKeyPair, error) { + alg, err := cryptosuites.AlgorithmForKey(ctx, m.authPrefGetter, purpose) + if err != nil { + return nil, trace.Wrap(err) + } + jwtKey, signer, err := m.backendForNewKeys.generateKey(ctx, alg) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/keystore/pkcs11.go b/lib/auth/keystore/pkcs11.go index a488ff875ed59..7c33df9791f5e 100644 --- a/lib/auth/keystore/pkcs11.go +++ b/lib/auth/keystore/pkcs11.go @@ -21,7 +21,7 @@ package keystore import ( "context" "crypto" - "crypto/rsa" + "crypto/elliptic" "encoding/hex" "encoding/json" "fmt" @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" ) @@ -116,9 +117,9 @@ func (p *pkcs11KeyStore) findUnusedID() (keyID, error) { return keyID{}, trace.AlreadyExists("failed to find unused CKA_ID for HSM") } -// generateRSA creates a new RSAprivate key and returns its identifier and a crypto.Signer. The returned +// generateKey creates a new private key and returns its identifier and a crypto.Signer. The returned // identifier can be passed to getSigner later to get an equivalent crypto.Signer. -func (p *pkcs11KeyStore) generateRSA(ctx context.Context, _ ...rsaKeyOption) ([]byte, crypto.Signer, error) { +func (p *pkcs11KeyStore) generateKey(ctx context.Context, alg cryptosuites.Algorithm, _ ...rsaKeyOption) ([]byte, crypto.Signer, error) { // the key identifiers are not created in a thread safe // manner so all calls are serialized to prevent races. p.semaphore <- struct{}{} @@ -131,22 +132,39 @@ func (p *pkcs11KeyStore) generateRSA(ctx context.Context, _ ...rsaKeyOption) ([] return nil, nil, trace.Wrap(err) } - p.log.DebugContext(ctx, "Creating new HSM keypair.", "id", id) - - ckaID, err := id.pkcs11Key(p.isYubiHSM) + rawTeleportID, err := id.marshal() if err != nil { return nil, nil, trace.Wrap(err) } - signer, err := p.ctx.GenerateRSAKeyPairWithLabel(ckaID, []byte(p.hostUUID), constants.RSAKeySize) - if err != nil { - return nil, nil, trace.Wrap(err, "generating RSA key pair") - } - keyID, err := id.marshal() + rawPKCS11ID, err := id.pkcs11Key(p.isYubiHSM) if err != nil { return nil, nil, trace.Wrap(err) } - return keyID, signer, nil + + p.log.InfoContext(ctx, "Creating new HSM keypair.", "id", id, "algorithm", alg.String()) + + label := []byte(p.hostUUID) + switch alg { + case cryptosuites.RSA2048: + signer, err := p.generateRSA2048(rawPKCS11ID, label) + return rawTeleportID, signer, trace.Wrap(err, "generating RSA2048 key") + case cryptosuites.ECDSAP256: + signer, err := p.generateECDSAP256(rawPKCS11ID, label) + return rawTeleportID, signer, trace.Wrap(err, "generating ECDSAP256 key") + default: + return nil, nil, trace.BadParameter("unsupported key algorithm for PKCS#11 HSM: %v", alg) + } +} + +func (p *pkcs11KeyStore) generateRSA2048(ckaID, label []byte) (crypto.Signer, error) { + signer, err := p.ctx.GenerateRSAKeyPairWithLabel(ckaID, label, constants.RSAKeySize) + return signer, trace.Wrap(err) +} + +func (p *pkcs11KeyStore) generateECDSAP256(ckaID, label []byte) (crypto.Signer, error) { + signer, err := p.ctx.GenerateECDSAKeyPairWithLabel(ckaID, label, elliptic.P256()) + return signer, trace.Wrap(err) } // getSigner returns a crypto.Signer for the given key identifier, if it is found. @@ -228,7 +246,7 @@ func (p *pkcs11KeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]by // It's necessary to fetch all PublicKeys for the known activeKeys in order to // compare with the signers returned by FindKeyPairs below. We have no way // to find the CKA_ID of an unused key if it is not known. - var activePublicKeys []*rsa.PublicKey + var activePublicKeys []publicKey for _, activeKey := range activeKeys { if keyType(activeKey) != types.PrivateKeyType_PKCS11 { continue @@ -254,20 +272,20 @@ func (p *pkcs11KeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]by if err != nil { return trace.Wrap(err) } - rsaPublicKey, ok := signer.Public().(*rsa.PublicKey) + publicKey, ok := signer.Public().(publicKey) if !ok { return trace.BadParameter("unknown public key type: %T", signer.Public()) } - activePublicKeys = append(activePublicKeys, rsaPublicKey) + activePublicKeys = append(activePublicKeys, publicKey) } keyIsActive := func(signer crypto.Signer) bool { - rsaPublicKey, ok := signer.Public().(*rsa.PublicKey) + publicKey, ok := signer.Public().(publicKey) if !ok { // unknown key type... we don't know what this is, so don't delete it return true } for _, k := range activePublicKeys { - if rsaPublicKey.Equal(k) { + if publicKey.Equal(k) { return true } } @@ -292,6 +310,10 @@ func (p *pkcs11KeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]by return nil } +type publicKey interface { + Equal(other crypto.PublicKey) bool +} + type keyID struct { HostID string `json:"host_id"` KeyID string `json:"key_id"` diff --git a/lib/auth/keystore/software.go b/lib/auth/keystore/software.go index 08ee777cd98b0..d23aeeabfe9b5 100644 --- a/lib/auth/keystore/software.go +++ b/lib/auth/keystore/software.go @@ -26,7 +26,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" - "github.com/gravitational/teleport/lib/auth/native" + "github.com/gravitational/teleport/lib/cryptosuites" ) type softwareKeyStore struct { @@ -40,14 +40,7 @@ type softwareConfig struct { rsaKeyPairSource RSAKeyPairSource } -func (cfg *softwareConfig) checkAndSetDefaults() { - if cfg.rsaKeyPairSource == nil { - cfg.rsaKeyPairSource = native.GenerateKeyPair - } -} - func newSoftwareKeyStore(config *softwareConfig) *softwareKeyStore { - config.checkAndSetDefaults() return &softwareKeyStore{ rsaKeyPairSource: config.rsaKeyPairSource, } @@ -59,30 +52,29 @@ func (s *softwareKeyStore) keyTypeDescription() string { return "raw software keys" } -// generateRSA creates a new RSA private key and returns its identifier and a -// crypto.Signer. The returned identifier for softwareKeyStore is a pem-encoded -// private key, and can be passed to getSigner later to get the same -// crypto.Signer. -func (s *softwareKeyStore) generateRSA(ctx context.Context, _ ...rsaKeyOption) ([]byte, crypto.Signer, error) { - priv, _, err := s.rsaKeyPairSource() - if err != nil { - return nil, nil, err +// generateRSA creates a new private key and returns its identifier and a crypto.Signer. The returned +// identifier for softwareKeyStore is a pem-encoded private key, and can be passed to getSigner later to get +// an equivalent crypto.Signer. +func (s *softwareKeyStore) generateKey(ctx context.Context, alg cryptosuites.Algorithm, _ ...rsaKeyOption) ([]byte, crypto.Signer, error) { + if alg == cryptosuites.RSA2048 && s.rsaKeyPairSource != nil { + privateKeyPEM, _, err := s.rsaKeyPairSource() + if err != nil { + return nil, nil, err + } + signer, err := keys.ParsePrivateKey(privateKeyPEM) + return privateKeyPEM, signer, trace.Wrap(err) } - signer, err := s.getSignerWithoutPublicKey(ctx, priv) + signer, err := cryptosuites.GenerateKeyWithAlgorithm(alg) if err != nil { return nil, nil, err } - return priv, signer, trace.Wrap(err) + privateKeyPEM, err := keys.MarshalPrivateKey(signer) + return privateKeyPEM, signer, trace.Wrap(err) } // getSigner returns a crypto.Signer for the given pem-encoded private key. func (s *softwareKeyStore) getSigner(ctx context.Context, rawKey []byte, publicKey crypto.PublicKey) (crypto.Signer, error) { - return s.getSignerWithoutPublicKey(ctx, rawKey) -} - -func (s *softwareKeyStore) getSignerWithoutPublicKey(ctx context.Context, rawKey []byte) (crypto.Signer, error) { - signer, err := keys.ParsePrivateKey(rawKey) - return signer, trace.Wrap(err) + return keys.ParsePrivateKey(rawKey) } // canSignWithKey returns true if the given key is a raw key. diff --git a/lib/auth/keystore/testhelpers.go b/lib/auth/keystore/testhelpers.go index 6c8e113a9af21..79692ec998a61 100644 --- a/lib/auth/keystore/testhelpers.go +++ b/lib/auth/keystore/testhelpers.go @@ -19,6 +19,7 @@ package keystore import ( + "context" "errors" "fmt" "os" @@ -30,6 +31,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/service/servicecfg" ) @@ -212,5 +214,18 @@ func NewSoftwareKeystoreForTests(_ *testing.T, opts ...TestKeystoreOption) *Mana return &Manager{ backendForNewKeys: softwareBackend, usableSigningBackends: []backend{softwareBackend}, + authPrefGetter: &fakeAuthPreferenceGetter{types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1}, } } + +type fakeAuthPreferenceGetter struct { + suite types.SignatureAlgorithmSuite +} + +func (f *fakeAuthPreferenceGetter) GetAuthPreference(context.Context) (types.AuthPreference, error) { + return &types.AuthPreferenceV2{ + Spec: types.AuthPreferenceSpecV2{ + SignatureAlgorithmSuite: f.suite, + }, + }, nil +} diff --git a/lib/cryptosuites/suites.go b/lib/cryptosuites/suites.go index 586f45ce69050..1fcb6597ac201 100644 --- a/lib/cryptosuites/suites.go +++ b/lib/cryptosuites/suites.go @@ -26,6 +26,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "fmt" "github.com/gravitational/trace" @@ -39,7 +40,7 @@ const defaultSuite = types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_LEG type KeyPurpose int const ( - keyPurposeUnspecified KeyPurpose = iota + KeyPurposeUnspecified KeyPurpose = iota // UserCATLS represents the TLS key for the user CA. UserCATLS @@ -100,6 +101,21 @@ const ( algorithmMax ) +func (a Algorithm) String() string { + switch a { + case algorithmUnspecified: + return "algorithm unspecified" + case RSA2048: + return "RSA2048" + case ECDSAP256: + return "ECDSAP256" + case Ed25519: + return "Ed25519" + default: + return fmt.Sprintf("unknown algorithm %d", a) + } +} + // suite defines the cryptographic signature algorithm used for each unique key purpose. type suite map[KeyPurpose]Algorithm @@ -132,11 +148,12 @@ var ( DatabaseCATLS: RSA2048, DatabaseClientCATLS: RSA2048, OpenSSHCASSH: Ed25519, - JWTCAJWT: ECDSAP256, - OIDCIdPCAJWT: ECDSAP256, - SAMLIdPCATLS: ECDSAP256, - SPIFFECATLS: ECDSAP256, - SPIFFECAJWT: ECDSAP256, + // TODO(nklaassen): update JWT algorithms to ECDSAP256 once supported. + JWTCAJWT: RSA2048, + OIDCIdPCAJWT: RSA2048, + SAMLIdPCATLS: ECDSAP256, + SPIFFECATLS: ECDSAP256, + SPIFFECAJWT: RSA2048, // TODO(nklaassen): subject key purposes. } @@ -151,11 +168,12 @@ var ( DatabaseCATLS: RSA2048, DatabaseClientCATLS: RSA2048, OpenSSHCASSH: ECDSAP256, - JWTCAJWT: ECDSAP256, - OIDCIdPCAJWT: ECDSAP256, - SAMLIdPCATLS: ECDSAP256, - SPIFFECATLS: ECDSAP256, - SPIFFECAJWT: ECDSAP256, + // TODO(nklaassen): update JWT algorithms to ECDSAP256 once supported. + JWTCAJWT: RSA2048, + OIDCIdPCAJWT: RSA2048, + SAMLIdPCATLS: ECDSAP256, + SPIFFECATLS: ECDSAP256, + SPIFFECAJWT: RSA2048, // TODO(nklaassen): subject key purposes. } @@ -172,11 +190,12 @@ var ( DatabaseCATLS: RSA2048, DatabaseClientCATLS: RSA2048, OpenSSHCASSH: ECDSAP256, - JWTCAJWT: ECDSAP256, - OIDCIdPCAJWT: ECDSAP256, - SAMLIdPCATLS: ECDSAP256, - SPIFFECATLS: ECDSAP256, - SPIFFECAJWT: ECDSAP256, + // TODO(nklaassen): update JWT algorithms to ECDSAP256 once supported. + JWTCAJWT: RSA2048, + OIDCIdPCAJWT: RSA2048, + SAMLIdPCATLS: ECDSAP256, + SPIFFECATLS: ECDSAP256, + SPIFFECAJWT: RSA2048, // TODO(nklaassen): subject key purposes. } diff --git a/lib/cryptosuites/suites_test.go b/lib/cryptosuites/suites_test.go index 8cc43194fb9b2..2735d6b0c5141 100644 --- a/lib/cryptosuites/suites_test.go +++ b/lib/cryptosuites/suites_test.go @@ -39,7 +39,7 @@ func TestSuites(t *testing.T) { authPrefGetter := &fakeAuthPrefGetter{ suite: suite, } - for purpose := keyPurposeUnspecified + 1; purpose < keyPurposeMax; purpose++ { + for purpose := KeyPurposeUnspecified + 1; purpose < keyPurposeMax; purpose++ { alg, err := AlgorithmForKey(ctx, authPrefGetter, purpose) require.NoError(t, err) assert.Greater(t, alg, algorithmUnspecified) From 12aa3d2c4063e34076615140fbeffa6675064dc2 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Tue, 25 Jun 2024 11:23:10 -0700 Subject: [PATCH 2/2] test with all supported suites --- lib/auth/keystore/gcp_kms_test.go | 2 +- lib/auth/keystore/keystore_test.go | 84 ++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/lib/auth/keystore/gcp_kms_test.go b/lib/auth/keystore/gcp_kms_test.go index b61af3dd5a206..240b0a453c5ba 100644 --- a/lib/auth/keystore/gcp_kms_test.go +++ b/lib/auth/keystore/gcp_kms_test.go @@ -110,7 +110,7 @@ func (f *fakeGCPKMSServer) CreateCryptoKey(ctx context.Context, req *kmspb.Creat var pem []byte switch cryptoKey.VersionTemplate.Algorithm { - case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: + case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256, kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512: pem = testRSAPrivateKeyPEM case kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: signer, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) diff --git a/lib/auth/keystore/keystore_test.go b/lib/auth/keystore/keystore_test.go index 1276f95e3bbef..d6a9001cc6f97 100644 --- a/lib/auth/keystore/keystore_test.go +++ b/lib/auth/keystore/keystore_test.go @@ -22,6 +22,7 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -33,6 +34,7 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" @@ -42,9 +44,14 @@ import ( "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" ) +const ( + clusterName = "test-cluster" +) + var ( testRSAPrivateKeyPEM = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAqiD2rRJ5kq7hP55eOCM9DtdkWPMI8PBKgxaAiQ9J9YF3aNur @@ -266,8 +273,6 @@ func TestManager(t *testing.T) { pack := newTestPack(ctx, t) - const clusterName = "test-cluster" - for _, backendDesc := range pack.backends { t.Run(backendDesc.name, func(t *testing.T) { manager, err := NewManager(ctx, &backendDesc.config, backendDesc.opts) @@ -409,6 +414,79 @@ func TestManager(t *testing.T) { } } +// TestAlgorithmSuites asserts that the keystore generates keys with the +// expected signature algorithm for all valid signature algorithm suites. +func TestAlgorithmSuites(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + pack := newTestPack(ctx, t) + for _, suite := range []types.SignatureAlgorithmSuite{ + types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_LEGACY, + types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1, + types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1, + } { + t.Run(suite.String(), func(t *testing.T) { + testAlgorithmSuite(t, ctx, pack, suite) + }) + } +} + +func testAlgorithmSuite(t *testing.T, ctx context.Context, pack *testPack, suite types.SignatureAlgorithmSuite) { + for _, backendDesc := range pack.backends { + t.Run(backendDesc.name, func(t *testing.T) { + authPrefGetter := &fakeAuthPreferenceGetter{suite} + backendDesc.opts.AuthPreferenceGetter = authPrefGetter + manager, err := NewManager(ctx, &backendDesc.config, backendDesc.opts) + require.NoError(t, err) + + // Delete all keys to clean up the test. + t.Cleanup(func() { + assert.NoError(t, manager.DeleteUnusedKeys(context.Background(), nil /*activeKeys*/)) + }) + + sshKeyPair, err := manager.NewSSHKeyPair(ctx, cryptosuites.UserCASSH) + require.NoError(t, err) + sshPubKey, _, _, _, err := ssh.ParseAuthorizedKey(sshKeyPair.PublicKey) + require.NoError(t, err) + sshPub := sshPubKey.(ssh.CryptoPublicKey).CryptoPublicKey() + expectedAlgorithm, err := cryptosuites.AlgorithmForKey(ctx, authPrefGetter, cryptosuites.UserCASSH) + require.NoError(t, err) + assertKeyAlgorithm(t, expectedAlgorithm, sshPub) + + tlsKeyPair, err := manager.NewTLSKeyPair(ctx, clusterName, cryptosuites.DatabaseClientCATLS) + require.NoError(t, err) + tlsCert, err := tlsca.ParseCertificatePEM(tlsKeyPair.Cert) + require.NoError(t, err) + expectedAlgorithm, err = cryptosuites.AlgorithmForKey(ctx, authPrefGetter, cryptosuites.DatabaseClientCATLS) + require.NoError(t, err) + assertKeyAlgorithm(t, expectedAlgorithm, tlsCert.PublicKey) + + jwtKeyPair, err := manager.NewJWTKeyPair(ctx, cryptosuites.JWTCAJWT) + require.NoError(t, err) + jwtPubKey, err := keys.ParsePublicKey(jwtKeyPair.PublicKey) + require.NoError(t, err) + expectedAlgorithm, err = cryptosuites.AlgorithmForKey(ctx, authPrefGetter, cryptosuites.JWTCAJWT) + require.NoError(t, err) + assertKeyAlgorithm(t, expectedAlgorithm, jwtPubKey) + }) + } +} + +func assertKeyAlgorithm(t *testing.T, expectedAlgorithm cryptosuites.Algorithm, pubKey crypto.PublicKey) { + t.Helper() + switch expectedAlgorithm { + case cryptosuites.RSA2048: + assert.IsType(t, &rsa.PublicKey{}, pubKey) + case cryptosuites.ECDSAP256: + assert.IsType(t, &ecdsa.PublicKey{}, pubKey) + case cryptosuites.Ed25519: + assert.IsType(t, ed25519.PublicKey{}, pubKey) + default: + t.Fatalf("test does not support algorithm %s", expectedAlgorithm.String()) + } +} + type testPack struct { backends []*backendDesc clock clockwork.FakeClock @@ -441,7 +519,7 @@ func newTestPack(ctx context.Context, t *testing.T) *testPack { testGCPKMSClient := newTestGCPKMSClient(t, gcpKMSDialer) clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ - ClusterName: "test-cluster", + ClusterName: clusterName, }) require.NoError(t, err)