From fb8ef40e01d7516a3147dd3fb09073728bd57cba Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Wed, 10 Jul 2024 11:50:39 -0700 Subject: [PATCH] remove RSA-specific methods on keys.PrivateKey (#43982) --- api/utils/keys/privatekey.go | 51 +++++++++++++++---------------- api/utils/sshutils/ppk/ppk.go | 3 ++ lib/benchmark/kube.go | 4 +-- lib/client/client.go | 6 ++-- lib/client/client_store.go | 4 +-- lib/kube/kubeconfig/kubeconfig.go | 10 +++--- lib/web/kube.go | 18 ++++------- tool/tsh/common/kube.go | 16 +++++----- 8 files changed, 53 insertions(+), 59 deletions(-) diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 94ea8f37b36..c1a45a8ebb4 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -144,7 +144,7 @@ func TLSCertificateForSigner(signer crypto.Signer, certPEMBlock []byte) (tls.Cer func (k *PrivateKey) PPKFile() ([]byte, error) { rsaKey, ok := k.Signer.(*rsa.PrivateKey) if !ok { - return nil, trace.BadParameter("cannot use private key of type %T as rsa.PrivateKey", k) + return nil, trace.BadParameter("only RSA keys are supported for PPK files, found private key of type %T", k.Signer) } ppkFile, err := ppk.ConvertToPPK(rsaKey, k.MarshalSSHPublicKey()) if err != nil { @@ -153,16 +153,18 @@ func (k *PrivateKey) PPKFile() ([]byte, error) { return ppkFile, nil } -// RSAPrivateKeyPEM returns a PEM encoded RSA private key for the given key. -// If the given key is not an RSA key, then an error will be returned. +// SoftwarePrivateKeyPEM returns the PEM encoding of the private key. If the key +// is not a raw software RSA, ECDSA, or Ed25519 key, then an error will be returned. // -// This is used by some integrations which currently only support raw RSA private keys, -// like Kubernetes, MongoDB, and PPK files for windows. -func (k *PrivateKey) RSAPrivateKeyPEM() ([]byte, error) { - if _, ok := k.Signer.(*rsa.PrivateKey); !ok { - return nil, trace.BadParameter("cannot get rsa key PEM for private key of type %T", k.Signer) +// This is used by some integrations which currently only support raw software +// private keys as opposed to hardware keys (yubikeys), like Kubernetes, +// MongoDB, and PPK files for windows. +func (k *PrivateKey) SoftwarePrivateKeyPEM() ([]byte, error) { + switch k.Signer.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, *ed25519.PrivateKey: + return k.keyPEM, nil } - return k.keyPEM, nil + return nil, trace.BadParameter("cannot get software key PEM for private key of type %T", k.Signer) } // LoadPrivateKey returns the PrivateKey for the given key file. @@ -316,27 +318,22 @@ func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (tls.Certificate, error) { return tlsCert, nil } -// IsRSAPrivateKey returns true if the given private key is an RSA private key. -// This function does a similar check to ParsePrivateKey, followed by key.RSAPrivateKeyPEM() -// without parsing the private fully into a crypto.Signer. -// This reduces the time it takes to check if a private key is an RSA private key -// and improves the performance compared to ParsePrivateKey by a factor of 20. -func IsRSAPrivateKey(privKey []byte) bool { +// AssertSoftwarePrivateKey returns nil if the given private key PEM looks like a +// raw software private key as opposed to a hardware key (yubikey). +// This function does a similar check to ParsePrivateKey, followed by +// key.SoftwarePrivateKeyPEM() without parsing the private fully into a +// crypto.Signer. This reduces the time it takes to check if a private key is +// a software private key and improves the performance compared to +// ParsePrivateKey by a factor of 20. +func AssertSoftwarePrivateKey(privKey []byte) error { block, _ := pem.Decode(privKey) if block == nil { - return false + return trace.BadParameter("no valid PEM block found") } switch block.Type { - case PKCS1PrivateKeyType: - return true - case PKCS8PrivateKeyType: - priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return false - } - _, ok := priv.(*rsa.PrivateKey) - return ok - default: - return false + case PKCS1PrivateKeyType, PKCS8PrivateKeyType, ECPrivateKeyType: + return nil } + return trace.BadParameter("found PEM block with type %q, only the following types are supported: %v", + block.Type, []string{PKCS1PrivateKeyType, PKCS8PrivateKeyType, ECPrivateKeyType}) } diff --git a/api/utils/sshutils/ppk/ppk.go b/api/utils/sshutils/ppk/ppk.go index 08b89136990..06048ee567c 100644 --- a/api/utils/sshutils/ppk/ppk.go +++ b/api/utils/sshutils/ppk/ppk.go @@ -37,6 +37,9 @@ import ( // ConvertToPPK takes a regular RSA-formatted keypair and converts it into the PPK file format used by the PuTTY SSH client. // The file format is described here: https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk +// +// TODO(nklaassen): support Ed25519 and ECDSA keys. The file format supports it, +// we just don't support writing them here. func ConvertToPPK(privateKey *rsa.PrivateKey, pub []byte) ([]byte, error) { // https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk // RSA keys are stored using an algorithm-name of 'ssh-rsa'. (Keys stored like this are also used by the updated RSA signature schemes that use diff --git a/lib/benchmark/kube.go b/lib/benchmark/kube.go index eb353826235..48769f2f250 100644 --- a/lib/benchmark/kube.go +++ b/lib/benchmark/kube.go @@ -101,7 +101,7 @@ func getKubeTLSClientConfig(ctx context.Context, tc *client.TeleportClient) (res certPem := k.KubeTLSCerts[tc.KubernetesCluster] - rsaKeyPEM, err := k.PrivateKey.RSAPrivateKeyPEM() + keyPEM, err := k.PrivateKey.SoftwarePrivateKeyPEM() if err != nil { return rest.TLSClientConfig{}, trace.Wrap(err) } @@ -133,7 +133,7 @@ func getKubeTLSClientConfig(ctx context.Context, tc *client.TeleportClient) (res return rest.TLSClientConfig{ CAData: bytes.Join(clusterCAs, []byte("\n")), CertData: certPem, - KeyData: rsaKeyPEM, + KeyData: keyPEM, ServerName: tlsServerName, }, nil } diff --git a/lib/client/client.go b/lib/client/client.go index 9fe7bd15084..8c002675c01 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -208,13 +208,13 @@ const ( func makeDatabaseClientPEM(proto string, cert []byte, pk *Key) ([]byte, error) { // MongoDB expects certificate and key pair in the same pem file. if proto == defaults.ProtocolMongoDB { - rsaKeyPEM, err := pk.PrivateKey.RSAPrivateKeyPEM() + keyPEM, err := pk.PrivateKey.SoftwarePrivateKeyPEM() if err == nil { - return append(cert, rsaKeyPEM...), nil + return append(cert, keyPEM...), nil } else if !trace.IsBadParameter(err) { return nil, trace.Wrap(err) } - log.WithError(err).Warn("MongoDB integration is not supported when logging in with a non-rsa private key.") + log.WithError(err).Warn("MongoDB integration is not supported when logging in with a hardware private key.") } return cert, nil } diff --git a/lib/client/client_store.go b/lib/client/client_store.go index 0b0b4f10b12..adda10e5554 100644 --- a/lib/client/client_store.go +++ b/lib/client/client_store.go @@ -275,8 +275,8 @@ func LoadKeysToKubeFromStore(profile *profile.Profile, dirPath, teleportCluster, return nil, nil, trace.Wrap(err) } - if ok := keys.IsRSAPrivateKey(privKey); !ok { - return nil, nil, trace.BadParameter("unsupported private key type") + if err := keys.AssertSoftwarePrivateKey(privKey); err != nil { + return nil, nil, trace.Wrap(err, "unsupported private key type") } return kubeCert, privKey, nil } diff --git a/lib/kube/kubeconfig/kubeconfig.go b/lib/kube/kubeconfig/kubeconfig.go index 5e967a9b8da..4d29c285258 100644 --- a/lib/kube/kubeconfig/kubeconfig.go +++ b/lib/kube/kubeconfig/kubeconfig.go @@ -250,9 +250,9 @@ func UpdateConfig(path string, v Values, storeAllCAs bool, fs ConfigFS) error { // Validate the provided credentials, to avoid partially-populated // kubeconfig. - // TODO (Joerger): Create a custom k8s Auth Provider or Exec Provider to use non-rsa - // private keys for kube credentials (if possible) - rsaKeyPEM, err := v.Credentials.PrivateKey.RSAPrivateKeyPEM() + // 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() if err == nil { if len(v.Credentials.TLSCert) == 0 { return trace.BadParameter("TLS certificate missing in provided credentials") @@ -260,7 +260,7 @@ func UpdateConfig(path string, v Values, storeAllCAs bool, fs ConfigFS) error { config.AuthInfos[contextName] = &clientcmdapi.AuthInfo{ ClientCertificateData: v.Credentials.TLSCert, - ClientKeyData: rsaKeyPEM, + ClientKeyData: keyPEM, } setContext(config.Contexts, contextName, clusterName, contextName, kubeClusterName, v.Namespace) setSelectedExtension(config.Contexts, config.CurrentContext, clusterName) @@ -268,7 +268,7 @@ func UpdateConfig(path string, v Values, storeAllCAs bool, fs ConfigFS) error { } else if !trace.IsBadParameter(err) { return trace.Wrap(err) } - log.WithError(err).Warn("Kubernetes integration is not supported when logging in with a non-rsa private key.") + log.WithError(err).Warn("Kubernetes integration is not supported when logging in with a hardware private key.") } return SaveConfig(path, *config, fs) diff --git a/lib/web/kube.go b/lib/web/kube.go index f05e52d011b..a7d0fe30efa 100644 --- a/lib/web/kube.go +++ b/lib/web/kube.go @@ -215,10 +215,10 @@ func (p *podHandler) handler(r *http.Request) error { if err != nil { return trace.Wrap(err, "failed getting user private key from the session") } - userKey := &client.Key{ - PrivateKey: pk, - Cert: p.sctx.cfg.Session.GetPub(), - TLSCert: p.sctx.cfg.Session.GetTLSCert(), + + keyPEM, err := pk.SoftwarePrivateKeyPEM() + if err != nil { + return trace.Wrap(err, "failed getting software private key") } resizeQueue := newTermSizeQueue(ctx, remotecommand.TerminalSize{ @@ -228,7 +228,7 @@ func (p *podHandler) handler(r *http.Request) error { stream := terminal.NewStream(ctx, terminal.StreamConfig{WS: p.ws, Logger: p.log, Handlers: map[string]terminal.WSHandlerFunc{defaults.WebsocketResize: p.handleResize(resizeQueue)}}) certsReq := clientproto.UserCertsRequest{ - PublicKey: userKey.MarshalSSHPublicKey(), + PublicKey: pk.MarshalSSHPublicKey(), Username: p.sctx.GetUser(), Expires: p.sctx.cfg.Session.GetExpiryTime(), Format: constants.CertificateFormatStandard, @@ -252,7 +252,6 @@ func (p *podHandler) handler(r *http.Request) error { Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, }, CertsReq: &certsReq, - Key: userKey, }) if err != nil && !errors.Is(err, services.ErrSessionMFANotRequired) { return trace.Wrap(err, "failed performing mfa ceremony") @@ -265,12 +264,7 @@ func (p *podHandler) handler(r *http.Request) error { } } - rsaKey, err := userKey.PrivateKey.RSAPrivateKeyPEM() - if err != nil { - return trace.Wrap(err, "failed getting rsa private key") - } - - restConfig, err := createKubeRestConfig(p.configServerAddr, p.configTLSServerName, p.localCA, certs.TLS, rsaKey) + restConfig, err := createKubeRestConfig(p.configServerAddr, p.configTLSServerName, p.localCA, certs.TLS, keyPEM) if err != nil { return trace.Wrap(err, "failed creating Kubernetes rest config") } diff --git a/tool/tsh/common/kube.go b/tool/tsh/common/kube.go index 78cd3c2ff31..f817b1bb9f0 100644 --- a/tool/tsh/common/kube.go +++ b/tool/tsh/common/kube.go @@ -803,34 +803,34 @@ func (c *kubeCredentialsCommand) writeKeyResponse(output io.Writer, key *client. expiry = expiry.Add(-1 * time.Minute) } - // TODO (Joerger): Create a custom k8s Auth Provider or Exec Provider to use non-rsa - // private keys for kube credentials (if possible) - rsaKeyPEM, err := key.PrivateKey.RSAPrivateKeyPEM() + // TODO (Joerger): Create a custom k8s Auth Provider or Exec Provider to use + // hardware private keys for kube credentials (if possible) + keyPEM, err := key.PrivateKey.SoftwarePrivateKeyPEM() if err != nil { return trace.Wrap(err) } - return trace.Wrap(c.writeResponse(output, key.KubeTLSCerts[kubeClusterName], rsaKeyPEM, expiry)) + return trace.Wrap(c.writeResponse(output, key.KubeTLSCerts[kubeClusterName], keyPEM, expiry)) } // writeByteResponse writes the exec credential response to the output stream. -func (c *kubeCredentialsCommand) writeByteResponse(output io.Writer, kubeTLSCert, rsaKeyPEM []byte, expiry time.Time) error { +func (c *kubeCredentialsCommand) writeByteResponse(output io.Writer, kubeTLSCert, keyPEM []byte, expiry time.Time) error { // Indicate slightly earlier expiration to avoid the cert expiring // mid-request, if possible. if time.Until(expiry) > time.Minute { expiry = expiry.Add(-1 * time.Minute) } - return trace.Wrap(c.writeResponse(output, kubeTLSCert, rsaKeyPEM, expiry)) + return trace.Wrap(c.writeResponse(output, kubeTLSCert, keyPEM, expiry)) } // writeResponse writes the exec credential response to the output stream. -func (c *kubeCredentialsCommand) writeResponse(output io.Writer, kubeTLSCert, rsaKeyPEM []byte, expiry time.Time) error { +func (c *kubeCredentialsCommand) writeResponse(output io.Writer, kubeTLSCert, keyPEM []byte, expiry time.Time) error { resp := &clientauthentication.ExecCredential{ Status: &clientauthentication.ExecCredentialStatus{ ExpirationTimestamp: &metav1.Time{Time: expiry}, ClientCertificateData: string(kubeTLSCert), - ClientKeyData: string(rsaKeyPEM), + ClientKeyData: string(keyPEM), }, } data, err := runtime.Encode(kubeCodecs.LegacyCodec(kubeGroupVersion), resp)