diff --git a/api/client/credentials_test.go b/api/client/credentials_test.go index cb1a4ae90767e..a3ae2473cb8f6 100644 --- a/api/client/credentials_test.go +++ b/api/client/credentials_test.go @@ -286,7 +286,8 @@ func writeProfile(t *testing.T, p *profile.Profile) { require.NoError(t, os.MkdirAll(p.KeyDir(), 0700)) require.NoError(t, os.MkdirAll(p.ProxyKeyDir(), 0700)) require.NoError(t, os.MkdirAll(p.TLSClusterCASDir(), 0700)) - require.NoError(t, os.WriteFile(p.UserKeyPath(), keyPEM, 0600)) + require.NoError(t, os.WriteFile(p.UserSSHKeyPath(), keyPEM, 0600)) + require.NoError(t, os.WriteFile(p.UserTLSKeyPath(), keyPEM, 0600)) require.NoError(t, os.WriteFile(p.TLSCertPath(), tlsCert, 0600)) require.NoError(t, os.WriteFile(p.TLSCAPathCluster(p.SiteName), tlsCACert, 0600)) require.NoError(t, os.WriteFile(p.KnownHostsPath(), sshCACert, 0600)) diff --git a/api/profile/profile.go b/api/profile/profile.go index c8b06bc0efb26..02a63e2cd171a 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -142,7 +142,7 @@ func (p *Profile) Name() string { // TLSConfig returns the profile's associated TLSConfig. func (p *Profile) TLSConfig() (*tls.Config, error) { - cert, err := keys.LoadX509KeyPair(p.TLSCertPath(), p.UserKeyPath()) + cert, err := keys.LoadX509KeyPair(p.TLSCertPath(), p.UserTLSKeyPath()) if err != nil { return nil, trace.Wrap(err) } @@ -251,7 +251,7 @@ func (p *Profile) SSHClientConfig() (*ssh.ClientConfig, error) { return nil, trace.Wrap(err) } - priv, err := keys.LoadPrivateKey(p.UserKeyPath()) + priv, err := keys.LoadPrivateKey(p.UserSSHKeyPath()) if err != nil { return nil, trace.Wrap(err) } @@ -449,9 +449,14 @@ func (p *Profile) ProxyKeyDir() string { return keypaths.ProxyKeyDir(p.Dir, p.Name()) } -// UserKeyPath returns the path to the profile's private key. -func (p *Profile) UserKeyPath() string { - return keypaths.UserKeyPath(p.Dir, p.Name(), p.Username) +// UserSSHKeyPath returns the path to the profile's SSH private key. +func (p *Profile) UserSSHKeyPath() string { + return keypaths.UserSSHKeyPath(p.Dir, p.Name(), p.Username) +} + +// UserTLSKeyPath returns the path to the profile's TLS private key. +func (p *Profile) UserTLSKeyPath() string { + return keypaths.UserTLSKeyPath(p.Dir, p.Name(), p.Username) } // TLSCertPath returns the path to the profile's TLS certificate. diff --git a/api/types/authentication.go b/api/types/authentication.go index e66c44bf73200..d00c5576a7c33 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -117,8 +117,11 @@ type AuthPreference interface { // SetAllowHeadless sets the value of the allow headless setting. SetAllowHeadless(b bool) + // SetRequireMFAType sets the type of MFA requirement enforced for this cluster. + SetRequireMFAType(RequireMFAType) // GetRequireMFAType returns the type of MFA requirement enforced for this cluster. GetRequireMFAType() RequireMFAType + // GetPrivateKeyPolicy returns the configured private key policy for the cluster. GetPrivateKeyPolicy() keys.PrivateKeyPolicy @@ -412,6 +415,11 @@ func (c *AuthPreferenceV2) SetAllowHeadless(b bool) { c.Spec.AllowHeadless = NewBoolOption(b) } +// SetRequireMFAType sets the type of MFA requirement enforced for this cluster. +func (c *AuthPreferenceV2) SetRequireMFAType(t RequireMFAType) { + c.Spec.RequireMFAType = t +} + // GetRequireMFAType returns the type of MFA requirement enforced for this cluster. func (c *AuthPreferenceV2) GetRequireMFAType() RequireMFAType { return c.Spec.RequireMFAType diff --git a/api/types/session.go b/api/types/session.go index 718b1d2677b55..500a1e1cb23d0 100644 --- a/api/types/session.go +++ b/api/types/session.go @@ -317,7 +317,7 @@ func (ws *WebSessionV2) SetSSHPriv(priv []byte) { // GetTLSPriv returns private TLS key. func (ws *WebSessionV2) GetTLSPriv() []byte { - // TODO(nklaassen): DELETE IN 18.0.0 + // TODO(nklaassen): DELETE IN 18.0.0 when all auth servers are writing web session TLS key. if ws.Spec.TLSPriv == nil { // An older auth instance may have written this web session before the // SSH and TLS keys were split. diff --git a/api/utils/keypaths/keypaths.go b/api/utils/keypaths/keypaths.go index 3226a86ad512d..65d855ce2f3ec 100644 --- a/api/utils/keypaths/keypaths.go +++ b/api/utils/keypaths/keypaths.go @@ -82,11 +82,12 @@ const ( // └── keys --> session keys directory // ├── one.example.com --> Proxy hostname // │ ├── certs.pem --> TLS CA certs for the Teleport CA -// │ ├── foo --> Private Key for user "foo" -// │ ├── foo.pub --> Public Key +// │ ├── foo.key --> TLS Private Key for user "foo" +// │ ├── foo.crt --> TLS client certificate for Auth Server +// │ ├── foo --> SSH Private Key for user "foo" +// │ ├── foo.pub --> SSH Public Key // │ ├── foo.ppk --> PuTTY PPK-formatted keypair for user "foo" // │ ├── kube_credentials.lock --> Kube credential lockfile, used to prevent excessive relogin attempts -// │ ├── foo-x509.pem --> TLS client certificate for Auth Server // │ ├── foo-ssh --> SSH certs for user "foo" // │ │ ├── root-cert.pub --> SSH cert for Teleport cluster "root" // │ │ └── leaf-cert.pub --> SSH cert for Teleport cluster "leaf" @@ -163,20 +164,28 @@ func ProxyKeyDir(baseDir, proxy string) string { return filepath.Join(KeyDir(baseDir), proxy) } -// UserKeyPath returns the path to the users's private key +// UserSSHKeyPath returns the path to the users's SSH private key // for the given proxy. // // /keys//. -func UserKeyPath(baseDir, proxy, username string) string { +func UserSSHKeyPath(baseDir, proxy, username string) string { return filepath.Join(ProxyKeyDir(baseDir, proxy), username) } +// UserTLSKeyPath returns the path to the users's TLS private key +// for the given proxy. +// +// /keys//.key +func UserTLSKeyPath(baseDir, proxy, username string) string { + return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtTLSKey) +} + // TLSCertPath returns the path to the users's TLS certificate // for the given proxy. // -// /keys//-x509.pem +// /keys//.crt func TLSCertPath(baseDir, proxy, username string) string { - return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtTLSCertLegacy) + return filepath.Join(ProxyKeyDir(baseDir, proxy), username+FileExtTLSCert) } // PublicKeyPath returns the path to the users's public key diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index acf71c41609e2..bd325a16d1d02 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -38,6 +38,7 @@ const ( PKCS1PrivateKeyType = "RSA PRIVATE KEY" PKCS8PrivateKeyType = "PRIVATE KEY" ECPrivateKeyType = "EC PRIVATE KEY" + OpenSSHPrivateKeyType = "OPENSSH PRIVATE KEY" pivYubiKeyPrivateKeyType = "PIV YUBIKEY PRIVATE KEY" ) @@ -57,7 +58,8 @@ type PrivateKey struct { keyPEM []byte } -// NewPrivateKey returns a new PrivateKey for the given crypto.Signer. +// NewPrivateKey returns a new PrivateKey for the given crypto.Signer with a +// pre-marshaled private key PEM, which may be a special PIV key PEM. func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) { sshPub, err := ssh.NewPublicKey(signer.Public()) if err != nil { @@ -71,16 +73,64 @@ func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) { }, nil } -// SSHPublicKey returns the ssh.PublicKey representiation of the public key. +// NewSoftwarePrivateKey returns a new PrivateKey for a crypto.Signer. +// [signer] must be an *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey. +func NewSoftwarePrivateKey(signer crypto.Signer) (*PrivateKey, error) { + sshPub, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + return nil, trace.Wrap(err) + } + keyPEM, err := MarshalPrivateKey(signer) + if err != nil { + return nil, trace.Wrap(err) + } + return &PrivateKey{ + Signer: signer, + sshPub: sshPub, + keyPEM: keyPEM, + }, nil +} + +// SSHPublicKey returns the ssh.PublicKey representation of the public key. func (k *PrivateKey) SSHPublicKey() ssh.PublicKey { return k.sshPub } -// SSHPublicKey returns the ssh.PublicKey representiation of the public key. +// MarshalSSHPrivateKey returns the private key marshaled to: +// - PEM-encoded OpenSSH format for Ed25519 or ECDSA keys +// - PEM-encoded PKCS#1 for RSA keys +// - a custom PEM-encoded format for PIV keys +func (k *PrivateKey) MarshalSSHPrivateKey() ([]byte, error) { + switch k.Signer.(type) { + case ed25519.PrivateKey, *ecdsa.PrivateKey: + // OpenSSH largely does not support PKCS8 private keys, write these in + // OpenSSH format. + const comment = "" + pemBlock, err := ssh.MarshalPrivateKey(k.Signer, comment) + if err != nil { + return nil, trace.Wrap(err) + } + return pem.EncodeToMemory(pemBlock), nil + } + // Otherwise we are dealing with either a hardware key which has a custom + // format, or an RSA key which would already be in PKCS#1, which OpenSSH can + // handle. + return k.keyPEM, nil +} + +// MarshalSSHPublicKey returns the public key marshaled to SSH authorized_keys format. func (k *PrivateKey) MarshalSSHPublicKey() []byte { return ssh.MarshalAuthorizedKey(k.sshPub) } +// MarshalTLSPublicKey returns a PEM encoding of the public key. Encodes RSA keys +// in PKCS1 format for backward compatibility. All other key types are encoded +// in PKIX, ASN.1 DER form. Only supports *rsa.PublicKey, *ecdsa.PublicKey, and +// ed25519.PublicKey. +func (k *PrivateKey) MarshalTLSPublicKey() ([]byte, error) { + return MarshalPublicKey(k.Signer.Public()) +} + // PrivateKeyPEM returns PEM encoded private key data. This may be data necessary // to retrieve the key, such as a YubiKey serial number and slot, or it can be a // PKCS marshaled private key. @@ -192,6 +242,22 @@ func ParsePrivateKey(keyPEM []byte) (*PrivateKey, error) { return nil, trace.BadParameter("x509.ParsePKCS8PrivateKey returned an invalid private key of type %T", priv) } return NewPrivateKey(cryptoSigner, keyPEM) + case OpenSSHPrivateKeyType: + priv, err := ssh.ParseRawPrivateKey(keyPEM) + if err != nil { + return nil, trace.Wrap(err) + } + cryptoSigner, ok := priv.(crypto.Signer) + if !ok { + return nil, trace.BadParameter("ssh.ParseRawPrivateKey returned an invalid private key of type %T", priv) + } + // For some reason ssh.ParseRawPrivateKey returns a *ed25519.PrivateKey + // instead of the plain ed25519.PrivateKey which is used everywhere + // else. This breaks comparisons and type switches, so explicitly convert it. + if pEdwards, ok := cryptoSigner.(*ed25519.PrivateKey); ok { + cryptoSigner = *pEdwards + } + return NewPrivateKey(cryptoSigner, keyPEM) case pivYubiKeyPrivateKeyType: priv, err := parseYubiKeyPrivateKeyData(block.Bytes) if err != nil { @@ -235,12 +301,12 @@ func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) { return nil, trace.ConvertSystemError(err) } - marshalledSSHPub, err := os.ReadFile(sshPubFile) + marshaledSSHPub, err := os.ReadFile(sshPubFile) if err != nil { return nil, trace.ConvertSystemError(err) } - priv, err := ParseKeyPair(privPEM, marshalledSSHPub) + priv, err := ParseKeyPair(privPEM, marshaledSSHPub) if err != nil { return nil, trace.Wrap(err) } @@ -248,14 +314,14 @@ func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) { } // ParseKeyPair returns the PrivateKey for the given private and public key PEM blocks. -func ParseKeyPair(privPEM, marshalledSSHPub []byte) (*PrivateKey, error) { +func ParseKeyPair(privPEM, marshaledSSHPub []byte) (*PrivateKey, error) { priv, err := ParsePrivateKey(privPEM) if err != nil { return nil, trace.Wrap(err) } // Verify that the private key's public key matches the expected public key. - if !bytes.Equal(ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), marshalledSSHPub) { + if !bytes.Equal(ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), marshaledSSHPub) { return nil, trace.CompareFailed("the given private and public keys do not form a valid keypair") } diff --git a/api/utils/keys/publickey.go b/api/utils/keys/publickey.go index a5db3f13b9382..26ef9192814d9 100644 --- a/api/utils/keys/publickey.go +++ b/api/utils/keys/publickey.go @@ -32,8 +32,10 @@ const ( PKIXPublicKeyType = "PUBLIC KEY" ) -// MarshalPublicKey returns a PEM encoding of the given public key. Encodes RSA keys in PKCS1 format for -// backward compatibility. Only supports *rsa.PublicKey, *ecdsa.PublicKey, and ed25519.PublicKey. +// MarshalPublicKey returns a PEM encoding of the given public key. Encodes RSA +// keys in PKCS1 format for backward compatibility. All other key types are +// encoded in PKIX, ASN.1 DER form. Only supports *rsa.PublicKey, +// *ecdsa.PublicKey, and ed25519.PublicKey. func MarshalPublicKey(pub crypto.PublicKey) ([]byte, error) { switch pubKey := pub.(type) { case *rsa.PublicKey: diff --git a/integration/helpers/helpers.go b/integration/helpers/helpers.go index 5bf8463a97963..7d1b7e58b819f 100644 --- a/integration/helpers/helpers.go +++ b/integration/helpers/helpers.go @@ -20,6 +20,7 @@ package helpers import ( "context" + "crypto" "fmt" "net" "os" @@ -44,6 +45,7 @@ import ( apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth" @@ -52,6 +54,7 @@ import ( "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/identityfile" "github.com/gravitational/teleport/lib/cloud/imds" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/multiplexer" @@ -189,12 +192,26 @@ func CloseAgent(teleAgent *teleagent.AgentServer, socketDirPath string) error { } func MustCreateUserKeyRing(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) *client.KeyRing { - keyRing, err := client.GenerateRSAKeyRing() + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(context.Background(), func(_ context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }) + require.NoError(t, err) + return mustCreateUserKeyRingWithKeys(t, tc, username, ttl, sshKey, tlsKey) +} + +func mustCreateUserKeyRingWithKeys(t *testing.T, tc *TeleInstance, username string, ttl time.Duration, sshKey, tlsKey crypto.Signer) *client.KeyRing { + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) require.NoError(t, err) + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + require.NoError(t, err) + keyRing := client.NewKeyRing(sshPriv, tlsPriv) keyRing.ClusterName = tc.Secrets.SiteName + tlsPub, err := keys.MarshalPublicKey(tlsKey.Public()) + require.NoError(t, err) sshCert, tlsCert, err := tc.Process.GetAuthServer().GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: keyRing.PrivateKey.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, Username: username, TTL: ttl, Compatibility: constants.CertificateFormatStandard, @@ -212,10 +229,16 @@ func MustCreateUserKeyRing(t *testing.T, tc *TeleInstance, username string, ttl } func MustCreateUserIdentityFile(t *testing.T, tc *TeleInstance, username string, ttl time.Duration) string { - keyRing := MustCreateUserKeyRing(t, tc, username, ttl) + key, err := cryptosuites.GenerateKey(context.Background(), func(_ context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }, cryptosuites.UserTLS) + require.NoError(t, err) + // Identity files must use the same key for SSH and TLS. + sshKey, tlsKey := key, key + keyRing := mustCreateUserKeyRingWithKeys(t, tc, username, ttl, sshKey, tlsKey) idPath := filepath.Join(t.TempDir(), "user_identity") - _, err := identityfile.Write(context.Background(), identityfile.WriteConfig{ + _, err = identityfile.Write(context.Background(), identityfile.WriteConfig{ OutputPath: idPath, KeyRing: keyRing, Format: identityfile.FormatFile, diff --git a/integration/helpers/usercreds.go b/integration/helpers/usercreds.go index 7ec18d61467b3..6f6c2192b7d0b 100644 --- a/integration/helpers/usercreds.go +++ b/integration/helpers/usercreds.go @@ -23,13 +23,13 @@ import ( "time" "github.com/gravitational/trace" - "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/services" ) @@ -114,14 +114,29 @@ func GenerateUserCreds(req UserCredsRequest) (*UserCreds, error) { ttl = time.Hour } - priv, err := testauthority.New().GeneratePrivateKey() + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(context.Background(), func(_ context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }) + if err != nil { + return nil, trace.Wrap(err) + } + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + if err != nil { + return nil, trace.Wrap(err) + } + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + if err != nil { + return nil, trace.Wrap(err) + } + sshPub := sshPriv.MarshalSSHPublicKey() + tlsPub, err := tlsPriv.MarshalTLSPublicKey() if err != nil { return nil, trace.Wrap(err) } a := req.Process.GetAuthServer() - sshPub := ssh.MarshalAuthorizedKey(priv.SSHPublicKey()) sshCert, x509Cert, err := a.GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: sshPub, + SSHPubKey: sshPub, + TLSPubKey: tlsPub, Username: req.Username, TTL: ttl, Compatibility: constants.CertificateFormatStandard, @@ -146,9 +161,10 @@ func GenerateUserCreds(req UserCredsRequest) (*UserCreds, error) { return &UserCreds{ HostCA: ca, KeyRing: client.KeyRing{ - PrivateKey: priv, - Cert: sshCert, - TLSCert: x509Cert, + SSHPrivateKey: sshPriv, + TLSPrivateKey: tlsPriv, + Cert: sshCert, + TLSCert: x509Cert, }, }, nil } diff --git a/integrations/lib/testing/integration/authhelper.go b/integrations/lib/testing/integration/authhelper.go index e784084838617..19558232980a3 100644 --- a/integrations/lib/testing/integration/authhelper.go +++ b/integrations/lib/testing/integration/authhelper.go @@ -22,7 +22,6 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/pem" "fmt" "testing" "time" @@ -36,7 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" libauth "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/auth/native" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/plugin" ) @@ -132,12 +131,15 @@ func (a *MinimalAuthHelper) getUserCerts(t *testing.T, user types.User) userCert clusterName, err := auth.GetClusterName() require.NoError(t, err) // Get user certs - userKey, err := native.GenerateRSAPrivateKey() + userKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) require.NoError(t, err) - userPubKey, err := ssh.NewPublicKey(&userKey.PublicKey) + sshPub, err := ssh.NewPublicKey(userKey.Public()) + require.NoError(t, err) + tlsPub, err := keys.MarshalPublicKey(userKey.Public()) require.NoError(t, err) testCertsReq := libauth.GenerateUserTestCertsRequest{ - Key: ssh.MarshalAuthorizedKey(userPubKey), + SSHPubKey: ssh.MarshalAuthorizedKey(sshPub), + TLSPubKey: tlsPub, Username: user.GetName(), TTL: time.Hour, Compatibility: constants.CertificateFormatStandard, @@ -147,14 +149,10 @@ func (a *MinimalAuthHelper) getUserCerts(t *testing.T, user types.User) userCert require.NoError(t, err) // Build credentials from the certs - pemKey := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(userKey), - }, - ) + keyPEM, err := keys.MarshalPrivateKey(userKey) + require.NoError(t, err) - return userCerts{pemKey, sshCert, tlsCert} + return userCerts{keyPEM, sshCert, tlsCert} } // CredentialsForUser implements the AuthHelper interface. diff --git a/integrations/terraform/testlib/main_test.go b/integrations/terraform/testlib/main_test.go index 502e49a0c5090..9a0bd8f67fa5d 100644 --- a/integrations/terraform/testlib/main_test.go +++ b/integrations/terraform/testlib/main_test.go @@ -177,7 +177,8 @@ func (s *TerraformBaseSuite) getTLSCreds(ctx context.Context, user types.User, o require.NoError(s.T(), err) privateKey, err := keys.NewPrivateKey(signer, privateKeyPEM) require.NoError(s.T(), err) - keyRing := libclient.NewKeyRing(privateKey) + // Identity files only support a single private key for SSH and TLS. + keyRing := libclient.NewKeyRing(privateKey, privateKey) certs, err := s.client.GenerateUserCerts(ctx, proto.UserCertsRequest{ TLSPublicKey: publicKeyPEM, diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 12acae7278a6d..edf6d90287347 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -2200,16 +2200,18 @@ func (a *Server) GenerateOpenSSHCert(ctx context.Context, req *proto.OpenSSHCert // GenerateUserTestCertsRequest is a request to generate test certificates. type GenerateUserTestCertsRequest struct { - Key []byte - Username string - TTL time.Duration - Compatibility string - RouteToCluster string - PinnedIP string - MFAVerified string - AttestationStatement *keys.AttestationStatement - AppName string - AppSessionID string + SSHPubKey []byte + TLSPubKey []byte + Username string + TTL time.Duration + Compatibility string + RouteToCluster string + PinnedIP string + MFAVerified string + SSHAttestationStatement *keys.AttestationStatement + TLSAttestationStatement *keys.AttestationStatement + AppName string + AppSessionID string } // GenerateUserTestCerts is used to generate user certificate, used internally for tests @@ -2229,31 +2231,20 @@ func (a *Server) GenerateUserTestCerts(req GenerateUserTestCertsRequest) ([]byte return nil, nil, trace.Wrap(err) } - // TODO(nklaassen): separate SSH and TLS keys. For now they are the same. - sshPublicKey := req.Key - cryptoPubKey, err := sshutils.CryptoPublicKey(req.Key) - if err != nil { - return nil, nil, trace.Wrap(err) - } - tlsPublicKey, err := keys.MarshalPublicKey(cryptoPubKey) - if err != nil { - return nil, nil, trace.Wrap(err) - } - certs, err := a.generateUserCert(ctx, certRequest{ user: userState, ttl: req.TTL, compatibility: req.Compatibility, - sshPublicKey: sshPublicKey, - tlsPublicKey: tlsPublicKey, + sshPublicKey: req.SSHPubKey, + tlsPublicKey: req.TLSPubKey, routeToCluster: req.RouteToCluster, checker: checker, traits: userState.GetTraits(), loginIP: req.PinnedIP, pinIP: req.PinnedIP != "", mfaVerified: req.MFAVerified, - sshPublicKeyAttestationStatement: req.AttestationStatement, - tlsPublicKeyAttestationStatement: req.AttestationStatement, + sshPublicKeyAttestationStatement: req.SSHAttestationStatement, + tlsPublicKeyAttestationStatement: req.TLSAttestationStatement, appName: req.AppName, appSessionID: req.AppSessionID, }) diff --git a/lib/auth/native/native.go b/lib/auth/native/native.go index f3b84d45de69a..24676b2c23a2d 100644 --- a/lib/auth/native/native.go +++ b/lib/auth/native/native.go @@ -139,14 +139,15 @@ func precomputeKeys() { } func precomputeTestKeys() { - testKeys, err := generateTestKeys() - if err != nil { - // Use only in tests. Safe to panic. - panic(err) + generatedTestKeys := generateTestKeys() + keysToReuse := make([]*rsa.PrivateKey, 0, testKeysNumber) + for range testKeysNumber { + k := <-generatedTestKeys + precomputedKeys <- k + keysToReuse = append(keysToReuse, k) } - for { - for _, k := range testKeys { + for _, k := range keysToReuse { precomputedKeys <- k } } @@ -155,36 +156,21 @@ func precomputeTestKeys() { // testKeysNumber is the number of RSA keys generated in tests. const testKeysNumber = 25 -func generateTestKeys() ([]*rsa.PrivateKey, error) { - privateKeys := make([]*rsa.PrivateKey, 0, testKeysNumber) - keysChan := make(chan *rsa.PrivateKey) - errC := make(chan error) - - go func() { - for i := 0; i < testKeysNumber; i++ { - // Generate each key in a separate goroutine to take advantage of - // multiple cores if possible. - go func() { - private, err := generateRSAPrivateKey() - if err != nil { - errC <- trace.Wrap(err) - return - } - keysChan <- private - }() - } - }() - - for i := 0; i < testKeysNumber; i++ { - select { - case err := <-errC: - return nil, trace.Wrap(err) - case privKey := <-keysChan: - privateKeys = append(privateKeys, privKey) - } +func generateTestKeys() <-chan *rsa.PrivateKey { + generatedTestKeys := make(chan *rsa.PrivateKey, testKeysNumber) + for range testKeysNumber { + // Generate each key in a separate goroutine to take advantage of + // multiple cores if possible. + go func() { + private, err := generateRSAPrivateKey() + if err != nil { + // Use only in tests. Safe to panic. + panic(err) + } + generatedTestKeys <- private + }() } - - return privateKeys, nil + return generatedTestKeys } // PrecomputeKeys sets this package into a mode where a small backlog of keys are diff --git a/lib/client/alpn.go b/lib/client/alpn.go index c6654c1986102..ec4e920923da7 100644 --- a/lib/client/alpn.go +++ b/lib/client/alpn.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/srv/alpnproxy" alpn "github.com/gravitational/teleport/lib/srv/alpnproxy/common" ) @@ -48,6 +49,9 @@ type ALPNAuthClient interface { // text format, signs it using User Certificate Authority signing key and // returns the resulting certificates. GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) + + // GetAuthPreference returns the current cluster auth preference. + GetAuthPreference(context.Context) (types.AuthPreference, error) } // ALPNAuthTunnelConfig contains the required fields used to create an authed ALPN Proxy @@ -114,13 +118,13 @@ func RunALPNAuthTunnel(ctx context.Context, cfg ALPNAuthTunnelConfig) error { } func getUserCerts(ctx context.Context, client ALPNAuthClient, mfaResponse *proto.MFAAuthenticateResponse, expires time.Time, routeToDatabase proto.RouteToDatabase, connectionDiagnosticID string) (tls.Certificate, error) { - // TODO(nklaassen): support configurable signature algorithms. - keyRing, err := GenerateRSAKeyRing() + key, err := cryptosuites.GenerateKey(ctx, + cryptosuites.GetCurrentSuiteFromAuthPreference(client), + cryptosuites.UserTLS) if err != nil { return tls.Certificate{}, trace.Wrap(err) } - - publicKeyPEM, err := keys.MarshalPublicKey(keyRing.PrivateKey.Public()) + publicKeyPEM, err := keys.MarshalPublicKey(key.Public()) if err != nil { return tls.Certificate{}, trace.Wrap(err) } @@ -142,9 +146,9 @@ func getUserCerts(ctx context.Context, client ALPNAuthClient, mfaResponse *proto return tls.Certificate{}, trace.Wrap(err) } - tlsCert, err := keys.X509KeyPair(certs.TLS, keyRing.PrivateKey.PrivateKeyPEM()) + tlsCert, err := keys.TLSCertificateForSigner(key, certs.TLS) if err != nil { - return tls.Certificate{}, trace.BadParameter("failed to parse private key: %v", err) + return tls.Certificate{}, trace.Wrap(err) } return tlsCert, nil diff --git a/lib/client/api.go b/lib/client/api.go index 287779d76d446..74fd055476196 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -20,6 +20,7 @@ package client import ( "context" + "crypto" "crypto/tls" "crypto/x509" "encoding/pem" @@ -70,12 +71,12 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/auth/authclient" - "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/auth/touchid" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" "github.com/gravitational/teleport/lib/authz" libmfa "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/client/terminal" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/devicetrust" dtauthntypes "github.com/gravitational/teleport/lib/devicetrust/authn/types" @@ -496,6 +497,10 @@ type Config struct { // SSHDialTimeout is the timeout value that should be used for SSH connections. SSHDialTimeout time.Duration + + // GenerateUnifiedKey indicates that the client should generate a single key + // for SSH and TLS instead of split keys. + GenerateUnifiedKey bool } // CachePolicy defines cache policy for local clients @@ -3348,8 +3353,8 @@ func (tc *TeleportClient) LoginWeb(ctx context.Context) (*WebClient, types.WebSe var clt *WebClient var session types.WebSession - _, err = tc.loginWithHardwareKeyRetry(ctx, func(ctx context.Context, priv *keys.PrivateKey) error { - clt, session, err = webLoginFunc(ctx, priv) + _, err = tc.loginWithHardwareKeyRetry(ctx, func(ctx context.Context, keyRing *KeyRing) error { + clt, session, err = webLoginFunc(ctx, keyRing) return trace.Wrap(err) }) @@ -3392,8 +3397,7 @@ func (tc *TeleportClient) AttemptDeviceLogin(ctx context.Context, keyRing *KeyRi // The TLS certificate is already part of the connection. SshAuthorizedKey: keyRing.Cert, }, - // TODO(nklaassen): split SSH private key from TLS key. - SSHSigner: keyRing.PrivateKey, + SSHSigner: keyRing.SSHPrivateKey, }) switch { case errors.Is(err, devicetrust.ErrDeviceKeyNotFound): @@ -3512,8 +3516,8 @@ func (tc *TeleportClient) getSSHLoginFunc(pr *webclient.PingResponse) (SSHLoginF return nil, trace.BadParameter("headless disallowed by cluster settings") } if tc.AllowHeadless { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { - return tc.headlessLogin(ctx, priv) + return func(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { + return tc.headlessLogin(ctx, keyRing) }, nil } return nil, trace.BadParameter("" + @@ -3528,26 +3532,26 @@ func (tc *TeleportClient) getSSHLoginFunc(pr *webclient.PingResponse) (SSHLoginF return tc.pwdlessLogin, nil } - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { - return tc.localLogin(ctx, priv, pr.Auth.SecondFactor) + return func(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { + return tc.localLogin(ctx, keyRing, pr.Auth.SecondFactor) }, nil default: return nil, trace.BadParameter("unsupported authentication connector type: %q", pr.Auth.Local.Name) } case constants.OIDC: oidc := pr.Auth.OIDC - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { - return tc.ssoLogin(ctx, priv, oidc.Name, oidc.Display, constants.OIDC) + return func(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { + return tc.ssoLogin(ctx, keyRing, oidc.Name, oidc.Display, constants.OIDC) }, nil case constants.SAML: saml := pr.Auth.SAML - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { - return tc.ssoLogin(ctx, priv, saml.Name, saml.Display, constants.SAML) + return func(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { + return tc.ssoLogin(ctx, keyRing, saml.Name, saml.Display, constants.SAML) }, nil case constants.Github: github := pr.Auth.Github - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { - return tc.ssoLogin(ctx, priv, github.Name, github.Display, constants.Github) + return func(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { + return tc.ssoLogin(ctx, keyRing, github.Name, github.Display, constants.Github) }, nil default: return nil, trace.BadParameter("unsupported authentication type: %q", pr.Auth.Type) @@ -3576,8 +3580,8 @@ func (tc *TeleportClient) getWebLoginFunc(pr *webclient.PingResponse) (WebLoginF return tc.pwdlessLoginWeb, nil } - return func(ctx context.Context, priv *keys.PrivateKey) (*WebClient, types.WebSession, error) { - return tc.localLoginWeb(ctx, priv, pr.Auth.SecondFactor) + return func(ctx context.Context, keyRing *KeyRing) (*WebClient, types.WebSession, error) { + return tc.localLoginWeb(ctx, keyRing, pr.Auth.SecondFactor) }, nil default: return nil, trace.BadParameter("unsupported authentication connector type: %q", pr.Auth.Local.Name) @@ -3594,7 +3598,7 @@ func (tc *TeleportClient) getWebLoginFunc(pr *webclient.PingResponse) (WebLoginF } // pwdlessLoginWeb performs a passwordless ceremony and then makes a request to authenticate via the web api. -func (tc *TeleportClient) pwdlessLoginWeb(ctx context.Context, priv *keys.PrivateKey) (*WebClient, types.WebSession, error) { +func (tc *TeleportClient) pwdlessLoginWeb(ctx context.Context, keyRing *KeyRing) (*WebClient, types.WebSession, error) { // Only pass on the user if explicitly set, otherwise let the credential // picker kick in. user := "" @@ -3602,7 +3606,7 @@ func (tc *TeleportClient) pwdlessLoginWeb(ctx context.Context, priv *keys.Privat user = tc.Username } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, nil, trace.Wrap(err) } @@ -3618,16 +3622,16 @@ func (tc *TeleportClient) pwdlessLoginWeb(ctx context.Context, priv *keys.Privat } // localLoginWeb performs the mfa ceremony and then makes a request to authenticate via the web api. -func (tc *TeleportClient) localLoginWeb(ctx context.Context, priv *keys.PrivateKey, secondFactor constants.SecondFactorType) (*WebClient, types.WebSession, error) { +func (tc *TeleportClient) localLoginWeb(ctx context.Context, keyRing *KeyRing, secondFactor constants.SecondFactorType) (*WebClient, types.WebSession, error) { // TODO(awly): mfa: ideally, clients should always go through mfaLocalLogin // (with a nop MFA challenge if no 2nd factor is required). That way we can // deprecate the direct login endpoint. switch secondFactor { case constants.SecondFactorOff, constants.SecondFactorOTP: - clt, session, err := tc.directLoginWeb(ctx, secondFactor, priv) + clt, session, err := tc.directLoginWeb(ctx, secondFactor, keyRing) return clt, session, trace.Wrap(err) case constants.SecondFactorU2F, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional: - clt, session, err := tc.mfaLocalLoginWeb(ctx, priv) + clt, session, err := tc.mfaLocalLoginWeb(ctx, keyRing) return clt, session, trace.Wrap(err) default: return nil, nil, trace.BadParameter("unsupported second factor type: %q", secondFactor) @@ -3635,7 +3639,7 @@ func (tc *TeleportClient) localLoginWeb(ctx context.Context, priv *keys.PrivateK } // directLoginWeb asks for a password + OTP token then makes a request to authenticate via the web api. -func (tc *TeleportClient) directLoginWeb(ctx context.Context, secondFactorType constants.SecondFactorType, priv *keys.PrivateKey) (*WebClient, types.WebSession, error) { +func (tc *TeleportClient) directLoginWeb(ctx context.Context, secondFactorType constants.SecondFactorType, keyRing *KeyRing) (*WebClient, types.WebSession, error) { password, err := tc.AskPassword(ctx) if err != nil { return nil, nil, trace.Wrap(err) @@ -3650,7 +3654,7 @@ func (tc *TeleportClient) directLoginWeb(ctx context.Context, secondFactorType c } } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, nil, trace.Wrap(err) } @@ -3666,13 +3670,13 @@ func (tc *TeleportClient) directLoginWeb(ctx context.Context, secondFactorType c } // mfaLocalLoginWeb asks for a password and performs the challenge-response authentication with the web api -func (tc *TeleportClient) mfaLocalLoginWeb(ctx context.Context, priv *keys.PrivateKey) (*WebClient, types.WebSession, error) { +func (tc *TeleportClient) mfaLocalLoginWeb(ctx context.Context, keyRing *KeyRing) (*WebClient, types.WebSession, error) { password, err := tc.AskPassword(ctx) if err != nil { return nil, nil, trace.Wrap(err) } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, nil, trace.Wrap(err) } @@ -3715,7 +3719,7 @@ func (tc *TeleportClient) canDefaultToPasswordless(pr *webclient.PingResponse) b } // SSHLoginFunc is a function which carries out authn with an auth server and returns an auth response. -type SSHLoginFunc func(context.Context, *keys.PrivateKey) (*authclient.SSHLoginResponse, error) +type SSHLoginFunc func(context.Context, *KeyRing) (*authclient.SSHLoginResponse, error) // SSHLogin uses the given login function to login the client. This function handles // private key logic and parsing the resulting auth response. @@ -3728,9 +3732,9 @@ func (tc *TeleportClient) SSHLogin(ctx context.Context, sshLoginFunc SSHLoginFun defer span.End() var response *authclient.SSHLoginResponse - priv, err := tc.loginWithHardwareKeyRetry(ctx, func(ctx context.Context, priv *keys.PrivateKey) error { + keyRing, err := tc.loginWithHardwareKeyRetry(ctx, func(ctx context.Context, keyRing *KeyRing) error { var err error - response, err = sshLoginFunc(ctx, priv) + response, err = sshLoginFunc(ctx, keyRing) return trace.Wrap(err) }) if err != nil { @@ -3742,32 +3746,32 @@ func (tc *TeleportClient) SSHLogin(ctx context.Context, sshLoginFunc SSHLoginFun return nil, trace.BadParameter("bad response from the server: expected at least one certificate, got 0") } - // extract the new certificate out of the response - keyRing := NewKeyRing(priv) + // Set the new certs from the response. keyRing.Cert = response.Cert keyRing.TLSCert = response.TLSCert keyRing.TrustedCerts = response.HostSigners - keyRing.Username = response.Username - keyRing.ProxyHost = tc.WebProxyHost() - if tc.KubernetesCluster != "" { keyRing.KubeTLSCredentials[tc.KubernetesCluster] = TLSCredential{ Cert: response.TLSCert, - PrivateKey: priv, + PrivateKey: keyRing.TLSPrivateKey, } } if tc.DatabaseService != "" { keyRing.DBTLSCredentials[tc.DatabaseService] = TLSCredential{ Cert: response.TLSCert, - PrivateKey: priv, + PrivateKey: keyRing.TLSPrivateKey, } } - // Store the requested cluster name in the keyring. - keyRing.ClusterName = tc.SiteName - if keyRing.ClusterName == "" { + // Set the KeyRingIndex based on the response. + keyRing.KeyRingIndex = KeyRingIndex{ + ProxyHost: tc.WebProxyHost(), + ClusterName: tc.SiteName, + Username: response.Username, + } + if keyRing.KeyRingIndex.ClusterName == "" { rootClusterName := keyRing.TrustedCerts[0].ClusterName - keyRing.ClusterName = rootClusterName + keyRing.KeyRingIndex.ClusterName = rootClusterName tc.SiteName = rootClusterName } @@ -3775,15 +3779,15 @@ func (tc *TeleportClient) SSHLogin(ctx context.Context, sshLoginFunc SSHLoginFun } // WebLoginFunc is a function which carries out authn with the web server and returns a web session and cookies. -type WebLoginFunc func(context.Context, *keys.PrivateKey) (*WebClient, types.WebSession, error) +type WebLoginFunc func(context.Context, *KeyRing) (*WebClient, types.WebSession, error) -func (tc *TeleportClient) loginWithHardwareKeyRetry(ctx context.Context, login func(ctx context.Context, priv *keys.PrivateKey) error) (*keys.PrivateKey, error) { - priv, err := tc.GetNewLoginKey(ctx) +func (tc *TeleportClient) loginWithHardwareKeyRetry(ctx context.Context, login func(ctx context.Context, keyRing *KeyRing) error) (*KeyRing, error) { + keyRing, err := tc.GetNewLoginKeyRing(ctx) if err != nil { return nil, trace.Wrap(err) } - loginErr := login(ctx, priv) + loginErr := login(ctx, keyRing) if loginErr != nil { if keys.IsPrivateKeyPolicyError(loginErr) { privateKeyPolicy, err := keys.ParsePrivateKeyPolicyError(loginErr) @@ -3796,16 +3800,16 @@ func (tc *TeleportClient) loginWithHardwareKeyRetry(ctx context.Context, login f } fmt.Fprintf(tc.Stderr, "Relogging in with hardware-backed private key.\n") - priv, err = tc.GetNewLoginKey(ctx) + keyRing, err = tc.GetNewLoginKeyRing(ctx) if err != nil { return nil, trace.Wrap(err) } - loginErr = login(ctx, priv) + loginErr = login(ctx, keyRing) } } - return priv, trace.Wrap(loginErr) + return keyRing, trace.Wrap(loginErr) } func (tc *TeleportClient) updatePrivateKeyPolicy(policy keys.PrivateKeyPolicy) error { @@ -3817,11 +3821,11 @@ func (tc *TeleportClient) updatePrivateKeyPolicy(policy keys.PrivateKeyPolicy) e return nil } -// GetNewLoginKey gets a new private key for login. -func (tc *TeleportClient) GetNewLoginKey(ctx context.Context) (priv *keys.PrivateKey, err error) { +// GetNewLoginKeyRing gets a KeyRing with new private keys for login. +func (tc *TeleportClient) GetNewLoginKeyRing(ctx context.Context) (keyRing *KeyRing, err error) { _, span := tc.Tracer.Start( ctx, - "teleportClient/GetNewLoginKey", + "teleportClient/GetNewLoginKeyRing", oteltrace.WithSpanKind(oteltrace.SpanKindClient), ) defer span.End() @@ -3831,38 +3835,68 @@ func (tc *TeleportClient) GetNewLoginKey(ctx context.Context) (priv *keys.Privat if tc.PIVSlot != "" { log.Debugf("Using PIV slot %q specified by client or server settings.", tc.PIVSlot) } - priv, err = keys.GetYubiKeyPrivateKey(ctx, tc.PrivateKeyPolicy, tc.PIVSlot) + priv, err := keys.GetYubiKeyPrivateKey(ctx, tc.PrivateKeyPolicy, tc.PIVSlot) if err != nil { return nil, trace.Wrap(err) } - return priv, nil + // Use a single hardware key for both protocols. + return NewKeyRing(priv, priv), nil } - log.Debugf("Attempting to login with a new RSA private key.") - priv, err = native.GeneratePrivateKey() + var sshKey, tlsKey crypto.Signer + if tc.GenerateUnifiedKey { + log.Debugf("Attempting to login with a new software private key.") + // Using the UserTLS key algorithm for both keys because SSH generally + // supports all TLS keys algorithms (RSA2048, ECDSAP256), but TLS does + // not support Ed25519 which may be used for SSH. + tlsKey, err = cryptosuites.GenerateKey(ctx, tc.GetCurrentSignatureAlgorithmSuite, cryptosuites.UserTLS) + if err != nil { + return nil, trace.Wrap(err) + } + sshKey = tlsKey + } else { + log.Debugf("Attempting to login with new software private keys.") + var err error + sshKey, tlsKey, err = cryptosuites.GenerateUserSSHAndTLSKey(ctx, tc.GetCurrentSignatureAlgorithmSuite) + if err != nil { + return nil, trace.Wrap(err) + } + } + + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + if err != nil { + return nil, trace.Wrap(err) + } + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) if err != nil { return nil, trace.Wrap(err) } - return priv, nil + return NewKeyRing(sshPriv, tlsPriv), nil } -// new SSHLogin generates a new SSHLogin using the given login key. -func (tc *TeleportClient) newSSHLogin(priv *keys.PrivateKey) (SSHLogin, error) { +// new SSHLogin generates a new SSHLogin using the given login KeyRing. +func (tc *TeleportClient) newSSHLogin(keyRing *KeyRing) (SSHLogin, error) { + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return SSHLogin{}, trace.Wrap(err) + } return SSHLogin{ - ProxyAddr: tc.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), - TTL: tc.KeyTTL, - Insecure: tc.InsecureSkipVerify, - Pool: loopbackPool(tc.WebProxyAddr), - Compatibility: tc.CertificateFormat, - RouteToCluster: tc.SiteName, - KubernetesCluster: tc.KubernetesCluster, - AttestationStatement: priv.GetAttestationStatement(), - ExtraHeaders: tc.ExtraProxyHeaders, + ProxyAddr: tc.WebProxyAddr, + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, + SSHAttestationStatement: keyRing.SSHPrivateKey.GetAttestationStatement(), + TLSAttestationStatement: keyRing.TLSPrivateKey.GetAttestationStatement(), + TTL: tc.KeyTTL, + Insecure: tc.InsecureSkipVerify, + Pool: loopbackPool(tc.WebProxyAddr), + Compatibility: tc.CertificateFormat, + RouteToCluster: tc.SiteName, + KubernetesCluster: tc.KubernetesCluster, + ExtraHeaders: tc.ExtraProxyHeaders, }, nil } -func (tc *TeleportClient) pwdlessLogin(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) pwdlessLogin(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { ctx, span := tc.Tracer.Start( ctx, "teleportClient/pwdlessLogin", @@ -3877,7 +3911,7 @@ func (tc *TeleportClient) pwdlessLogin(ctx context.Context, priv *keys.PrivateKe user = tc.Username } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -3893,7 +3927,7 @@ func (tc *TeleportClient) pwdlessLogin(ctx context.Context, priv *keys.PrivateKe return response, trace.Wrap(err) } -func (tc *TeleportClient) localLogin(ctx context.Context, priv *keys.PrivateKey, secondFactor constants.SecondFactorType) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) localLogin(ctx context.Context, keyRing *KeyRing, secondFactor constants.SecondFactorType) (*authclient.SSHLoginResponse, error) { var err error var response *authclient.SSHLoginResponse @@ -3902,12 +3936,12 @@ func (tc *TeleportClient) localLogin(ctx context.Context, priv *keys.PrivateKey, // deprecate the direct login endpoint. switch secondFactor { case constants.SecondFactorOff, constants.SecondFactorOTP: - response, err = tc.directLogin(ctx, secondFactor, priv) + response, err = tc.directLogin(ctx, secondFactor, keyRing) if err != nil { return nil, trace.Wrap(err) } case constants.SecondFactorU2F, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional: - response, err = tc.mfaLocalLogin(ctx, priv) + response, err = tc.mfaLocalLogin(ctx, keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -3921,7 +3955,7 @@ func (tc *TeleportClient) localLogin(ctx context.Context, priv *keys.PrivateKey, } // directLogin asks for a password + OTP token, makes a request to CA via proxy -func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType constants.SecondFactorType, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType constants.SecondFactorType, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { ctx, span := tc.Tracer.Start( ctx, "teleportClient/directLogin", @@ -3943,7 +3977,7 @@ func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType cons } } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -3960,7 +3994,7 @@ func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType cons } // mfaLocalLogin asks for a password and performs the challenge-response authentication -func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { ctx, span := tc.Tracer.Start( ctx, "teleportClient/mfaLocalLogin", @@ -3973,7 +4007,7 @@ func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, priv *keys.PrivateK return nil, trace.Wrap(err) } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -3988,12 +4022,12 @@ func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, priv *keys.PrivateK return response, trace.Wrap(err) } -func (tc *TeleportClient) headlessLogin(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) headlessLogin(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) { if tc.MockHeadlessLogin != nil { - return tc.MockHeadlessLogin(ctx, priv) + return tc.MockHeadlessLogin(ctx, keyRing) } - headlessAuthenticationID := services.NewHeadlessAuthenticationID(priv.MarshalSSHPublicKey()) + headlessAuthenticationID := services.NewHeadlessAuthenticationID(keyRing.SSHPrivateKey.MarshalSSHPublicKey()) webUILink, err := url.JoinPath("https://"+tc.WebProxyAddr, "web", "headless", headlessAuthenticationID) if err != nil { @@ -4005,10 +4039,15 @@ func (tc *TeleportClient) headlessLogin(ctx context.Context, priv *keys.PrivateK fmt.Fprintf(tc.Stderr, "Complete headless authentication in your local web browser:\n\n%s\n"+ "\nor execute this command in your local terminal:\n\n%s\n", webUILink, tshApprove) + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } response, err := SSHAgentHeadlessLogin(ctx, SSHLoginHeadless{ SSHLogin: SSHLogin{ ProxyAddr: tc.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, TTL: tc.KeyTTL, Insecure: tc.InsecureSkipVerify, Compatibility: tc.CertificateFormat, @@ -4024,7 +4063,7 @@ func (tc *TeleportClient) headlessLogin(ctx context.Context, priv *keys.PrivateK } // SSOLoginFunc is a function used in tests to mock SSO logins. -type SSOLoginFunc func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) +type SSOLoginFunc func(ctx context.Context, connectorID string, keyRing *KeyRing, protocol string) (*authclient.SSHLoginResponse, error) // TODO(atburke): DELETE in v17.0.0 func versionSupportsKeyPolicyMessage(proxyVersion *semver.Version) bool { @@ -4041,13 +4080,13 @@ func versionSupportsKeyPolicyMessage(proxyVersion *semver.Version) bool { } // samlLogin opens browser window and uses OIDC or SAML redirect cycle with browser -func (tc *TeleportClient) ssoLogin(ctx context.Context, priv *keys.PrivateKey, connectorID, connectorName, protocol string) (*authclient.SSHLoginResponse, error) { +func (tc *TeleportClient) ssoLogin(ctx context.Context, keyRing *KeyRing, connectorID, connectorName, protocol string) (*authclient.SSHLoginResponse, error) { if tc.MockSSOLogin != nil { // sso login response is being mocked for testing purposes - return tc.MockSSOLogin(ctx, connectorID, priv, protocol) + return tc.MockSSOLogin(ctx, connectorID, keyRing, protocol) } - sshLogin, err := tc.newSSHLogin(priv) + sshLogin, err := tc.newSSHLogin(keyRing) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/api_login_test.go b/lib/client/api_login_test.go index 42d12e2b151dc..36b6e1ffe3f61 100644 --- a/lib/client/api_login_test.go +++ b/lib/client/api_login_test.go @@ -21,6 +21,8 @@ package client_test import ( "bytes" "context" + "crypto/ecdsa" + "crypto/ed25519" "encoding/base32" "encoding/pem" "errors" @@ -308,11 +310,12 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { // Disable MFA. It makes testing easier. ctx := context.Background() authServer := sa.Auth.GetAuthServer() - authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ - Type: constants.Local, - SecondFactor: constants.SecondFactorOff, - }) - require.NoError(t, err, "NewAuthPreference failed") + authPref, err := authServer.GetAuthPreference(ctx) + require.NoError(t, err, "GetAuthPreference failed") + authPref.SetType(constants.Local) + authPref.SetSecondFactor(constants.SecondFactorOff) + authPref.SetAllowPasswordless(false) + authPref.SetAllowHeadless(false) _, err = authServer.UpsertAuthPreference(ctx, authPref) require.NoError(t, err, "UpsertAuthPreference failed") @@ -340,6 +343,10 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { keyRing, err := teleportClient.Login(ctx) require.NoError(t, err, "Login failed") + // Sanity check we're generating EC keys. + assert.IsType(t, ed25519.PrivateKey{}, keyRing.SSHPrivateKey.Signer) + assert.IsType(t, &ecdsa.PrivateKey{}, keyRing.TLSPrivateKey.Signer) + proxyClient, rootAuthClient, err := teleportClient.ConnectToRootCluster(ctx, keyRing) require.NoError(t, err, "Connecting to the root cluster failed") t.Cleanup(func() { @@ -392,8 +399,7 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { Certs: &devicepb.UserCertificates{ SshAuthorizedKey: keyRing.Cert, }, - // TODO(nklaassen): split SSH private key from TLS key. - SSHSigner: keyRing.PrivateKey, + SSHSigner: keyRing.SSHPrivateKey, }) require.NoError(t, err, "DeviceLogin failed") require.Equal(t, validCerts, got, "DeviceLogin mismatch") @@ -468,7 +474,7 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { Certs: &devicepb.UserCertificates{ SshAuthorizedKey: keyRing.Cert, }, - SSHSigner: keyRing.PrivateKey, + SSHSigner: keyRing.SSHPrivateKey, }) require.NoError(t, err, "DeviceLogin failed") assert.Equal(t, got, validCerts, "DeviceLogin mismatch") @@ -537,6 +543,7 @@ func newStandaloneTeleport(t *testing.T, clock clockwork.Clock) *standaloneBundl Webauthn: &types.Webauthn{ RPID: "localhost", }, + SignatureAlgorithmSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, }) require.NoError(t, err) cfg.Auth.BootstrapResources = []types.Resource{role, user} diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 45fbf56b52c51..536809ceea02b 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -41,6 +41,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/grpc/interceptors" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/modules" @@ -52,6 +53,7 @@ import ( func TestMain(m *testing.M) { utils.InitLoggerForTests() modules.SetInsecureTestMode(true) + native.PrecomputeTestKeys(m) os.Exit(m.Run()) } diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go index 3d8861d9a6f0b..331414b2cdeee 100644 --- a/lib/client/client_store_test.go +++ b/lib/client/client_store_test.go @@ -69,11 +69,13 @@ func newTestAuthority(t *testing.T) testAuthority { // makeSignedKeyRing helper returns a new user key ring signed by CAPriv key. func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeExpired bool) *KeyRing { - signer, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(context.Background(), func(context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }) require.NoError(t, err) - keyPEM, err := keys.MarshalPrivateKey(signer) + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) require.NoError(t, err) - priv, err := keys.NewPrivateKey(signer, keyPEM) + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) require.NoError(t, err) allowedLogins := []string{idx.Username, "root"} @@ -82,8 +84,6 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx ttl = -ttl } - // reuse the same keys for SSH and TLS keys - // TODO(nklaassen): don't reuse these keys. clock := clockwork.NewRealClock() identity := tlsca.Identity{ Username: idx.Username, @@ -93,7 +93,7 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx require.NoError(t, err) tlsCert, err := s.tlsCA.GenerateCertificate(tlsca.CertificateRequest{ Clock: clock, - PublicKey: priv.Public(), + PublicKey: tlsKey.Public(), Subject: subject, NotAfter: clock.Now().UTC().Add(ttl), }) @@ -104,7 +104,7 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx cert, err := s.keygen.GenerateUserCert(services.UserCertParams{ CASigner: caSigner, - PublicUserKey: ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), + PublicUserKey: sshPriv.MarshalSSHPublicKey(), Username: idx.Username, AllowedLogins: allowedLogins, TTL: ttl, @@ -113,15 +113,14 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx }) require.NoError(t, err) - keyRing := NewKeyRing(priv) + keyRing := NewKeyRing(sshPriv, tlsPriv) keyRing.KeyRingIndex = idx - keyRing.PrivateKey = priv keyRing.Cert = cert keyRing.TLSCert = tlsCert keyRing.TrustedCerts = []authclient.TrustedCerts{s.trustedCerts} keyRing.DBTLSCredentials["example-db"] = TLSCredential{ Cert: tlsCert, - PrivateKey: priv, + PrivateKey: tlsPriv, } return keyRing } @@ -197,12 +196,12 @@ func TestClientStore(t *testing.T) { require.NoError(t, err) expectKeyRing := keyRing.Copy() expectKeyRing.TrustedCerts = nil - require.Equal(t, expectKeyRing, retrievedKeyRing) + assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing) // Getting the key from the client store should fill in the trusted certs. retrievedKeyRing, err = clientStore.GetKeyRing(idx, WithAllCerts...) require.NoError(t, err) - require.Equal(t, keyRing, retrievedKeyRing) + assertEqualKeyRings(t, keyRing, retrievedKeyRing) var profileDir string if fs, ok := clientStore.KeyStore.(*FSKeyStore); ok { @@ -412,7 +411,8 @@ func BenchmarkLoadKeysToKubeFromStore(b *testing.B) { Username: "tester", ClusterName: "teleportcluster", }, - PrivateKey: privateKey, + TLSPrivateKey: privateKey, + SSHPrivateKey: privateKey, TLSCert: certPEM, KubeTLSCredentials: make(map[string]TLSCredential, 10), } diff --git a/lib/client/cluster_client.go b/lib/client/cluster_client.go index e1a4dd458bc19..e084ea1c21de7 100644 --- a/lib/client/cluster_client.go +++ b/lib/client/cluster_client.go @@ -197,7 +197,7 @@ func (c *ClusterClient) generateUserCerts(ctx context.Context, cachePolicy CertC } } - privKey, req, err := c.prepareUserCertsRequest(ctx, params, keyRing) + newUserKeys, req, err := c.prepareUserCertsRequest(ctx, params, keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -223,27 +223,30 @@ func (c *ClusterClient) generateUserCerts(ctx context.Context, cachePolicy CertC // usage-restricted certificates. switch params.usage() { case proto.UserCertsRequest_All: + keyRing.SSHPrivateKey = newUserKeys.ssh + keyRing.TLSPrivateKey = newUserKeys.tls keyRing.Cert = certs.SSH keyRing.TLSCert = certs.TLS case proto.UserCertsRequest_SSH: + keyRing.SSHPrivateKey = newUserKeys.ssh keyRing.Cert = certs.SSH case proto.UserCertsRequest_App: keyRing.AppTLSCredentials[params.RouteToApp.Name] = TLSCredential{ - PrivateKey: privKey, + PrivateKey: newUserKeys.app, Cert: certs.TLS, } case proto.UserCertsRequest_Database: - dbCert, err := makeDatabaseClientPEM(params.RouteToDatabase.Protocol, certs.TLS, privKey) + dbCert, err := makeDatabaseClientPEM(params.RouteToDatabase.Protocol, certs.TLS, newUserKeys.db) if err != nil { return nil, trace.Wrap(err) } keyRing.DBTLSCredentials[params.RouteToDatabase.ServiceName] = TLSCredential{ Cert: dbCert, - PrivateKey: privKey, + PrivateKey: newUserKeys.db, } case proto.UserCertsRequest_Kubernetes: keyRing.KubeTLSCredentials[params.KubernetesCluster] = TLSCredential{ - PrivateKey: privKey, + PrivateKey: newUserKeys.kube, Cert: certs.TLS, } } @@ -334,7 +337,7 @@ func (c *ClusterClient) SessionSSHConfig(ctx context.Context, user string, targe // prepareUserCertsRequest creates a [proto.UserCertsRequest] with the fields // set accordingly from the provided ReissueParams. -func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params ReissueParams, keyRing *KeyRing) (*keys.PrivateKey, *proto.UserCertsRequest, error) { +func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params ReissueParams, keyRing *KeyRing) (*newUserKeys, *proto.UserCertsRequest, error) { tlsCert, err := keyRing.TeleportTLSCertificate() if err != nil { return nil, nil, trace.Wrap(err) @@ -352,35 +355,36 @@ func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params Reis params.AccessRequests = activeRequests.AccessRequests } - var sshPublicKey, tlsPublicKey []byte - var privateKey *keys.PrivateKey + // newUserKeys holds new subject keys per-protocol so that the keyring can + // be updated with the correct keys if cert issuance is successful. + newUserKeys := &newUserKeys{} + var sshSubjectKey, tlsSubjectKey *keys.PrivateKey switch params.usage() { - case proto.UserCertsRequest_App, proto.UserCertsRequest_Kubernetes: - privateKey, err = keyRing.generateSubjectTLSKey(ctx, c.tc, cryptosuites.UserTLS) + case proto.UserCertsRequest_App: + tlsSubjectKey, err = keyRing.generateSubjectTLSKey(ctx, c.tc, cryptosuites.UserTLS) if err != nil { return nil, nil, trace.Wrap(err) } - tlsPublicKey, err = keys.MarshalPublicKey(privateKey.Public()) + newUserKeys.app = tlsSubjectKey + case proto.UserCertsRequest_Kubernetes: + tlsSubjectKey, err = keyRing.generateSubjectTLSKey(ctx, c.tc, cryptosuites.UserTLS) if err != nil { return nil, nil, trace.Wrap(err) } + newUserKeys.kube = tlsSubjectKey case proto.UserCertsRequest_Database: - privateKey, err = keyRing.generateSubjectTLSKey(ctx, c.tc, cryptosuites.UserDatabase) - if err != nil { - return nil, nil, trace.Wrap(err) - } - tlsPublicKey, err = keys.MarshalPublicKey(privateKey.Public()) + tlsSubjectKey, err = keyRing.generateSubjectTLSKey(ctx, c.tc, cryptosuites.UserDatabase) if err != nil { return nil, nil, trace.Wrap(err) } + newUserKeys.db = tlsSubjectKey default: - // TODO(nklaassen): split the SSH and TLS key for remaining protocols. - privateKey = keyRing.PrivateKey - sshPublicKey = keyRing.PrivateKey.MarshalSSHPublicKey() - tlsPublicKey, err = keys.MarshalPublicKey(keyRing.PrivateKey.Public()) - if err != nil { - return nil, nil, trace.Wrap(err) - } + // Assume we're reissuing the base SSH and TLS certs, reuse the existing + // private keys. + sshSubjectKey = keyRing.SSHPrivateKey + tlsSubjectKey = keyRing.TLSPrivateKey + newUserKeys.ssh = sshSubjectKey + newUserKeys.tls = tlsSubjectKey } expires := tlsCert.NotAfter @@ -388,30 +392,45 @@ func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params Reis expires = time.Now().Add(params.TTL) } - return privateKey, &proto.UserCertsRequest{ - SSHPublicKey: sshPublicKey, - TLSPublicKey: tlsPublicKey, - Username: tlsCert.Subject.CommonName, - Expires: expires, - RouteToCluster: params.RouteToCluster, - KubernetesCluster: params.KubernetesCluster, - AccessRequests: params.AccessRequests, - DropAccessRequests: params.DropAccessRequests, - RouteToDatabase: params.RouteToDatabase, - RouteToApp: params.RouteToApp, - NodeName: params.NodeName, - Usage: params.usage(), - Format: c.tc.CertificateFormat, - RequesterName: params.RequesterName, - SSHLogin: c.tc.HostLogin, - AttestationStatement: keyRing.PrivateKey.GetAttestationStatement().ToProto(), + var sshPub, tlsPub []byte + var sshAttestationStatement, tlsAttestationStatement *keys.AttestationStatement + if sshSubjectKey != nil { + sshPub = sshSubjectKey.MarshalSSHPublicKey() + sshAttestationStatement = sshSubjectKey.GetAttestationStatement() + } + if tlsSubjectKey != nil { + tlsPub, err = tlsSubjectKey.MarshalTLSPublicKey() + if err != nil { + return nil, nil, trace.Wrap(err) + } + tlsAttestationStatement = tlsSubjectKey.GetAttestationStatement() + } + + return newUserKeys, &proto.UserCertsRequest{ + SSHPublicKey: sshPub, + TLSPublicKey: tlsPub, + Username: tlsCert.Subject.CommonName, + Expires: expires, + RouteToCluster: params.RouteToCluster, + KubernetesCluster: params.KubernetesCluster, + AccessRequests: params.AccessRequests, + DropAccessRequests: params.DropAccessRequests, + RouteToDatabase: params.RouteToDatabase, + RouteToApp: params.RouteToApp, + NodeName: params.NodeName, + Usage: params.usage(), + Format: c.tc.CertificateFormat, + RequesterName: params.RequesterName, + SSHLogin: c.tc.HostLogin, + SSHPublicKeyAttestationStatement: sshAttestationStatement.ToProto(), + TLSPublicKeyAttestationStatement: tlsAttestationStatement.ToProto(), }, nil } // performMFACeremony runs the mfa ceremony to completion. // If successful the returned [KeyRing] will be authorized to connect to the target. func (c *ClusterClient) performMFACeremony(ctx context.Context, rootClient *ClusterClient, params ReissueParams, keyRing *KeyRing, mfaPrompt mfa.Prompt) (*KeyRing, error) { - privKey, certsReq, err := rootClient.prepareUserCertsRequest(ctx, params, keyRing) + newUserKeys, certsReq, err := rootClient.prepareUserCertsRequest(ctx, params, keyRing) if err != nil { return nil, trace.Wrap(err) } @@ -429,9 +448,9 @@ func (c *ClusterClient) performMFACeremony(ctx context.Context, rootClient *Clus ChallengeExtensions: mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, }, - CertsReq: certsReq, - KeyRing: keyRing, - PrivateKey: privKey, + CertsReq: certsReq, + KeyRing: keyRing, + newUserKeys: newUserKeys, }) return keyRing, trace.Wrap(err) } @@ -587,9 +606,13 @@ type PerformMFACeremonyParams struct { // Optional. KeyRing *KeyRing - // PrivateKey is the private key to include in any TLSCredential added to - // [KeyRing]. - PrivateKey *keys.PrivateKey + // newUserKeys holds private keys that should be used as the subject of any + // new keys added to [KeyRing]. + newUserKeys *newUserKeys +} + +type newUserKeys struct { + ssh, tls, app, db, kube *keys.PrivateKey } // PerformMFACeremony issues single-use certificates via GenerateUserCerts, @@ -689,10 +712,10 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* } keyRing.KubeTLSCredentials[certsReq.KubernetesCluster] = TLSCredential{ Cert: newCerts.TLS, - PrivateKey: params.PrivateKey, + PrivateKey: params.newUserKeys.kube, } case proto.UserCertsRequest_Database: - dbCert, err := makeDatabaseClientPEM(certsReq.RouteToDatabase.Protocol, newCerts.TLS, params.PrivateKey) + dbCert, err := makeDatabaseClientPEM(certsReq.RouteToDatabase.Protocol, newCerts.TLS, params.newUserKeys.db) if err != nil { return nil, nil, trace.Wrap(err) } @@ -701,7 +724,7 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* } keyRing.DBTLSCredentials[certsReq.RouteToDatabase.ServiceName] = TLSCredential{ Cert: dbCert, - PrivateKey: params.PrivateKey, + PrivateKey: params.newUserKeys.db, } case proto.UserCertsRequest_App: if keyRing.AppTLSCredentials == nil { @@ -709,7 +732,7 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* } keyRing.AppTLSCredentials[certsReq.RouteToApp.Name] = TLSCredential{ Cert: newCerts.TLS, - PrivateKey: params.PrivateKey, + PrivateKey: params.newUserKeys.app, } default: return nil, nil, trace.BadParameter("server returned a TLS certificate but cert request usage was %s", certsReq.Usage) diff --git a/lib/client/conntest/ssh.go b/lib/client/conntest/ssh.go index d647d9405764c..bb363b4d3c414 100644 --- a/lib/client/conntest/ssh.go +++ b/lib/client/conntest/ssh.go @@ -33,7 +33,6 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" @@ -113,7 +112,7 @@ func (s *SSHConnectionTester) TestConnection(ctx context.Context, req TestConnec return nil, trace.Wrap(err) } - publicKeyPEM, err := keys.MarshalPublicKey(keyRing.PrivateKey.Public()) + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() if err != nil { return nil, trace.Wrap(err) } @@ -129,8 +128,8 @@ func (s *SSHConnectionTester) TestConnection(ctx context.Context, req TestConnec } certs, err := s.cfg.UserClient.GenerateUserCerts(ctx, proto.UserCertsRequest{ - SSHPublicKey: keyRing.PrivateKey.MarshalSSHPublicKey(), - TLSPublicKey: publicKeyPEM, + SSHPublicKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPublicKey: tlsPub, Username: currentUser.GetName(), Expires: time.Now().Add(time.Minute).UTC(), ConnectionDiagnosticID: connectionDiagnosticID, diff --git a/lib/client/db/database_certificates.go b/lib/client/db/database_certificates.go index ed8cbab0fff8d..3a1f49f3ccadc 100644 --- a/lib/client/db/database_certificates.go +++ b/lib/client/db/database_certificates.go @@ -94,6 +94,7 @@ func GenerateDatabaseServerCertificates(ctx context.Context, req GenerateDatabas } if req.KeyRing == nil { + // TODO(nklaassen): don't hardcode RSA here. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return nil, trace.Wrap(err) @@ -101,7 +102,7 @@ func GenerateDatabaseServerCertificates(ctx context.Context, req GenerateDatabas req.KeyRing = keyRing } - csr, err := tlsca.GenerateCertificateRequestPEM(subject, req.KeyRing.PrivateKey) + csr, err := tlsca.GenerateCertificateRequestPEM(subject, req.KeyRing.TLSPrivateKey) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/db/oracle/oracle.go b/lib/client/db/oracle/oracle.go index 5a3760001ed32..cb4b0c5a79730 100644 --- a/lib/client/db/oracle/oracle.go +++ b/lib/client/db/oracle/oracle.go @@ -20,6 +20,7 @@ package oracle import ( "bytes" + "crypto" "crypto/x509" "os" "path/filepath" @@ -32,7 +33,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/constants" - "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" @@ -42,8 +42,8 @@ import ( // wallet.jks - Java Wallet format used by JDBC Drivers. // sqlnet.ora - Generic Oracle Client Configuration File allowing to specify Wallet Location. // tnsnames.ora - Oracle Net Service mapped to connections descriptors. -func GenerateClientConfiguration(keyRing *client.KeyRing, db tlsca.RouteToDatabase, profile *client.ProfileStatus) error { - walletPath := profile.OracleWalletDir(keyRing.ClusterName, db.ServiceName) +func GenerateClientConfiguration(signer crypto.Signer, db tlsca.RouteToDatabase, profile *client.ProfileStatus) error { + walletPath := profile.OracleWalletDir(profile.Cluster, db.ServiceName) if err := os.MkdirAll(walletPath, teleport.PrivateDirMode); err != nil { return trace.Wrap(err) } @@ -57,7 +57,7 @@ func GenerateClientConfiguration(keyRing *client.KeyRing, db tlsca.RouteToDataba return trace.ConvertSystemError(err) } - jksWalletPath, err := createClientWallet(keyRing, localProxyCAPem, password, walletPath) + jksWalletPath, err := createClientWallet(signer, localProxyCAPem, password, walletPath) if err != nil { return trace.Wrap(err) } @@ -73,8 +73,8 @@ func GenerateClientConfiguration(keyRing *client.KeyRing, db tlsca.RouteToDataba return nil } -func createClientWallet(keyRing *client.KeyRing, certPem []byte, password string, walletPath string) (string, error) { - buff, err := createJKSWallet(keyRing.PrivateKey.PrivateKeyPEM(), certPem, certPem, password) +func createClientWallet(signer crypto.Signer, certPem []byte, password string, walletPath string) (string, error) { + buff, err := createJKSWallet(signer, certPem, certPem, password) if err != nil { return "", trace.Wrap(err) } @@ -85,12 +85,8 @@ func createClientWallet(keyRing *client.KeyRing, certPem []byte, password string return walletFile, nil } -func createJKSWallet(keyPEM, certPEM, caPEM []byte, password string) ([]byte, error) { - key, err := keys.ParsePrivateKey(keyPEM) - if err != nil { - return nil, trace.Wrap(err) - } - privateKey, err := x509.MarshalPKCS8PrivateKey(key.Signer) +func createJKSWallet(signer crypto.Signer, certPEM, caPEM []byte, password string) ([]byte, error) { + privateKey, err := x509.MarshalPKCS8PrivateKey(signer) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index ebec2a4d866b3..ad929ce4cc218 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -226,13 +226,20 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err switch cfg.Format { // dump user identity into a single file: case FormatFile: + // Identity files only hold a single private key, and all certs are + // associated with that key. All callers should provide a + // [client.KeyRing] where [KeyRing.SSHPrivateKey] and + // [KeyRing.TLSPrivateKey] are equal. Assert that here. + if !bytes.Equal(cfg.KeyRing.SSHPrivateKey.MarshalSSHPublicKey(), cfg.KeyRing.TLSPrivateKey.MarshalSSHPublicKey()) { + return nil, trace.BadParameter("identity files don't support mismatched SSH and TLS keys, this is a bug") + } filesWritten = append(filesWritten, cfg.OutputPath) if err := checkOverwrite(ctx, writer, cfg.OverwriteDestination, filesWritten...); err != nil { return nil, trace.Wrap(err) } idFile := &identityfile.IdentityFile{ - PrivateKey: cfg.KeyRing.PrivateKey.PrivateKeyPEM(), + PrivateKey: cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM(), Certs: identityfile.Certs{ SSH: cfg.KeyRing.Cert, TLS: cfg.KeyRing.TLSCert, @@ -279,7 +286,11 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err return nil, trace.Wrap(err) } - err = writer.WriteFile(keyPath, cfg.KeyRing.PrivateKey.PrivateKeyPEM(), identityfile.FilePermissions) + sshPrivateKeyPEM, err := cfg.KeyRing.SSHPrivateKey.MarshalSSHPrivateKey() + if err != nil { + return nil, trace.Wrap(err) + } + err = writer.WriteFile(keyPath, sshPrivateKeyPEM, identityfile.FilePermissions) if err != nil { return nil, trace.Wrap(err) } @@ -316,7 +327,7 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err return nil, trace.Wrap(err) } - err = writer.WriteFile(keyPath, cfg.KeyRing.PrivateKey.PrivateKeyPEM(), identityfile.FilePermissions) + err = writer.WriteFile(keyPath, cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM(), identityfile.FilePermissions) if err != nil { return nil, trace.Wrap(err) } @@ -356,7 +367,7 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err return nil, trace.Wrap(err) } - err = writer.WriteFile(keyPath, cfg.KeyRing.PrivateKey.PrivateKeyPEM(), identityfile.FilePermissions) + err = writer.WriteFile(keyPath, cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM(), identityfile.FilePermissions) if err != nil { return nil, trace.Wrap(err) } @@ -379,7 +390,7 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err if err := checkOverwrite(ctx, writer, cfg.OverwriteDestination, filesWritten...); err != nil { return nil, trace.Wrap(err) } - err = writer.WriteFile(certPath, append(cfg.KeyRing.TLSCert, cfg.KeyRing.PrivateKey.PrivateKeyPEM()...), identityfile.FilePermissions) + err = writer.WriteFile(certPath, append(cfg.KeyRing.TLSCert, cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM()...), identityfile.FilePermissions) if err != nil { return nil, trace.Wrap(err) } @@ -519,7 +530,7 @@ func writeOracleFormat(cfg WriteConfig, writer ConfigWriter) ([]string, error) { if err != nil { return nil, trace.Wrap(err) } - keyK, err := keys.ParsePrivateKey(cfg.KeyRing.PrivateKey.PrivateKeyPEM()) + keyK, err := keys.ParsePrivateKey(cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM()) if err != nil { return nil, trace.Wrap(err) } @@ -662,7 +673,7 @@ func prepareCassandraTruststore(cfg WriteConfig) (*bytes.Buffer, error) { func prepareCassandraKeystore(cfg WriteConfig) (*bytes.Buffer, error) { certBlock, _ := pem.Decode(cfg.KeyRing.TLSCert) - privBlock, _ := pem.Decode(cfg.KeyRing.PrivateKey.PrivateKeyPEM()) + privBlock, _ := pem.Decode(cfg.KeyRing.TLSPrivateKey.PrivateKeyPEM()) privKey, err := x509.ParsePKCS1PrivateKey(privBlock.Bytes) if err != nil { @@ -741,7 +752,8 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string) (*clie return nil, trace.Wrap(err) } - keyRing := client.NewKeyRing(priv) + // Identity file uses same private key for SSH and TLS. + keyRing := client.NewKeyRing(priv, priv) keyRing.Cert = ident.Certs.SSH keyRing.TLSCert = ident.Certs.TLS keyRing.KeyRingIndex = client.KeyRingIndex{ @@ -871,7 +883,7 @@ func LoadIdentityFileIntoClientStore(store *client.Store, identityFile, proxyAdd WebProxyAddr: proxyAddr, SiteName: keyRing.ClusterName, Username: keyRing.Username, - PrivateKeyPolicy: keyRing.PrivateKey.GetPrivateKeyPolicy(), + PrivateKeyPolicy: keyRing.TLSPrivateKey.GetPrivateKeyPolicy(), } if err := store.SaveProfile(profile, true); err != nil { return trace.Wrap(err) diff --git a/lib/client/identityfile/identity_test.go b/lib/client/identityfile/identity_test.go index bc0e095c50b3b..3f52aefe162db 100644 --- a/lib/client/identityfile/identity_test.go +++ b/lib/client/identityfile/identity_test.go @@ -75,6 +75,7 @@ func newSelfSignedCA(priv crypto.Signer) (*tlsca.CertAuthority, authclient.Trust } func newClientKeyRing(t *testing.T, modifiers ...func(*tlsca.Identity)) *client.KeyRing { + // Some formats only support RSA (certain DBs, PPK files). privateKey, err := keys.ParsePrivateKey(fixtures.PEMBytes["rsa"]) require.NoError(t, err) @@ -115,7 +116,8 @@ func newClientKeyRing(t *testing.T, modifiers ...func(*tlsca.Identity)) *client. }) require.NoError(t, err) - keyRing := client.NewKeyRing(privateKey) + // Identity files use a single key for SSH and TLS. + keyRing := client.NewKeyRing(privateKey, privateKey) keyRing.KeyRingIndex = client.KeyRingIndex{ ProxyHost: "localhost", Username: "testuser", @@ -143,7 +145,7 @@ func TestWrite(t *testing.T) { // key is OK: out, err := os.ReadFile(cfg.OutputPath) require.NoError(t, err) - require.Equal(t, string(out), string(keyRing.PrivateKey.PrivateKeyPEM())) + require.Equal(t, string(out), string(keyRing.SSHPrivateKey.PrivateKeyPEM())) // cert is OK: out, err = os.ReadFile(keypaths.IdentitySSHCertPath(cfg.OutputPath)) @@ -168,7 +170,7 @@ func TestWrite(t *testing.T) { require.NoError(t, err) wantArr := [][]byte{ - keyRing.PrivateKey.PrivateKeyPEM(), + keyRing.TLSPrivateKey.PrivateKeyPEM(), keyRing.Cert, keyRing.TLSCert, []byte(knownHosts), diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index 767405256c227..78970a6c830e0 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -101,24 +101,24 @@ func (c *TLSCredential) TLSCertificate() (tls.Certificate, error) { type KeyRing struct { KeyRingIndex - // PrivateKey used to represent the single cryptographic key associated with all - // certificates in the KeyRing. This is in the process of being deprecated - // and replaced with unique keys for each certificate, as part of the - // implementation of RFD 136. - PrivateKey *keys.PrivateKey + // SSHPrivateKey is a private key used for SSH authentication. + SSHPrivateKey *keys.PrivateKey + // Cert is an SSH client certificate. + Cert []byte - // Cert is an SSH client certificate - Cert []byte `json:"Cert,omitempty"` + // TLSPrivateKey is a private key used for TLS authentication. + TLSPrivateKey *keys.PrivateKey // TLSCert is a PEM encoded client TLS x509 certificate. // It's used to authenticate to the Teleport APIs. - TLSCert []byte `json:"TLSCert,omitempty"` - // KubeTLSCredentials are TLS credentials for individual - // kubernetes clusters. Map key is a kubernetes cluster name. + TLSCert []byte + + // KubeTLSCredentials are TLS credentials for individual kubernetes clusters. + // Map key is a kubernetes cluster name. KubeTLSCredentials map[string]TLSCredential // DBTLSCredentials are TLS credentials for database access. // Map key is the database service name. DBTLSCredentials map[string]TLSCredential - // AppTLSCredetials are TLS credentials for application access. + // AppTLSCredentials are TLS credentials for application access. // Map key is the application name. AppTLSCredentials map[string]TLSCredential // TrustedCerts is a list of trusted certificate authorities @@ -138,46 +138,45 @@ func (k *KeyRing) Copy() *KeyRing { // [purpose], meant to be used as the subject key for a new user cert request. // If [k.PrivateKey] is a PIV/hardware key or an RSA key, it will be re-used. func (k *KeyRing) generateSubjectTLSKey(ctx context.Context, tc *TeleportClient, purpose cryptosuites.KeyPurpose) (*keys.PrivateKey, error) { - if k.PrivateKey.IsHardware() { + if k.TLSPrivateKey.IsHardware() { // We always re-use the root TLS key if it is a hardware key. - return k.PrivateKey, nil + return k.TLSPrivateKey, nil } - if _, isRSA := k.PrivateKey.Public().(*rsa.PublicKey); isRSA { + if _, isRSA := k.TLSPrivateKey.Public().(*rsa.PublicKey); isRSA { // We always re-use the root TLS key if it is RSA (it would be expensive // to always generate new RSA keys). If [k.PrivateKey] is RSA we must be // using the `legacy` signature algorithm suitei and the subject keys // should be RSA as well. - return k.PrivateKey, nil + return k.TLSPrivateKey, nil } key, err := cryptosuites.GenerateKey(ctx, tc.GetCurrentSignatureAlgorithmSuite, purpose) if err != nil { return nil, trace.Wrap(err) } - privateKeyPEM, err := keys.MarshalPrivateKey(key) - if err != nil { - return nil, trace.Wrap(err) - } - priv, err := keys.NewPrivateKey(key, privateKeyPEM) + priv, err := keys.NewSoftwarePrivateKey(key) if err != nil { return nil, trace.Wrap(err) } return priv, nil } -// GenerateRSAKeyRing generates a new unsigned key ring. +// GenerateRSAKeyRing generates a new unsigned RSA key ring. +// +// TODO(nklaassen): get away from hardcoding RSA here. func GenerateRSAKeyRing() (*KeyRing, error) { priv, err := native.GeneratePrivateKey() if err != nil { return nil, trace.Wrap(err) } - return NewKeyRing(priv), nil + return NewKeyRing(priv, priv), nil } -// NewKeyRing creates a new KeyRing for the given private key. -func NewKeyRing(priv *keys.PrivateKey) *KeyRing { +// NewKeyRing creates a new KeyRing for the given private keys. +func NewKeyRing(sshPriv, tlsPriv *keys.PrivateKey) *KeyRing { return &KeyRing{ - PrivateKey: priv, + SSHPrivateKey: sshPriv, + TLSPrivateKey: tlsPriv, KubeTLSCredentials: make(map[string]TLSCredential), DBTLSCredentials: make(map[string]TLSCredential), AppTLSCredentials: make(map[string]TLSCredential), @@ -276,7 +275,7 @@ func (k *KeyRing) TeleportClientTLSConfig(cipherSuites []uint16, clusters []stri return nil, trace.NotFound("TLS certificate not found") } return k.clientTLSConfig(cipherSuites, TLSCredential{ - PrivateKey: k.PrivateKey, + PrivateKey: k.TLSPrivateKey, Cert: k.TLSCert, }, clusters) } @@ -335,7 +334,7 @@ func (k *KeyRing) ProxyClientSSHConfig(hostname string) (*ssh.ClientConfig, erro return nil, trace.Wrap(err, "failed to extract username from SSH certificate") } - sshConfig, err := sshutils.ProxyClientSSHConfig(sshCert, k.PrivateKey.Signer) + sshConfig, err := sshutils.ProxyClientSSHConfig(sshCert, k.SSHPrivateKey.Signer) if err != nil { return nil, trace.Wrap(err) } @@ -432,7 +431,7 @@ func (k *KeyRing) AsAgentKey() (agent.AddedKey, error) { } return agent.AddedKey{ - PrivateKey: k.PrivateKey.Signer, + PrivateKey: k.SSHPrivateKey.Signer, Certificate: sshCert, Comment: teleportAgentKeyComment(k.KeyRingIndex), LifetimeSecs: 0, @@ -552,7 +551,7 @@ func (k *KeyRing) AsAuthMethod() (ssh.AuthMethod, error) { if err != nil { return nil, trace.Wrap(err) } - return sshutils.AsAuthMethod(cert, k.PrivateKey) + return sshutils.AsAuthMethod(cert, k.SSHPrivateKey) } // SSHSigner returns an ssh.Signer using the SSH certificate in this key. @@ -561,7 +560,7 @@ func (k *KeyRing) SSHSigner() (ssh.Signer, error) { if err != nil { return nil, trace.Wrap(err) } - return sshutils.SSHSigner(cert, k.PrivateKey) + return sshutils.SSHSigner(cert, k.SSHPrivateKey) } // SSHCert returns parsed SSH certificate @@ -606,7 +605,7 @@ func (k *KeyRing) CheckCert() error { func (k *KeyRing) checkCert(sshCert *ssh.Certificate) error { // Check that the certificate was for the current public key. If not, the // public/private key pair may have been rotated. - if !sshutils.KeysEqual(sshCert.Key, k.PrivateKey.SSHPublicKey()) { + if !sshutils.KeysEqual(sshCert.Key, k.SSHPrivateKey.SSHPublicKey()) { return trace.CompareFailed("public key in profile does not match the public key in SSH certificate") } @@ -645,6 +644,8 @@ func (k *KeyRing) EqualPrivateKey(other *KeyRing) bool { // For example, for PIV keys, the private key PEM only uniquely // identifies a PIV slot, so we can use the public key to verify // that the private key on the slot hasn't changed. - return subtle.ConstantTimeCompare(k.PrivateKey.PrivateKeyPEM(), other.PrivateKey.PrivateKeyPEM()) == 1 && - bytes.Equal(k.PrivateKey.MarshalSSHPublicKey(), other.PrivateKey.MarshalSSHPublicKey()) + return bytes.Equal(k.SSHPrivateKey.MarshalSSHPublicKey(), other.SSHPrivateKey.MarshalSSHPublicKey()) && + bytes.Equal(k.TLSPrivateKey.MarshalSSHPublicKey(), other.TLSPrivateKey.MarshalSSHPublicKey()) && + subtle.ConstantTimeCompare(k.SSHPrivateKey.PrivateKeyPEM(), other.SSHPrivateKey.PrivateKeyPEM()) == 1 && + subtle.ConstantTimeCompare(k.TLSPrivateKey.PrivateKeyPEM(), other.TLSPrivateKey.PrivateKeyPEM()) == 1 } diff --git a/lib/client/keyagent.go b/lib/client/keyagent.go index 89181fee0ad02..741c1f5b989ec 100644 --- a/lib/client/keyagent.go +++ b/lib/client/keyagent.go @@ -302,11 +302,6 @@ func (a *LocalKeyAgent) GetKeyRing(clusterName string, opts ...CertOption) (*Key if err != nil { return nil, trace.Wrap(err) } - trustedCerts, err := a.clientStore.GetTrustedCerts(idx.ProxyHost) - if err != nil { - return nil, trace.Wrap(err) - } - keyRing.TrustedCerts = trustedCerts return keyRing, nil } @@ -627,7 +622,7 @@ func (a *LocalKeyAgent) Signers() ([]ssh.Signer, error) { if err := k.checkCert(cert); err != nil { return nil, trace.Wrap(err) } - signer, err := sshutils.SSHSigner(cert, k.PrivateKey.Signer) + signer, err := sshutils.SSHSigner(cert, k.SSHPrivateKey.Signer) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index 976d46a0c8c25..bbd2ae2677dc7 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -20,6 +20,7 @@ package client import ( "bytes" + "context" "fmt" "io" "net" @@ -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" "golang.org/x/crypto/ssh/agent" @@ -44,8 +46,8 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/sshutils" "github.com/gravitational/teleport/lib/auth/authclient" - "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/auth/testauthority" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/fixtures" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/tlsca" @@ -109,11 +111,7 @@ func makeSuite(t *testing.T, opts ...keyAgentTestSuiteFunc) *KeyAgentTestSuite { s.tlsca, s.tlscaCert, err = newSelfSignedCA(pemBytes, settings.clusterName) require.NoError(t, err) - keygen := testauthority.New() - priv, err := keygen.GeneratePrivateKey() - require.NoError(t, err) - - s.keyRing = s.makeKeyRing(t, s.username, s.hostname, priv) + s.keyRing = s.makeKeyRing(t, s.username, s.hostname) return s } @@ -142,7 +140,8 @@ func TestAddKey(t *testing.T) { // check that the key has been written to disk expectedFiles := []string{ - keypaths.UserKeyPath(s.keyDir, s.hostname, s.username), // private key + keypaths.UserSSHKeyPath(s.keyDir, s.hostname, s.username), // SSH private key + keypaths.UserTLSKeyPath(s.keyDir, s.hostname, s.username), // TLS private key keypaths.TLSCertPath(s.keyDir, s.hostname, s.username), // Teleport TLS certificate keypaths.SSHCertPath(s.keyDir, s.hostname, s.username, s.keyRing.ClusterName), // SSH certificate } @@ -160,22 +159,22 @@ func TestAddKey(t *testing.T) { // and it's for the user we expected to add a certificate for expectComment := teleportAgentKeyComment(s.keyRing.KeyRingIndex) require.Len(t, teleportAgentKeys, 2) - require.Equal(t, ssh.CertAlgoRSAv01, teleportAgentKeys[0].Type()) - require.Equal(t, expectComment, teleportAgentKeys[0].Comment) - require.Equal(t, "ssh-rsa", teleportAgentKeys[1].Type()) - require.Equal(t, expectComment, teleportAgentKeys[1].Comment) + assert.Equal(t, ssh.CertAlgoED25519v01, teleportAgentKeys[0].Type()) + assert.Equal(t, expectComment, teleportAgentKeys[0].Comment) + assert.Equal(t, "ssh-ed25519", teleportAgentKeys[1].Type()) + assert.Equal(t, expectComment, teleportAgentKeys[1].Comment) // check that we've loaded a cert as well as a private key into the system again found := false for _, sak := range systemAgentKeys { - if sak.Comment == expectComment && sak.Type() == "ssh-rsa" { + if sak.Comment == expectComment && sak.Type() == "ssh-ed25519" { found = true } } require.True(t, found) found = false for _, sak := range systemAgentKeys { - if sak.Comment == expectComment && sak.Type() == ssh.CertAlgoRSAv01 { + if sak.Comment == expectComment && sak.Type() == ssh.CertAlgoED25519v01 { found = true } } @@ -205,8 +204,8 @@ func TestLoadKey(t *testing.T) { // Create 3 separate keyRings, with overlapping user and cluster names keyRings := []*KeyRing{ s.keyRing, - s.genKeyRing(t, s.keyRing.Username, "other-proxy-host"), - s.genKeyRing(t, "other-user", s.keyRing.ProxyHost), + s.makeKeyRing(t, s.keyRing.Username, "other-proxy-host"), + s.makeKeyRing(t, "other-user", s.keyRing.ProxyHost), } // We should see two agent keys for each key added @@ -270,9 +269,9 @@ func TestLoadKey(t *testing.T) { require.NoError(t, err) // verify data signed by both the teleport agent and system agent was signed correctly - err = keyRing.PrivateKey.SSHPublicKey().Verify(userdata, teleportAgentSignature) + err = keyRing.SSHPrivateKey.SSHPublicKey().Verify(userdata, teleportAgentSignature) require.NoError(t, err) - err = keyRing.PrivateKey.SSHPublicKey().Verify(userdata, systemAgentSignature) + err = keyRing.SSHPrivateKey.SSHPublicKey().Verify(userdata, systemAgentSignature) require.NoError(t, err) } }) @@ -708,7 +707,7 @@ func TestLocalKeyAgent_AddDatabaseKey(t *testing.T) { addKey := *s.keyRing addKey.DBTLSCredentials = map[string]TLSCredential{ "some-db": TLSCredential{ - PrivateKey: addKey.PrivateKey, + PrivateKey: addKey.TLSPrivateKey, Cert: addKey.TLSCert, }, } @@ -721,8 +720,7 @@ func TestLocalKeyAgent_AddDatabaseKey(t *testing.T) { }) } -func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string, priv *keys.PrivateKey) *KeyRing { - keygen := testauthority.New() +func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string) *KeyRing { ttl := time.Minute clock := clockwork.NewRealClock() @@ -730,11 +728,16 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string Username: username, } + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(context.Background(), func(context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }) + require.NoError(t, err) + subject, err := identity.Subject() require.NoError(t, err) tlsCert, err := s.tlsca.GenerateCertificate(tlsca.CertificateRequest{ Clock: clock, - PublicKey: priv.Public(), + PublicKey: tlsKey.Public(), Subject: subject, NotAfter: clock.Now().UTC().Add(ttl), }) @@ -746,10 +749,12 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string caSigner, err := ssh.ParsePrivateKey(pemBytes) require.NoError(t, err) - certificate, err := keygen.GenerateUserCert(services.UserCertParams{ + sshPub, err := ssh.NewPublicKey(sshKey.Public()) + require.NoError(t, err) + certificate, err := testauthority.New().GenerateUserCert(services.UserCertParams{ CertificateFormat: constants.CertificateFormatStandard, CASigner: caSigner, - PublicUserKey: ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), + PublicUserKey: ssh.MarshalAuthorizedKey(sshPub), Username: username, AllowedLogins: []string{username}, TTL: ttl, @@ -759,10 +764,16 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string }) require.NoError(t, err) + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) + require.NoError(t, err) + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) + require.NoError(t, err) + return &KeyRing{ - PrivateKey: priv, - Cert: certificate, - TLSCert: tlsCert, + SSHPrivateKey: sshPriv, + TLSPrivateKey: tlsPriv, + Cert: certificate, + TLSCert: tlsCert, KeyRingIndex: KeyRingIndex{ ProxyHost: proxyHost, Username: username, @@ -771,12 +782,6 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string } } -func (s *KeyAgentTestSuite) genKeyRing(t *testing.T, username, proxyHost string) *KeyRing { - priv, err := native.GeneratePrivateKey() - require.NoError(t, err) - return s.makeKeyRing(t, username, proxyHost, priv) -} - func startDebugAgent(t *testing.T) error { // Create own tmp dir instead of using t.TmpDir // because net.Listen("unix", path) has dir path length limitation diff --git a/lib/client/keystore.go b/lib/client/keystore.go index ba1d1eaeda315..c84c0506d48cd 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -107,9 +107,14 @@ func NewFSKeyStore(dirPath string) *FSKeyStore { } } -// userKeyPath returns the private key path for the given KeyRingIndex. -func (fs *FSKeyStore) userKeyPath(idx KeyRingIndex) string { - return keypaths.UserKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username) +// userSSHKeyPath returns the SSH private key path for the given KeyRingIndex. +func (fs *FSKeyStore) userSSHKeyPath(idx KeyRingIndex) string { + return keypaths.UserSSHKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username) +} + +// userTLSKeyPath returns the TLS private key path for the given KeyRingIndex. +func (fs *FSKeyStore) userTLSKeyPath(idx KeyRingIndex) string { + return keypaths.UserTLSKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username) } // tlsCertPath returns the TLS certificate path given KeyRingIndex. @@ -177,20 +182,27 @@ func (fs *FSKeyStore) AddKeyRing(keyRing *KeyRing) error { // Store TLS key and cert. if err := fs.writeTLSCredential(TLSCredential{ - PrivateKey: keyRing.PrivateKey, + PrivateKey: keyRing.TLSPrivateKey, Cert: keyRing.TLSCert, - }, fs.userKeyPath(keyRing.KeyRingIndex), fs.tlsCertPath(keyRing.KeyRingIndex)); err != nil { + }, fs.userTLSKeyPath(keyRing.KeyRingIndex), fs.tlsCertPath(keyRing.KeyRingIndex)); err != nil { return trace.Wrap(err) } - // Store SSH public key (it currently matches the same private key as the TLS cert). - if err := fs.writeBytes(keyRing.PrivateKey.MarshalSSHPublicKey(), fs.publicKeyPath(keyRing.KeyRingIndex)); err != nil { + // Store SSH private and public key. + sshPrivateKeyPEM, err := keyRing.SSHPrivateKey.MarshalSSHPrivateKey() + if err != nil { + return trace.Wrap(err) + } + if err := fs.writeBytes(sshPrivateKeyPEM, fs.userSSHKeyPath(keyRing.KeyRingIndex)); err != nil { + return trace.Wrap(err) + } + if err := fs.writeBytes(keyRing.SSHPrivateKey.MarshalSSHPublicKey(), fs.publicKeyPath(keyRing.KeyRingIndex)); err != nil { return trace.Wrap(err) } // We only generate PPK files for use by PuTTY when running tsh on Windows. if runtime.GOOS == constants.WindowsOS { - ppkFile, err := keyRing.PrivateKey.PPKFile() + ppkFile, err := keyRing.SSHPrivateKey.PPKFile() // PPKFile can only be generated from an RSA private key. If the key is in a different // format, a BadParameter error is returned and we can skip PPK generation. if err != nil && !trace.IsBadParameter(err) { @@ -296,6 +308,10 @@ func readTLSCredentialFiles(keyPath, certPath string) ([]byte, []byte, error) { if err != nil { return nil, nil, trace.ConvertSystemError(err) } + if len(keyPEM) == 0 { + // Acquiring the read lock can end up creating an empty file. + return nil, nil, trace.NotFound("%s is empty", keyPath) + } certPEM, err := os.ReadFile(certPath) if err != nil { return nil, nil, trace.ConvertSystemError(err) @@ -389,10 +405,11 @@ func (fs *FSKeyStore) writeBytes(bytes []byte, fp string) error { return trace.ConvertSystemError(err) } -// DeleteKeyRing deletes the user's key with all its certs. +// DeleteKeyRing deletes all the user's keys and certs. func (fs *FSKeyStore) DeleteKeyRing(idx KeyRingIndex) error { files := []string{ - fs.userKeyPath(idx), + fs.userSSHKeyPath(idx), + fs.userTLSKeyPath(idx), fs.publicKeyPath(idx), fs.tlsCertPath(idx), } @@ -481,20 +498,19 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing return nil, trace.Wrap(err, "no session keys for %+v", idx) } - tlsCertFile := fs.tlsCertPath(idx) - tlsCert, err := os.ReadFile(tlsCertFile) + tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx)) if err != nil { - return nil, trace.ConvertSystemError(err) + return nil, trace.Wrap(err) } - priv, err := keys.LoadKeyPair(fs.userKeyPath(idx), fs.publicKeyPath(idx)) + sshPriv, err := keys.LoadKeyPair(fs.userSSHKeyPath(idx), fs.publicKeyPath(idx)) if err != nil { return nil, trace.ConvertSystemError(err) } - keyRing := NewKeyRing(priv) + keyRing := NewKeyRing(sshPriv, tlsCred.PrivateKey) keyRing.KeyRingIndex = idx - keyRing.TLSCert = tlsCert + keyRing.TLSCert = tlsCred.Cert for _, o := range opts { if err := fs.updateKeyRingWithCerts(o, keyRing); err != nil && !trace.IsNotFound(err) { @@ -773,7 +789,7 @@ func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRin return nil, trace.NotFound("key ring for %+v not found", idx) } - retKeyRing := NewKeyRing(keyRing.PrivateKey) + retKeyRing := NewKeyRing(keyRing.SSHPrivateKey, keyRing.TLSPrivateKey) retKeyRing.KeyRingIndex = idx retKeyRing.TLSCert = keyRing.TLSCert for _, o := range opts { diff --git a/lib/client/keystore_test.go b/lib/client/keystore_test.go index 092d9ff7afa7b..613f0a7481929 100644 --- a/lib/client/keystore_test.go +++ b/lib/client/keystore_test.go @@ -24,10 +24,13 @@ import ( "path/filepath" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/gravitational/trace" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/utils/cert" ) @@ -67,7 +70,7 @@ func TestKeyStore(t *testing.T) { retrievedKeyRing, err := keyStore.GetKeyRing(idx, WithAllCerts...) require.NoError(t, err) keyRing.TrustedCerts = nil - require.Equal(t, keyRing, retrievedKeyRing) + assertEqualKeyRings(t, keyRing, retrievedKeyRing) // Delete just the db cred, reload & verify it's gone err = keyStore.DeleteUserCerts(idx, WithDBCerts{}) @@ -76,14 +79,14 @@ func TestKeyStore(t *testing.T) { require.NoError(t, err) expectKeyRing := keyRing.Copy() expectKeyRing.DBTLSCredentials = make(map[string]TLSCredential) - require.Equal(t, expectKeyRing, retrievedKeyRing) + assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing) // check for the key, now without cluster name retrievedKeyRing, err = keyStore.GetKeyRing(KeyRingIndex{idx.ProxyHost, idx.Username, ""}) require.NoError(t, err) expectKeyRing.ClusterName = "" expectKeyRing.Cert = nil - require.Equal(t, expectKeyRing, retrievedKeyRing) + assertEqualKeyRings(t, expectKeyRing, retrievedKeyRing) // delete the key err = keyStore.DeleteKeyRing(idx) @@ -128,14 +131,16 @@ func TestListKeys(t *testing.T) { keyRing, err := keyStore.GetKeyRing(keys[i].KeyRingIndex, WithSSHCerts{}, WithDBCerts{}) require.NoError(t, err) keyRing.TrustedCerts = keys[i].TrustedCerts - require.Equal(t, &keys[i], keyRing) + assertEqualKeyRings(t, &keys[i], keyRing) } // read sam's key and make sure it's the same: skeyRing, err := keyStore.GetKeyRing(samIdx, WithSSHCerts{}) require.NoError(t, err) require.Equal(t, samKeyRing.Cert, skeyRing.Cert) - require.Equal(t, samKeyRing.PrivateKey.MarshalSSHPublicKey(), skeyRing.PrivateKey.MarshalSSHPublicKey()) + require.Equal(t, samKeyRing.TLSCert, skeyRing.TLSCert) + require.Equal(t, samKeyRing.SSHPrivateKey.MarshalSSHPublicKey(), skeyRing.SSHPrivateKey.MarshalSSHPublicKey()) + require.Equal(t, samKeyRing.TLSPrivateKey.MarshalSSHPublicKey(), skeyRing.TLSPrivateKey.MarshalSSHPublicKey()) }) } @@ -290,3 +295,10 @@ func TestConfigDirNotDeleted(t *testing.T) { require.NoDirExists(t, filepath.Join(keyStore.KeyDir, "keys")) } + +func assertEqualKeyRings(t *testing.T, expected, actual *KeyRing) { + t.Helper() + // Ignore differences in unexported private key fields, for example keyPEM + // may change after being serialized in OpenSSH format and then deserialized. + require.Empty(t, cmp.Diff(expected, actual, cmpopts.IgnoreUnexported(keys.PrivateKey{}))) +} diff --git a/lib/client/profile.go b/lib/client/profile.go index 08618f0ebbb35..e335cd62e5137 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -429,16 +429,28 @@ func (p *ProfileStatus) CACertPathForCluster(cluster string) string { return filepath.Join(keypaths.ProxyKeyDir(p.Dir, p.Name), "cas", cluster+".pem") } -// KeyPath returns path to the private key for this profile. +// SSHKeyPath returns path to the SSH private key for this profile. // // It's kept in /keys//. -func (p *ProfileStatus) KeyPath() string { +func (p *ProfileStatus) SSHKeyPath() string { // Return an env var override if both valid and present for this identity. if path, ok := p.virtualPathFromEnv(VirtualPathKey, nil); ok { return path } - return keypaths.UserKeyPath(p.Dir, p.Name, p.Username) + return keypaths.UserSSHKeyPath(p.Dir, p.Name, p.Username) +} + +// TLSKeyPath returns path to the TLS private key for this profile. +// +// It's kept in /keys//. +func (p *ProfileStatus) TLSKeyPath() string { + // Return an env var override if both valid and present for this identity. + if path, ok := p.virtualPathFromEnv(VirtualPathKey, nil); ok { + return path + } + + return keypaths.UserTLSKeyPath(p.Dir, p.Name, p.Username) } // DatabaseCertPathForCluster returns path to the specified database access @@ -498,7 +510,7 @@ func (p *ProfileStatus) OracleWalletDir(clusterName string, databaseName string) return keypaths.DatabaseOracleWalletDirectory(p.Dir, p.Name, p.Username, clusterName, databaseName) } -// DatabaseLocalCAPath returns the specified db 's self-signed localhost CA path for +// DatabaseLocalCAPath returns the specified db's self-signed localhost CA path for // this profile. // // It's kept in /keys//-db/proxy-localca.pem diff --git a/lib/client/redirect.go b/lib/client/redirect.go index db7f08896d248..c1b573e527f4b 100644 --- a/lib/client/redirect.go +++ b/lib/client/redirect.go @@ -214,9 +214,10 @@ func (rd *Redirector) Start() error { req := SSOLoginConsoleReq{ RedirectURL: u.String(), SSOUserPublicKeys: SSOUserPublicKeys{ - // TODO(nklaassen): split keys on client side. - PublicKey: rd.PubKey, - AttestationStatement: rd.AttestationStatement, + SSHPubKey: rd.SSHPubKey, + TLSPubKey: rd.TLSPubKey, + SSHAttestationStatement: rd.SSHAttestationStatement, + TLSAttestationStatement: rd.TLSAttestationStatement, }, CertTTL: rd.TTL, ConnectorID: rd.ConnectorID, diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index b7f5bdbf3097e..121dfae5baa39 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -333,8 +333,10 @@ type HeadlessRequest struct { type SSHLogin struct { // ProxyAddr is the target proxy address ProxyAddr string - // PubKey is SSH public key to sign - PubKey []byte + // SSHPubKey is SSH public key to sign + SSHPubKey []byte + // TLSPubKey is TLS public key to sign + TLSPubKey []byte // TTL is requested TTL of the client certificates TTL time.Duration // Insecure turns off verification for x509 target proxy @@ -349,8 +351,10 @@ type SSHLogin struct { // KubernetesCluster is an optional k8s cluster name to route the response // credentials to. KubernetesCluster string - // AttestationStatement is an attestation statement. - AttestationStatement *keys.AttestationStatement + // SSHAttestationStatement is an attestation statement for SSHPubKey. + SSHAttestationStatement *keys.AttestationStatement + // TLSAttestationStatement is an attestation statement for TLSPubKey. + TLSAttestationStatement *keys.AttestationStatement // ExtraHeaders is a map of extra HTTP headers to be included in requests. ExtraHeaders map[string]string } @@ -575,8 +579,10 @@ func SSHAgentLogin(ctx context.Context, login SSHLoginDirect) (*authclient.SSHLo Password: login.Password, OTPToken: login.OTPToken, UserPublicKeys: UserPublicKeys{ - PubKey: login.PubKey, - AttestationStatement: login.AttestationStatement, + SSHPubKey: login.SSHPubKey, + TLSPubKey: login.TLSPubKey, + SSHAttestationStatement: login.SSHAttestationStatement, + TLSAttestationStatement: login.TLSAttestationStatement, }, TTL: login.TTL, Compatibility: login.Compatibility, @@ -644,8 +650,10 @@ func SSHAgentHeadlessLogin(ctx context.Context, login SSHLoginHeadless) (*authcl User: login.User, HeadlessAuthenticationID: login.HeadlessAuthenticationID, UserPublicKeys: UserPublicKeys{ - PubKey: login.PubKey, - AttestationStatement: login.AttestationStatement, + SSHPubKey: login.SSHPubKey, + TLSPubKey: login.TLSPubKey, + SSHAttestationStatement: login.SSHAttestationStatement, + TLSAttestationStatement: login.TLSAttestationStatement, }, TTL: login.TTL, Compatibility: login.Compatibility, @@ -725,8 +733,10 @@ func SSHAgentPasswordlessLogin(ctx context.Context, login SSHLoginPasswordless) User: "", // User carried on WebAuthn assertion. WebauthnChallengeResponse: wantypes.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn()), UserPublicKeys: UserPublicKeys{ - PubKey: login.PubKey, - AttestationStatement: login.AttestationStatement, + SSHPubKey: login.SSHPubKey, + TLSPubKey: login.TLSPubKey, + SSHAttestationStatement: login.SSHAttestationStatement, + TLSAttestationStatement: login.TLSAttestationStatement, }, TTL: login.TTL, Compatibility: login.Compatibility, @@ -791,8 +801,10 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*authclient.SSHLo User: login.User, Password: login.Password, UserPublicKeys: UserPublicKeys{ - PubKey: login.PubKey, - AttestationStatement: login.AttestationStatement, + SSHPubKey: login.SSHPubKey, + TLSPubKey: login.TLSPubKey, + SSHAttestationStatement: login.SSHAttestationStatement, + TLSAttestationStatement: login.TLSAttestationStatement, }, TTL: login.TTL, Compatibility: login.Compatibility, diff --git a/lib/client/weblogin_test.go b/lib/client/weblogin_test.go index 242cbfe43d11c..39722c78c2efe 100644 --- a/lib/client/weblogin_test.go +++ b/lib/client/weblogin_test.go @@ -30,12 +30,15 @@ import ( "github.com/jonboulle/clockwork" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/cryptosuites" ) // TestHostCredentialsHttpFallback tests that HostCredentials requests (/v1/webapi/host/credentials/) @@ -153,7 +156,12 @@ func TestSSHAgentPasswordlessLogin(t *testing.T) { tc, err := client.NewClient(cfg) require.NoError(t, err) - keyRing, err := client.GenerateRSAKeyRing() + + userKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + require.NoError(t, err) + sshPub, err := ssh.NewPublicKey(userKey.Public()) + require.NoError(t, err) + tlsPub, err := keys.MarshalPublicKey(userKey.Public()) require.NoError(t, err) // customPromptCalled is a flag to ensure the custom prompt was indeed called @@ -213,7 +221,8 @@ func TestSSHAgentPasswordlessLogin(t *testing.T) { req := client.SSHLoginPasswordless{ SSHLogin: client.SSHLogin{ ProxyAddr: tc.WebProxyAddr, - PubKey: keyRing.PrivateKey.MarshalSSHPublicKey(), + SSHPubKey: ssh.MarshalAuthorizedKey(sshPub), + TLSPubKey: tlsPub, TTL: tc.KeyTTL, Insecure: tc.InsecureSkipVerify, Compatibility: tc.CertificateFormat, diff --git a/lib/kube/kubeconfig/kubeconfig.go b/lib/kube/kubeconfig/kubeconfig.go index 5093e99281f46..30e587da834d1 100644 --- a/lib/kube/kubeconfig/kubeconfig.go +++ b/lib/kube/kubeconfig/kubeconfig.go @@ -252,7 +252,7 @@ func UpdateConfig(path string, v Values, storeAllCAs bool, fs ConfigFS) error { // TODO (Joerger): Create a custom k8s Auth Provider or Exec Provider to // use hardware private keys for kube credentials (if possible) - keyPEM, err := v.Credentials.PrivateKey.SoftwarePrivateKeyPEM() + keyPEM, err := v.Credentials.TLSPrivateKey.SoftwarePrivateKeyPEM() if err == nil { if len(v.Credentials.TLSCert) == 0 { return trace.BadParameter("TLS certificate missing in provided credentials") diff --git a/lib/kube/kubeconfig/kubeconfig_test.go b/lib/kube/kubeconfig/kubeconfig_test.go index d9731d4074b00..0ee118d4ad090 100644 --- a/lib/kube/kubeconfig/kubeconfig_test.go +++ b/lib/kube/kubeconfig/kubeconfig_test.go @@ -33,9 +33,10 @@ import ( "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/authclient" - "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/tlsca" ) @@ -197,7 +198,7 @@ func TestUpdate(t *testing.T) { } wantConfig.AuthInfos[clusterName] = &clientcmdapi.AuthInfo{ ClientCertificateData: creds.TLSCert, - ClientKeyData: creds.PrivateKey.PrivateKeyPEM(), + ClientKeyData: creds.TLSPrivateKey.PrivateKeyPEM(), LocationOfOrigin: kubeconfigPath, Extensions: map[string]runtime.Object{}, } @@ -564,8 +565,11 @@ func genUserKeyRing(hostname string) (*client.KeyRing, []byte, error) { return nil, nil, trace.Wrap(err) } - keygen := testauthority.New() - priv, err := keygen.GeneratePrivateKey() + key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + if err != nil { + return nil, nil, trace.Wrap(err) + } + priv, err := keys.NewSoftwarePrivateKey(key) if err != nil { return nil, nil, trace.Wrap(err) } @@ -584,8 +588,8 @@ func genUserKeyRing(hostname string) (*client.KeyRing, []byte, error) { } return &client.KeyRing{ - PrivateKey: priv, - TLSCert: tlsCert, + TLSPrivateKey: priv, + TLSCert: tlsCert, TrustedCerts: []authclient.TrustedCerts{{ TLSCertificates: [][]byte{caCert}, }}, diff --git a/lib/tbot/identity/identity.go b/lib/tbot/identity/identity.go index bfcea0c13b502..0e441b4414fd1 100644 --- a/lib/tbot/identity/identity.go +++ b/lib/tbot/identity/identity.go @@ -69,13 +69,12 @@ var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentTBo // Identity is collection of raw key and certificate data as well as the // parsed equivalents that make up a Teleport identity. -// TODO(nklaassen): split SSH and TLS keys. type Identity struct { // PrivateKeyBytes is a PEM encoded private key PrivateKeyBytes []byte // PublicKeyBytes contains bytes of the original SSH public key PublicKeyBytes []byte - // CertBytes is a PEM encoded SSH host cert + // CertBytes is a PEM encoded SSH user cert CertBytes []byte // TLSCertBytes is a PEM encoded TLS x509 client certificate TLSCertBytes []byte diff --git a/lib/tbot/output_utils.go b/lib/tbot/output_utils.go index 52f58526d2b1b..9aa6f8c3d438d 100644 --- a/lib/tbot/output_utils.go +++ b/lib/tbot/output_utils.go @@ -114,10 +114,12 @@ func NewClientKeyRing(ident *identity.Identity, hostCAs []types.CertAuthority) ( KeyRingIndex: client.KeyRingIndex{ ClusterName: ident.ClusterName, }, - PrivateKey: pk, - Cert: ident.CertBytes, - TLSCert: ident.TLSCertBytes, - TrustedCerts: authclient.AuthoritiesToTrustedCerts(hostCAs), + // tbot identities use a single private key for SSH and TLS. + SSHPrivateKey: pk, + TLSPrivateKey: pk, + Cert: ident.CertBytes, + TLSCert: ident.TLSCertBytes, + TrustedCerts: authclient.AuthoritiesToTrustedCerts(hostCAs), // Note: these fields are never used or persisted with identity files, // so we won't bother to set them. (They may need to be reconstituted @@ -233,8 +235,8 @@ func writeTLSCAs(ctx context.Context, dest bot.Destination, hostCAs, userCAs, da // generateKeys generates TLS and SSH keypairs. func generateKeys() (private, sshpub, tlspub []byte, err error) { - // TODO(nklaassen): split SSH and TLS keys, support configurable key - // algorithms. + // TODO(nklaassen): consider splitting SSH and TLS keys, support + // configurable key algorithms. privateKey, publicKey, err := native.GenerateKeyPair() if err != nil { return nil, nil, nil, trace.Wrap(err) @@ -325,7 +327,8 @@ func generateIdentity( // Generate a fresh keypair for the impersonated identity. We don't care to // reuse keys here: impersonated certs might not be as well-protected so // constantly rotating private keys - // TODO(nklaassen): split SSH and TLS keys, support configurable algorithms. + // TODO(nklaassen): consider splitting SSH and TLS keys, support + // configurable algorithms. key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.RSA2048) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/tbot/service_bot_identity.go b/lib/tbot/service_bot_identity.go index 65947057a0cbc..26a4305cce918 100644 --- a/lib/tbot/service_bot_identity.go +++ b/lib/tbot/service_bot_identity.go @@ -364,7 +364,7 @@ func botIdentityFromAuth( return nil, trace.BadParameter("renewIdentityWithAuth must be called with non-nil client and identity") } - // TODO(nklaassen): split SSH and TLS keys in identity. + // TODO(nklaassen): consider splitting SSH and TLS keys in identity. sshPublicKey := ident.PublicKeyBytes cryptoPubKey, err := sshutils.CryptoPublicKey(sshPublicKey) if err != nil { diff --git a/lib/tbot/service_kubernetes_output.go b/lib/tbot/service_kubernetes_output.go index c7053bf674023..d477891344b9f 100644 --- a/lib/tbot/service_kubernetes_output.go +++ b/lib/tbot/service_kubernetes_output.go @@ -313,7 +313,7 @@ func generateKubeConfigWithoutPlugin(ks *kubernetesStatus) (*clientcmdapi.Config config.AuthInfos[contextName] = &clientcmdapi.AuthInfo{ ClientCertificateData: ks.credentials.TLSCert, - ClientKeyData: ks.credentials.PrivateKey.PrivateKeyPEM(), + ClientKeyData: ks.credentials.TLSPrivateKey.PrivateKeyPEM(), } // Last, create a context linking the cluster to the auth info. diff --git a/lib/tbot/service_ssh_host_output.go b/lib/tbot/service_ssh_host_output.go index 36c26bd2774e5..6cc18b9e88311 100644 --- a/lib/tbot/service_ssh_host_output.go +++ b/lib/tbot/service_ssh_host_output.go @@ -122,6 +122,7 @@ func (s *SSHHostOutputService) generate(ctx context.Context) error { clusterName := facade.Get().ClusterName // generate a keypair + // TODO(nklaassen): get away from RSA here. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) @@ -129,7 +130,7 @@ func (s *SSHHostOutputService) generate(ctx context.Context) error { // For now, we'll reuse the bot's regular TTL, and hostID and nodeName are // left unset. res, err := impersonatedClient.TrustClient().GenerateHostCert(ctx, &trustpb.GenerateHostCertRequest{ - Key: keyRing.PrivateKey.MarshalSSHPublicKey(), + Key: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), HostId: "", NodeName: "", Principals: s.cfg.Principals, diff --git a/lib/tbot/ssh_proxy.go b/lib/tbot/ssh_proxy.go index 96e93fcdabc1f..6769fd83103ae 100644 --- a/lib/tbot/ssh_proxy.go +++ b/lib/tbot/ssh_proxy.go @@ -262,8 +262,8 @@ func parseIdentity(destPath, proxy, cluster string, insecure, fips bool) (*ident } i, err := identity.ReadIdentityFromStore(&identity.LoadIdentityParams{ - PrivateKeyBytes: keyRing.PrivateKey.PrivateKeyPEM(), - PublicKeyBytes: keyRing.PrivateKey.MarshalSSHPublicKey(), + PrivateKeyBytes: keyRing.SSHPrivateKey.PrivateKeyPEM(), + PublicKeyBytes: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), }, &proto.Certs{ SSH: keyRing.Cert, TLS: keyRing.TLSCert, diff --git a/lib/teleterm/clusters/cluster_auth.go b/lib/teleterm/clusters/cluster_auth.go index 16793f715a4e4..12ee9e08a69a4 100644 --- a/lib/teleterm/clusters/cluster_auth.go +++ b/lib/teleterm/clusters/cluster_auth.go @@ -222,11 +222,16 @@ func (c *Cluster) login(ctx context.Context, sshLoginFunc client.SSHLoginFunc) e } func (c *Cluster) localMFALogin(user, password string) client.SSHLoginFunc { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } response, err := client.SSHAgentMFALogin(ctx, client.SSHLoginMFA{ SSHLogin: client.SSHLogin{ ProxyAddr: c.clusterClient.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, TTL: c.clusterClient.KeyTTL, Insecure: c.clusterClient.InsecureSkipVerify, Compatibility: c.clusterClient.CertificateFormat, @@ -245,11 +250,16 @@ func (c *Cluster) localMFALogin(user, password string) client.SSHLoginFunc { } func (c *Cluster) localLogin(user, password, otpToken string) client.SSHLoginFunc { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } response, err := client.SSHAgentLogin(ctx, client.SSHLoginDirect{ SSHLogin: client.SSHLogin{ ProxyAddr: c.clusterClient.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, TTL: c.clusterClient.KeyTTL, Insecure: c.clusterClient.InsecureSkipVerify, Compatibility: c.clusterClient.CertificateFormat, @@ -267,11 +277,16 @@ func (c *Cluster) localLogin(user, password, otpToken string) client.SSHLoginFun } func (c *Cluster) ssoLogin(providerType, providerName string) client.SSHLoginFunc { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } response, err := client.SSHAgentSSOLogin(ctx, client.SSHLoginSSO{ SSHLogin: client.SSHLogin{ ProxyAddr: c.clusterClient.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, TTL: c.clusterClient.KeyTTL, Insecure: c.clusterClient.InsecureSkipVerify, Compatibility: c.clusterClient.CertificateFormat, @@ -290,11 +305,16 @@ func (c *Cluster) ssoLogin(providerType, providerName string) client.SSHLoginFun } func (c *Cluster) passwordlessLogin(stream api.TerminalService_LoginPasswordlessServer) client.SSHLoginFunc { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } response, err := client.SSHAgentPasswordlessLogin(ctx, client.SSHLoginPasswordless{ SSHLogin: client.SSHLogin{ ProxyAddr: c.clusterClient.WebProxyAddr, - PubKey: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, TTL: c.clusterClient.KeyTTL, Insecure: c.clusterClient.InsecureSkipVerify, Compatibility: c.clusterClient.CertificateFormat, diff --git a/lib/utils/cert/selfsigned.go b/lib/utils/cert/selfsigned.go index be1678c709884..cde88823f1af0 100644 --- a/lib/utils/cert/selfsigned.go +++ b/lib/utils/cert/selfsigned.go @@ -28,9 +28,9 @@ import ( "time" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" - "github.com/gravitational/teleport/lib/auth/native" + "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/cryptosuites" ) // macMaxTLSCertValidityPeriod is the maximum validity period @@ -59,7 +59,7 @@ func GenerateSelfSignedCert(hostNames []string, ipAddresses []string, eku ...x50 eku = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} } - priv, err := native.GenerateRSAPrivateKey() + priv, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) if err != nil { return nil, trace.Wrap(err) } @@ -104,20 +104,23 @@ func GenerateSelfSignedCert(hostNames []string, ipAddresses []string, eku ...x50 template.IPAddresses = append(template.IPAddresses, ipParsed) } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv) if err != nil { return nil, trace.Wrap(err) } - publicKeyBytes, err := x509.MarshalPKIXPublicKey(priv.Public()) + privateKeyBytes, err := keys.MarshalPrivateKey(priv) + if err != nil { + return nil, trace.Wrap(err) + } + publicKeyBytes, err := keys.MarshalPublicKey(priv.Public()) if err != nil { - logrus.Error(err) return nil, trace.Wrap(err) } return &Credentials{ - PublicKey: pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: publicKeyBytes}), - PrivateKey: pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}), + PrivateKey: privateKeyBytes, + PublicKey: publicKeyBytes, Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), }, nil } diff --git a/lib/utils/utils_test.go b/lib/utils/utils_test.go index 2073398c4c827..1ff85e1ff8d31 100644 --- a/lib/utils/utils_test.go +++ b/lib/utils/utils_test.go @@ -32,6 +32,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/fixtures" "github.com/gravitational/teleport/lib/utils/cert" ) @@ -94,9 +95,11 @@ func TestSelfSignedCert(t *testing.T) { creds, err := cert.GenerateSelfSignedCert([]string{"example.com"}, nil) require.NoError(t, err) - require.NotNil(t, creds) - require.Equal(t, 4, len(creds.PublicKey)/100) - require.Equal(t, 16, len(creds.PrivateKey)/100) + signer, err := keys.ParsePrivateKey(creds.PrivateKey) + require.NoError(t, err) + pub, err := keys.ParsePublicKey(creds.PublicKey) + require.NoError(t, err) + require.Equal(t, signer.Public(), pub) } func TestRandomDuration(t *testing.T) { diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 95e22bba713e5..b1645df6240b9 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -1222,15 +1222,16 @@ func (h *Handler) AccessGraphAddr() utils.NetAddr { func localSettings(cap types.AuthPreference) (webclient.AuthenticationSettings, error) { as := webclient.AuthenticationSettings{ - Type: constants.Local, - SecondFactor: cap.GetSecondFactor(), - PreferredLocalMFA: cap.GetPreferredLocalMFA(), - AllowPasswordless: cap.GetAllowPasswordless(), - AllowHeadless: cap.GetAllowHeadless(), - Local: &webclient.LocalSettings{}, - PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), - PIVSlot: cap.GetPIVSlot(), - DeviceTrust: deviceTrustSettings(cap), + Type: constants.Local, + SecondFactor: cap.GetSecondFactor(), + PreferredLocalMFA: cap.GetPreferredLocalMFA(), + AllowPasswordless: cap.GetAllowPasswordless(), + AllowHeadless: cap.GetAllowHeadless(), + Local: &webclient.LocalSettings{}, + PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + PIVSlot: cap.GetPIVSlot(), + DeviceTrust: deviceTrustSettings(cap), + SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(), } // Only copy the connector name if it's truly local and not a local fallback. @@ -1267,11 +1268,12 @@ func oidcSettings(connector types.OIDCConnector, cap types.AuthPreference) webcl Display: connector.GetDisplay(), }, // Local fallback / MFA. - SecondFactor: cap.GetSecondFactor(), - PreferredLocalMFA: cap.GetPreferredLocalMFA(), - PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), - PIVSlot: cap.GetPIVSlot(), - DeviceTrust: deviceTrustSettings(cap), + SecondFactor: cap.GetSecondFactor(), + PreferredLocalMFA: cap.GetPreferredLocalMFA(), + PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + PIVSlot: cap.GetPIVSlot(), + DeviceTrust: deviceTrustSettings(cap), + SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(), } } @@ -1284,11 +1286,12 @@ func samlSettings(connector types.SAMLConnector, cap types.AuthPreference) webcl SingleLogoutEnabled: connector.GetSingleLogoutURL() != "", }, // Local fallback / MFA. - SecondFactor: cap.GetSecondFactor(), - PreferredLocalMFA: cap.GetPreferredLocalMFA(), - PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), - PIVSlot: cap.GetPIVSlot(), - DeviceTrust: deviceTrustSettings(cap), + SecondFactor: cap.GetSecondFactor(), + PreferredLocalMFA: cap.GetPreferredLocalMFA(), + PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + PIVSlot: cap.GetPIVSlot(), + DeviceTrust: deviceTrustSettings(cap), + SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(), } } @@ -1300,11 +1303,12 @@ func githubSettings(connector types.GithubConnector, cap types.AuthPreference) w Display: connector.GetDisplay(), }, // Local fallback / MFA. - SecondFactor: cap.GetSecondFactor(), - PreferredLocalMFA: cap.GetPreferredLocalMFA(), - PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), - PIVSlot: cap.GetPIVSlot(), - DeviceTrust: deviceTrustSettings(cap), + SecondFactor: cap.GetSecondFactor(), + PreferredLocalMFA: cap.GetPreferredLocalMFA(), + PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + PIVSlot: cap.GetPIVSlot(), + DeviceTrust: deviceTrustSettings(cap), + SignatureAlgorithmSuite: cap.GetSignatureAlgorithmSuite(), } } @@ -1402,7 +1406,6 @@ func getAuthSettings(ctx context.Context, authClient authclient.ClientI) (webcli } as.LoadAllCAs = pingResp.LoadAllCAs as.DefaultSessionTTL = authPreference.GetDefaultSessionTTL() - as.SignatureAlgorithmSuite = authPreference.GetSignatureAlgorithmSuite() return as, nil } diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index 597bdd9c5e282..503301638461c 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -379,6 +379,7 @@ func (a *AuthCommand) generateWindowsCert(ctx context.Context, clusterAPI certif // generateSnowflakeKey exports DatabaseCA public key in the format required by Snowflake // Ref: https://docs.snowflake.com/en/user-guide/key-pair-auth.html#step-2-generate-a-public-key func (a *AuthCommand) generateSnowflakeKey(ctx context.Context, clusterAPI certificateSigner) error { + // TODO(nklaassen): don't hardcode RSA here. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) @@ -491,6 +492,7 @@ func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI certifica principals := strings.Split(a.genHost, ",") // generate a keypair + // TODO(nklaassen): get away from RSA here. Only need an SSH key. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) @@ -503,7 +505,7 @@ func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI certifica clusterName := cn.GetClusterName() res, err := clusterAPI.TrustClient().GenerateHostCert(ctx, &trustpb.GenerateHostCertRequest{ - Key: keyRing.PrivateKey.MarshalSSHPublicKey(), + Key: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), HostId: "", NodeName: "", Principals: principals, @@ -547,6 +549,7 @@ func (a *AuthCommand) generateHostKeys(ctx context.Context, clusterAPI certifica // generateDatabaseKeys generates a new unsigned key and signs it with Teleport // CA for database access. func (a *AuthCommand) generateDatabaseKeys(ctx context.Context, clusterAPI certificateSigner) error { + // TODO(nklaasen): don't hardcode RSA. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) @@ -847,7 +850,8 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI certifica } // Generate a keypair. - // TODO(nklaassen): support configurable key algorithms, split SSH and TLS keys. + // TODO(nklaassen): get away from RSA here, but identity files only support + // a single private key. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) @@ -917,8 +921,8 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI certifica certUsage = proto.UserCertsRequest_Database } - sshPublicKey := keyRing.PrivateKey.MarshalSSHPublicKey() - tlsPublicKey, err := keys.MarshalPublicKey(keyRing.PrivateKey.Public()) + sshPublicKey := keyRing.SSHPrivateKey.MarshalSSHPublicKey() + tlsPublicKey, err := keys.MarshalPublicKey(keyRing.TLSPrivateKey.Public()) if err != nil { return trace.Wrap(err) } @@ -1163,6 +1167,7 @@ func (a *AuthCommand) checkProxyAddr(ctx context.Context, clusterAPI certificate } func (a *AuthCommand) generateDBOracleCert(ctx context.Context, api certificateSigner) error { + // TODO(nklaassen): don't hardcode RSA. keyRing, err := client.GenerateRSAKeyRing() if err != nil { return trace.Wrap(err) diff --git a/tool/tctl/common/auth_command_test.go b/tool/tctl/common/auth_command_test.go index a611a835bacee..1f9b307412841 100644 --- a/tool/tctl/common/auth_command_test.go +++ b/tool/tctl/common/auth_command_test.go @@ -603,6 +603,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { cas: []types.CertAuthority{dbCA}, } + // TODO(nklaassen): don't hardcode RSA. keyRing, err := client.GenerateRSAKeyRing() require.NoError(t, err) @@ -627,7 +628,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { outSubject: pkix.Name{CommonName: "postgres.example.com"}, outServerNames: []string{"postgres.example.com"}, wantFiles: map[string][]byte{ - "db.key": keyRing.PrivateKey.PrivateKeyPEM(), + "db.key": keyRing.TLSPrivateKey.PrivateKeyPEM(), "db.crt": certBytes, "db.cas": dbClientCABytes, }, @@ -641,7 +642,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { outSubject: pkix.Name{CommonName: "mysql.external.net"}, outServerNames: []string{"mysql.external.net", "mysql.internal.net", "192.168.1.1"}, wantFiles: map[string][]byte{ - "db.key": keyRing.PrivateKey.PrivateKeyPEM(), + "db.key": keyRing.TLSPrivateKey.PrivateKeyPEM(), "db.crt": certBytes, "db.cas": dbClientCABytes, }, @@ -655,7 +656,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { outSubject: pkix.Name{CommonName: "mongo.example.com", Organization: []string{"example.com"}}, outServerNames: []string{"mongo.example.com"}, wantFiles: map[string][]byte{ - "mongo.crt": append(certBytes, keyRing.PrivateKey.PrivateKeyPEM()...), + "mongo.crt": append(certBytes, keyRing.TLSPrivateKey.PrivateKeyPEM()...), "mongo.cas": dbClientCABytes, }, }, @@ -667,7 +668,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { outSubject: pkix.Name{CommonName: "node"}, outServerNames: []string{"node", "localhost", "roach1"}, // "node" principal should always be added wantFiles: map[string][]byte{ - "node.key": keyRing.PrivateKey.PrivateKeyPEM(), + "node.key": keyRing.TLSPrivateKey.PrivateKeyPEM(), "node.crt": certBytes, "ca.crt": dbServerCABytes, "ca-client.crt": dbClientCABytes, @@ -682,7 +683,7 @@ func TestGenerateDatabaseKeys(t *testing.T) { outSubject: pkix.Name{CommonName: "localhost"}, outServerNames: []string{"localhost", "redis1", "172.0.0.1"}, wantFiles: map[string][]byte{ - "db.key": keyRing.PrivateKey.PrivateKeyPEM(), + "db.key": keyRing.TLSPrivateKey.PrivateKeyPEM(), "db.crt": certBytes, "db.cas": dbClientCABytes, }, diff --git a/tool/tctl/sso/tester/command.go b/tool/tctl/sso/tester/command.go index 70a82bf3c6081..d2f18359af4c0 100644 --- a/tool/tctl/sso/tester/command.go +++ b/tool/tctl/sso/tester/command.go @@ -32,6 +32,7 @@ import ( kyaml "k8s.io/apimachinery/pkg/util/yaml" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/cryptosuites" @@ -173,12 +174,15 @@ type AuthRequestInfo struct { } func (cmd *SSOTestCommand) runSSOLoginFlow(ctx context.Context, protocol string, c *authclient.Client, config *client.RedirectorConfig) (*authclient.SSHLoginResponse, error) { - // TODO(nklaassen): support configurable key algorithms. - key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.RSA2048) + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(ctx, cryptosuites.GetCurrentSuiteFromAuthPreference(c)) if err != nil { return nil, trace.Wrap(err) } - sshPub, err := ssh.NewPublicKey(key.Public()) + sshPub, err := ssh.NewPublicKey(sshKey.Public()) + if err != nil { + return nil, trace.Wrap(err) + } + tlsPub, err := keys.MarshalPublicKey(tlsKey.Public()) if err != nil { return nil, trace.Wrap(err) } @@ -204,7 +208,8 @@ func (cmd *SSOTestCommand) runSSOLoginFlow(ctx context.Context, protocol string, return client.SSHAgentSSOLogin(ctx, client.SSHLoginSSO{ SSHLogin: client.SSHLogin{ ProxyAddr: tc.WebProxyAddr, - PubKey: ssh.MarshalAuthorizedKey(sshPub), + SSHPubKey: ssh.MarshalAuthorizedKey(sshPub), + TLSPubKey: tlsPub, TTL: tc.KeyTTL, Insecure: tc.InsecureSkipVerify, Pool: nil, diff --git a/tool/teleport/testenv/test_server.go b/tool/teleport/testenv/test_server.go index 151e3bba113fa..090adcd8cbb04 100644 --- a/tool/teleport/testenv/test_server.go +++ b/tool/teleport/testenv/test_server.go @@ -166,6 +166,7 @@ func MakeTestServer(t *testing.T, opts ...TestServerOptFunc) (process *service.T }) require.NoError(t, err) cfg.Auth.StaticTokens = staticToken + cfg.Auth.Preference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1) // Disable session recording to prevent writing to disk after the test concludes. cfg.Auth.SessionRecordingConfig.SetMode(types.RecordOff) diff --git a/tool/tsh/common/app_azure.go b/tool/tsh/common/app_azure.go index 18b49c0396872..1bd32ed26f704 100644 --- a/tool/tsh/common/app_azure.go +++ b/tool/tsh/common/app_azure.go @@ -91,7 +91,7 @@ func newAzureApp(tc *client.TeleportClient, cf *CLIConf, appInfo *appInfo) (*azu return &azureApp{ localProxyApp: newLocalProxyApp(tc, appInfo, cf.LocalProxyPort, cf.InsecureSkipVerify), cf: cf, - signer: keyRing.PrivateKey, + signer: keyRing.TLSPrivateKey, msiSecret: msiSecret, }, nil } diff --git a/tool/tsh/common/config.go b/tool/tsh/common/config.go index 817d01d9c09c0..6f87a3e89a752 100644 --- a/tool/tsh/common/config.go +++ b/tool/tsh/common/config.go @@ -77,7 +77,7 @@ func onConfig(cf *CLIConf) error { keysDir := profile.FullProfilePath(tc.Config.KeysDir) knownHostsPath := keypaths.KnownHostsPath(keysDir) - identityFilePath := keypaths.UserKeyPath(keysDir, proxyHost, tc.Config.Username) + identityFilePath := keypaths.UserSSHKeyPath(keysDir, proxyHost, tc.Config.Username) leafClustersNames := make([]string, 0, len(leafClusters)) for _, leafCluster := range leafClusters { diff --git a/tool/tsh/common/db.go b/tool/tsh/common/db.go index 8e3e73ff87a86..cf107f20dc8c2 100644 --- a/tool/tsh/common/db.go +++ b/tool/tsh/common/db.go @@ -332,10 +332,10 @@ func databaseLogin(cf *CLIConf, tc *client.TeleportClient, dbInfo *databaseInfo) } if dbInfo.Protocol == defaults.ProtocolOracle { - if err := generateDBLocalProxyCert(keyRing, profile); err != nil { + if err := generateDBLocalProxyCert(keyRing.TLSPrivateKey, profile); err != nil { return trace.Wrap(err) } - err = oracle.GenerateClientConfiguration(keyRing, dbInfo.RouteToDatabase, profile) + err = oracle.GenerateClientConfiguration(keyRing.TLSPrivateKey, dbInfo.RouteToDatabase, profile) if err != nil { return trace.Wrap(err) } @@ -681,7 +681,7 @@ func createLocalProxyListener(addr string, route tlsca.RouteToDatabase, profile if route.Protocol == defaults.ProtocolOracle { localCert, err := tls.LoadX509KeyPair( profile.DatabaseLocalCAPath(), - profile.KeyPath(), + profile.TLSKeyPath(), ) if err != nil { return nil, trace.Wrap(err) diff --git a/tool/tsh/common/hardware_key_test.go b/tool/tsh/common/hardware_key_test.go index a4c6210ad3393..8b83f293b94cd 100644 --- a/tool/tsh/common/hardware_key_test.go +++ b/tool/tsh/common/hardware_key_test.go @@ -40,6 +40,7 @@ import ( "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/mocku2f" + "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/modules" "github.com/gravitational/teleport/lib/service/servicecfg" testserver "github.com/gravitational/teleport/tool/teleport/testenv" @@ -77,29 +78,28 @@ func TestHardwareKeyLogin(t *testing.T) { // mock SSO login and count the number of login attempts. var lastLoginCount int mockSSOLogin := mockSSOLogin(authServer, alice) - mockSSOLoginWithCountAndAttestation := func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) { + mockSSOLoginWithCountAndAttestation := func(ctx context.Context, connectorID string, keyRing *client.KeyRing, protocol string) (*authclient.SSHLoginResponse, error) { lastLoginCount++ // Set MockAttestationData to attest the expected key policy and reset it after login. testModules.MockAttestationData = &keys.AttestationData{ - PrivateKeyPolicy: priv.GetPrivateKeyPolicy(), + PrivateKeyPolicy: keyRing.SSHPrivateKey.GetPrivateKeyPolicy(), } defer func() { testModules.MockAttestationData = nil }() - return mockSSOLogin(ctx, connectorID, priv, protocol) + return mockSSOLogin(ctx, connectorID, keyRing, protocol) } setMockSSOLogin := setMockSSOLoginCustom(mockSSOLoginWithCountAndAttestation, connector.GetName()) t.Run("cap", func(t *testing.T) { setRequireMFAType := func(t *testing.T, requireMFAType types.RequireMFAType) { // Set require MFA type in the cluster auth preference. - _, err := authServer.UpsertAuthPreference(ctx, &types.AuthPreferenceV2{ - Spec: types.AuthPreferenceSpecV2{ - RequireMFAType: requireMFAType, - }, - }) + authPref, err := authServer.GetAuthPreference(ctx) + require.NoError(t, err) + authPref.SetRequireMFAType(requireMFAType) + _, err = authServer.UpsertAuthPreference(ctx, authPref) require.NoError(t, err) } @@ -348,6 +348,7 @@ func TestHardwareKeyApp(t *testing.T) { Webauthn: &types.Webauthn{ RPID: "127.0.0.1", }, + SignatureAlgorithmSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, }, }) require.NoError(t, err) diff --git a/tool/tsh/common/kubectl_test.go b/tool/tsh/common/kubectl_test.go index 78246e134876d..21eab7e8d4465 100644 --- a/tool/tsh/common/kubectl_test.go +++ b/tool/tsh/common/kubectl_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/fixtures" "github.com/gravitational/teleport/lib/kube/kubeconfig" ) @@ -266,15 +267,17 @@ func Test_overwriteKubeconfigFlagInEnv(t *testing.T) { func mustSetupKubeconfig(t *testing.T, tshHome, kubeCluster string) string { kubeconfigLocation := path.Join(tshHome, "kubeconfig") - priv, err := keys.ParsePrivateKey([]byte(fixtures.SSHCAPrivateKey)) + key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + require.NoError(t, err) + priv, err := keys.NewSoftwarePrivateKey(key) require.NoError(t, err) err = kubeconfig.Update(kubeconfigLocation, kubeconfig.Values{ TeleportClusterName: "localhost", ClusterAddr: "https://localhost:443", KubeClusters: []string{kubeCluster}, Credentials: &client.KeyRing{ - PrivateKey: priv, - TLSCert: []byte(fixtures.TLSCACertPEM), + TLSPrivateKey: priv, + TLSCert: []byte(fixtures.TLSCACertPEM), TrustedCerts: []authclient.TrustedCerts{{ TLSCertificates: [][]byte{[]byte(fixtures.TLSCACertPEM)}, }}, diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go index a435ac75968ef..2d9146475d476 100644 --- a/tool/tsh/common/proxy.go +++ b/tool/tsh/common/proxy.go @@ -20,6 +20,7 @@ package common import ( "context" + "crypto" "crypto/tls" "crypto/x509/pkix" "fmt" @@ -626,7 +627,7 @@ func makeBasicLocalProxyConfig(ctx context.Context, tc *libclient.TeleportClient } } -func generateDBLocalProxyCert(keyRing *libclient.KeyRing, profile *libclient.ProfileStatus) error { +func generateDBLocalProxyCert(signer crypto.Signer, profile *libclient.ProfileStatus) error { path := profile.DatabaseLocalCAPath() if utils.FileExists(path) { return nil @@ -636,7 +637,7 @@ func generateDBLocalProxyCert(keyRing *libclient.KeyRing, profile *libclient.Pro CommonName: "localhost", Organization: []string{"Teleport"}, }, - Signer: keyRing.PrivateKey.Signer, + Signer: signer, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP(defaults.Localhost)}, TTL: defaults.CATTL, diff --git a/tool/tsh/common/proxy_test.go b/tool/tsh/common/proxy_test.go index b49d4a9aba399..d06b8bbeca673 100644 --- a/tool/tsh/common/proxy_test.go +++ b/tool/tsh/common/proxy_test.go @@ -19,6 +19,7 @@ package common import ( + "bufio" "bytes" "context" "crypto/ecdsa" @@ -52,6 +53,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth" @@ -61,12 +63,14 @@ import ( "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/db/dbcmd" "github.com/gravitational/teleport/lib/client/identityfile" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/teleagent" "github.com/gravitational/teleport/lib/tlsca" + "github.com/gravitational/teleport/lib/utils" testserver "github.com/gravitational/teleport/tool/teleport/testenv" ) @@ -363,12 +367,13 @@ func TestWithRsync(t *testing.T) { } defer conn.Close() - // the child will send the public key - key := make([]byte, 512) - _, err = conn.Read(key) - if !assert.NoError(t, err) { - return - } + // the child will send the SSH public key and then the TLS + // public key, with a 0 byte after key as a delimiter + reader := bufio.NewReader(conn) + sshPubKey, err := reader.ReadBytes(0) + require.NoError(t, err) + tlsPubKey, err := reader.ReadBytes(0) + require.NoError(t, err) // generate certificates for our user clusterName, err := asrv.GetClusterName() @@ -376,7 +381,8 @@ func TestWithRsync(t *testing.T) { return } sshCert, tlsCert, err := asrv.GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: key, + SSHPubKey: sshPubKey, + TLSPubKey: tlsPubKey, Username: s.user.GetName(), TTL: time.Hour, Compatibility: constants.CertificateFormatStandard, @@ -456,13 +462,15 @@ func TestWithRsync(t *testing.T) { t.Cleanup(cancel) cmd := tt.createCmd(ctx, testDir, srcPath, dstPath) - err := cmd.Run() + var stderr bytes.Buffer + cmd.Stderr = &stderr + err = cmd.Run() var msg string var exitErr *exec.ExitError if errors.As(err, &exitErr) { - msg = fmt.Sprintf("exit code: %d", exitErr.ExitCode()) - msg += fmt.Sprintf("stderr: %s", exitErr.Stderr) + msg += fmt.Sprintf("exit code: %d\n", exitErr.ExitCode()) } + msg += fmt.Sprintf("stderr: %s", stderr.String()) require.NoError(t, err, msg) // verify that dst exists and that its contents match src @@ -1007,7 +1015,7 @@ func mustLogin(t *testing.T, s *suite, args ...string) (tshHome, kubeConfig stri setHomePath(tshHome), setKubeConfigPath(kubeConfig), ) - require.NoError(t, err) + require.NoError(t, err, trace.DebugReport(err)) return } @@ -1528,16 +1536,23 @@ func TestProxyAppWithIdentity(t *testing.T) { }) require.NoError(t, err) - keyRing, err := client.GenerateRSAKeyRing() + key, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256) + require.NoError(t, err) + privateKey, err := keys.NewSoftwarePrivateKey(key) require.NoError(t, err) + // Identity files only support a single key for SSH/TLS + keyRing := client.NewKeyRing(privateKey, privateKey) keyRing.ClusterName = clusterName // generate user certs with a RouteToApp. note that unlike certs generated // with `tsh app login`, this is intended to match Machine ID-type certs // which are not usage restricted to apps only; this is required for tsh to // make other auth API calls beyond just accessing the app. + tlsPub, err := privateKey.MarshalTLSPublicKey() + require.NoError(t, err) sshCert, tlsCert, err := authServer.GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: keyRing.PrivateKey.MarshalSSHPublicKey(), + SSHPubKey: privateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, Username: userName, TTL: time.Hour, Compatibility: constants.CertificateFormatStandard, @@ -1563,38 +1578,31 @@ func TestProxyAppWithIdentity(t *testing.T) { require.NoError(t, err) port := ports.Pop() + tshArgs := []string{ + "--debug", + "--insecure", + "--identity", idPath, + "--proxy", process.Config.Proxy.WebAddr.Addr, + "proxy", "app", appName, + "--port", port, + } + utils.RunTestBackgroundTask(ctx, t, &utils.TestBackgroundTask{ + Name: "tsh proxy app", + Task: func(ctx context.Context) error { + return Run(ctx, tshArgs) + }, + }) - cancelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - errC := make(chan error) - go func() { - var tshArgs []string - if testing.Verbose() { - tshArgs = append(tshArgs, "--debug") - } - - errC <- Run(cancelCtx, append(tshArgs, - "--debug", - "--insecure", - "--identity", idPath, - "--proxy", process.Config.Proxy.WebAddr.Addr, - "proxy", "app", appName, - "--port", port, - )) - }() - - require.Eventually(t, func() bool { + err = retryutils.RetryStaticFor(5*time.Second, 50*time.Millisecond, func() error { r, err := http.Get(fmt.Sprintf("http://localhost:%s", port)) if err != nil { - return false + return err } - defer r.Body.Close() - - return r.StatusCode == 200 - }, time.Second*5, time.Millisecond*250, "a proxied app request must eventually succeed") - - cancel() - require.NoError(t, <-errC) + if r.StatusCode != 200 { + return trace.ReadError(r.StatusCode, nil) + } + return nil + }) + require.NoError(t, err, "no proxied app request succeeded") } diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 6067b6c2cbd31..500dcab53c79f 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1950,7 +1950,7 @@ func onLogin(cf *CLIConf) error { // Only allow the option during the login ceremony. tc.AllowStdinHijack = true - key, err := tc.Login(cf.Context) + keyRing, err := tc.Login(cf.Context) if err != nil { if !cf.ExplicitUsername && authclient.IsInvalidLocalCredentialError(err) { fmt.Fprintf(os.Stderr, "\nhint: set the --user flag to log in as a specific user, or leave it empty to use the system user (%v)\n\n", tc.Username) @@ -1964,7 +1964,7 @@ func onLogin(cf *CLIConf) error { // "authoritative" source. cf.Username = tc.Username - clusterClient, rootAuthClient, err := tc.ConnectToRootCluster(cf.Context, key) + clusterClient, rootAuthClient, err := tc.ConnectToRootCluster(cf.Context, keyRing) if err != nil { return trace.Wrap(err) } @@ -1976,14 +1976,14 @@ func onLogin(cf *CLIConf) error { // TODO(fspmarshall): Refactor access request & cert reissue logic to allow // access requests to be applied to identity files. if cf.IdentityFileOut != "" { - // key.TrustedCA at this point only has the CA of the root cluster we + // keyRing.TrustedCA at this point only has the CA of the root cluster we // logged into. We need to fetch all the CAs for leaf clusters too, to // make them available in the identity file. authorities, err := rootAuthClient.GetCertAuthorities(cf.Context, types.HostCA, false) if err != nil { return trace.Wrap(err) } - key.TrustedCerts = authclient.AuthoritiesToTrustedCerts(authorities) + keyRing.TrustedCerts = authclient.AuthoritiesToTrustedCerts(authorities) // If we're in multiplexed mode get SNI name for kube from single multiplexed proxy addr kubeTLSServerName := "" if tc.TLSRoutingEnabled { @@ -1993,7 +1993,7 @@ func onLogin(cf *CLIConf) error { } filesWritten, err := identityfile.Write(cf.Context, identityfile.WriteConfig{ OutputPath: cf.IdentityFileOut, - KeyRing: key, + KeyRing: keyRing, Format: cf.IdentityFormat, KubeProxyAddr: tc.KubeClusterAddr(), OverwriteDestination: cf.IdentityOverwrite, @@ -2008,10 +2008,10 @@ func onLogin(cf *CLIConf) error { return nil } - // Attempt device login. This activates a fresh key if successful. + // Attempt device login. This activates a fresh keyRing if successful. // We do not save the resulting in the identity file above on purpose, as this // certificate is bound to the present device. - if err := tc.AttemptDeviceLogin(cf.Context, key, rootAuthClient); err != nil { + if err := tc.AttemptDeviceLogin(cf.Context, keyRing, rootAuthClient); err != nil { return trace.Wrap(err) } @@ -4144,6 +4144,11 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err c.AddKeysToAgent = client.AddKeysToAgentNo } + // Identity files don't support split SSH/TLS keys. + if cf.IdentityFileOut != "" && cf.IdentityFormat == identityfile.FormatFile { + c.GenerateUnifiedKey = true + } + // headless login produces short-lived MFA-verifed certs, which should never be added to the agent. if cf.AuthConnector == constants.HeadlessConnector { if cf.AddKeysToAgent == client.AddKeysToAgentYes || cf.AddKeysToAgent == client.AddKeysToAgentOnly { @@ -4457,8 +4462,8 @@ func onShow(cf *CLIConf) error { return trace.Wrap(err) } - fmt.Printf("Cert: %#v\nPriv: %#v\nPub: %#v\n", cert, keyRing.PrivateKey.Signer, keyRing.PrivateKey.MarshalSSHPublicKey()) - fmt.Printf("Fingerprint: %s\n", ssh.FingerprintSHA256(keyRing.PrivateKey.SSHPublicKey())) + fmt.Printf("Cert: %#v\nPriv: %#v\nPub: %#v\n", cert, keyRing.SSHPrivateKey.Signer, keyRing.SSHPrivateKey.MarshalSSHPublicKey()) + fmt.Printf("Fingerprint: %s\n", ssh.FingerprintSHA256(keyRing.SSHPrivateKey.SSHPublicKey())) return nil } diff --git a/tool/tsh/common/tsh_helper_test.go b/tool/tsh/common/tsh_helper_test.go index 2c419ef64afc9..380b012805d3c 100644 --- a/tool/tsh/common/tsh_helper_test.go +++ b/tool/tsh/common/tsh_helper_test.go @@ -186,6 +186,9 @@ func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { ClusterName: "leaf1", ProxyListenerMode: types.ProxyListenerMode_Multiplex, SessionRecording: "node-sync", + Authentication: &config.AuthenticationConfig{ + SignatureAlgorithmSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, + }, }, } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 2602190eb2602..97e4dc47ffdaa 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -82,6 +82,7 @@ import ( "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/identityfile" + "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/kube/kubeconfig" "github.com/gravitational/teleport/lib/modules" @@ -137,18 +138,35 @@ func handleReexec() { // is re-executed. if addr := os.Getenv(tshBinMockHeadlessAddrEnv); addr != "" { runOpts = append(runOpts, func(c *CLIConf) error { - c.MockHeadlessLogin = func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + c.MockHeadlessLogin = func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { conn, err := net.Dial("tcp", addr) if err != nil { return nil, trace.Wrap(err, "dialing mock headless server") } defer conn.Close() - // send the server the public key - _, err = conn.Write(priv.MarshalSSHPublicKey()) + // send the server the SSH public key + _, err = conn.Write(keyRing.SSHPrivateKey.MarshalSSHPublicKey()) if err != nil { return nil, trace.Wrap(err, "writing public key to mock headless server") } + // send 0 byte as key delimiter + if _, err := conn.Write([]byte{0}); err != nil { + return nil, trace.Wrap(err, "writing delimiter") + } + // send the server the TLS public key + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } + _, err = conn.Write(tlsPub) + if err != nil { + return nil, trace.Wrap(err, "writing public key to mock headless server") + } + // send 0 byte as key delimiter + if _, err := conn.Write([]byte{0}); err != nil { + return nil, trace.Wrap(err, "writing delimiter") + } // read and decode response from server reply, err := io.ReadAll(conn) if err != nil { @@ -386,6 +404,7 @@ func TestAlias(t *testing.T) { } func TestFailedLogin(t *testing.T) { + t.Parallel() tmpHomePath := t.TempDir() connector := mockConnector(t) @@ -397,7 +416,7 @@ func TestFailedLogin(t *testing.T) { // build a mock SSO login function to patch into tsh loginFailed := trace.AccessDenied("login failed") - ssoLogin := func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) { + ssoLogin := func(ctx context.Context, connectorID string, keyRing *client.KeyRing, protocol string) (*authclient.SSHLoginResponse, error) { return nil, loginFailed } @@ -1811,50 +1830,56 @@ func TestNoRelogin(t *testing.T) { }, setHomePath(tmpHomePath), setMockSSOLogin(authProcess.GetAuthServer(), alice, connector.GetName())) require.NoError(t, err) - var loginAttempts atomic.Int32 - trackingLoginFunc := func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) { - loginAttempts.Add(1) - return mockSSOLogin(authServer, alice)(ctx, connectorID, priv, protocol) + reloginErr := errors.New("relogin failed") + trackingLoginFunc := func(ctx context.Context, connectorID string, keyRing *client.KeyRing, protocol string) (*authclient.SSHLoginResponse, error) { + return nil, reloginErr } - // should try to relogin due to bad parameter without passing --relogin - err = Run(context.Background(), []string{ - "ssh", - "--insecure", - "--user", "alice", - "--proxy", proxyAddr.String(), - "12.12.12.12:8080", - "uptime", - }, setHomePath(tmpHomePath), setMockSSOLoginCustom(trackingLoginFunc, connector.GetName())) - require.Error(t, err) - require.Equal(t, int32(1), loginAttempts.Load()) - - // should try to relogin due to bad parameter when passing --relogin - err = Run(context.Background(), []string{ - "ssh", - "--insecure", - "--relogin", - "--user", "alice", - "--proxy", proxyAddr.String(), - "12.12.12.12:8080", - "uptime", - }, setHomePath(tmpHomePath), setMockSSOLoginCustom(trackingLoginFunc, connector.GetName())) - require.Error(t, err) - require.Equal(t, int32(2), loginAttempts.Load()) - - // should skip relogin and fail instantly when passing --no-relogin - err = Run(context.Background(), []string{ - "ssh", - "--no-relogin", - "--insecure", - "--user", "alice", - "--proxy", proxyAddr.String(), - "12.12.12.12:8080", - "uptime", - }, setHomePath(tmpHomePath), setMockSSOLoginCustom(trackingLoginFunc, connector.GetName())) - require.Error(t, err) - // login not called a third time - require.Equal(t, int32(2), loginAttempts.Load()) + for _, tc := range []struct { + desc string + extraArgs []string + errorAssertion func(*testing.T, error) + }{ + { + // Should try to relogin due to bad parameter without passing --relogin. + desc: "default", + errorAssertion: func(t *testing.T, err error) { + require.ErrorIs(t, err, reloginErr) + }, + }, + { + // Should try to relogin due to bad parameter when passing --relogin. + desc: "relogin", + extraArgs: []string{"--relogin"}, + errorAssertion: func(t *testing.T, err error) { + require.ErrorIs(t, err, reloginErr) + }, + }, + { + // Should skip relogin and fail instantly when passing --no-relogin. + desc: "no relogin", + extraArgs: []string{"--no-relogin"}, + errorAssertion: func(t *testing.T, err error) { + require.Error(t, err) + require.NotErrorIs(t, err, reloginErr) + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + tshArgs := []string{"ssh"} + tshArgs = append(tshArgs, tc.extraArgs...) + tshArgs = append(tshArgs, + "--insecure", + "--user", "alice", + "--proxy", proxyAddr.String(), + "12.12.12.12:8080", + "uptime", + ) + err := Run(context.Background(), tshArgs, setHomePath(tmpHomePath), setMockSSOLoginCustom(trackingLoginFunc, connector.GetName())) + tc.errorAssertion(t, err) + }) + } } // TestSSHAccessRequest tests that a user can automatically request access to a @@ -2504,11 +2529,13 @@ func tryCreateTrustedCluster(t *testing.T, authServer *auth.Server, trustedClust } func TestKubeCredentialsLock(t *testing.T) { + t.Parallel() ctx, cancel := context.WithCancel(context.Background()) defer cancel() const kubeClusterName = "kube-cluster" t.Run("failed client creation doesn't create lockfile", func(t *testing.T) { + t.Parallel() tmpHomePath := t.TempDir() firstErr := Run(ctx, []string{ @@ -2523,6 +2550,7 @@ func TestKubeCredentialsLock(t *testing.T) { }) t.Run("kube credentials called multiple times, SSO login called only once", func(t *testing.T) { + t.Parallel() tmpHomePath := t.TempDir() connector := mockConnector(t) alice, err := types.NewUser("alice@example.com") @@ -2567,9 +2595,9 @@ func TestKubeCredentialsLock(t *testing.T) { var ssoCalls atomic.Int32 mockSSOLogin := mockSSOLogin(authServer, alice) - mockSSOLoginWithCountCalls := func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) { + mockSSOLoginWithCountCalls := func(ctx context.Context, connectorID string, keyRing *client.KeyRing, protocol string) (*authclient.SSHLoginResponse, error) { ssoCalls.Add(1) - return mockSSOLogin(ctx, connectorID, priv, protocol) + return mockSSOLogin(ctx, connectorID, keyRing, protocol) } err = Run(context.Background(), []string{ @@ -3813,6 +3841,7 @@ func makeTestServers(t *testing.T, opts ...testServerOptFunc) (auth *service.Tel cfg.SSH.Enabled = false cfg.Auth.Enabled = true cfg.Auth.ListenAddr = authAddr + cfg.Auth.Preference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1) cfg.Proxy.Enabled = true cfg.Proxy.WebAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: net.JoinHostPort("127.0.0.1", ports.Pop())} cfg.Proxy.SSHAddr = utils.NetAddr{AddrNetwork: "tcp", Addr: net.JoinHostPort("127.0.0.1", ports.Pop())} @@ -3858,20 +3887,26 @@ func mockConnector(t *testing.T) types.OIDCConnector { } func mockSSOLogin(authServer *auth.Server, user types.User) client.SSOLoginFunc { - return func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, connectorID string, keyRing *client.KeyRing, protocol string) (*authclient.SSHLoginResponse, error) { // generate certificates for our user clusterName, err := authServer.GetClusterName() if err != nil { return nil, trace.Wrap(err) } + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + if err != nil { + return nil, trace.Wrap(err) + } sshCert, tlsCert, err := authServer.GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: priv.MarshalSSHPublicKey(), - Username: user.GetName(), - TTL: time.Hour, - Compatibility: constants.CertificateFormatStandard, - RouteToCluster: clusterName.GetClusterName(), - AttestationStatement: priv.GetAttestationStatement(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, + Username: user.GetName(), + TTL: time.Hour, + Compatibility: constants.CertificateFormatStandard, + RouteToCluster: clusterName.GetClusterName(), + SSHAttestationStatement: keyRing.SSHPrivateKey.GetAttestationStatement(), + TLSAttestationStatement: keyRing.TLSPrivateKey.GetAttestationStatement(), }) if err != nil { return nil, trace.Wrap(err) @@ -3897,12 +3932,15 @@ func mockSSOLogin(authServer *auth.Server, user types.User) client.SSOLoginFunc } func mockHeadlessLogin(t *testing.T, authServer *auth.Server, user types.User) client.SSHLoginFunc { - return func(ctx context.Context, priv *keys.PrivateKey) (*authclient.SSHLoginResponse, error) { + return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { // generate certificates for our user clusterName, err := authServer.GetClusterName() require.NoError(t, err) + tlsPub, err := keyRing.TLSPrivateKey.MarshalTLSPublicKey() + require.NoError(t, err) sshCert, tlsCert, err := authServer.GenerateUserTestCerts(auth.GenerateUserTestCertsRequest{ - Key: priv.MarshalSSHPublicKey(), + SSHPubKey: keyRing.SSHPrivateKey.MarshalSSHPublicKey(), + TLSPubKey: tlsPub, Username: user.GetName(), TTL: time.Hour, Compatibility: constants.CertificateFormatStandard, @@ -5663,11 +5701,16 @@ func TestBenchmarkMySQL(t *testing.T) { } func TestLogout(t *testing.T) { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + t.Parallel() + + ctx := context.Background() + sshKey, tlsKey, err := cryptosuites.GenerateUserSSHAndTLSKey(ctx, func(_ context.Context) (types.SignatureAlgorithmSuite, error) { + return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1, nil + }) require.NoError(t, err) - privPEM, err := keys.MarshalPrivateKey(key) + sshPriv, err := keys.NewSoftwarePrivateKey(sshKey) require.NoError(t, err) - privateKey, err := keys.NewPrivateKey(key, privPEM) + tlsPriv, err := keys.NewSoftwarePrivateKey(tlsKey) require.NoError(t, err) clientKeyRing := &client.KeyRing{ KeyRingIndex: client.KeyRingIndex{ @@ -5675,7 +5718,8 @@ func TestLogout(t *testing.T) { Username: "user", ClusterName: "cluster", }, - PrivateKey: privateKey, + SSHPrivateKey: sshPriv, + TLSPrivateKey: tlsPriv, } profile := &profile.Profile{ WebProxyAddr: clientKeyRing.ProxyHost, @@ -5690,19 +5734,29 @@ func TestLogout(t *testing.T) { { name: "normal home dir", modifyKeyDir: func(t *testing.T, homePath string) {}, - }, { + }, + { name: "public key missing", modifyKeyDir: func(t *testing.T, homePath string) { pubKeyPath := keypaths.PublicKeyPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username) require.NoError(t, os.Remove(pubKeyPath)) }, - }, { - name: "private key missing", + }, + { + name: "SSH private key missing", modifyKeyDir: func(t *testing.T, homePath string) { - privKeyPath := keypaths.UserKeyPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username) + privKeyPath := keypaths.UserSSHKeyPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username) require.NoError(t, os.Remove(privKeyPath)) }, - }, { + }, + { + name: "TLS private key missing", + modifyKeyDir: func(t *testing.T, homePath string) { + privKeyPath := keypaths.UserTLSKeyPath(homePath, clientKeyRing.ProxyHost, clientKeyRing.Username) + require.NoError(t, os.Remove(privKeyPath)) + }, + }, + { name: "public key mismatch", modifyKeyDir: func(t *testing.T, homePath string) { newKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -5717,20 +5771,21 @@ func TestLogout(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tmpHomePath := t.TempDir() store := client.NewFSClientStore(tmpHomePath) - err = store.AddKeyRing(clientKeyRing) + err := store.AddKeyRing(clientKeyRing) require.NoError(t, err) store.SaveProfile(profile, true) tt.modifyKeyDir(t, tmpHomePath) - _, err := os.Lstat(tmpHomePath) + _, err = os.Lstat(tmpHomePath) require.NoError(t, err) err = Run(context.Background(), []string{"logout"}, setHomePath(tmpHomePath)) - require.NoError(t, err) + require.NoError(t, err, trace.DebugReport(err)) // direcory should be empty. f, err := os.Open(tmpHomePath)