From 51b3bc3ddaddcb8293cd865f25d7fc4075589dff Mon Sep 17 00:00:00 2001 From: Gus Luxton Date: Tue, 12 Jul 2022 00:31:06 +0100 Subject: [PATCH] tsh: Also create a PuTTY PPK-formatted keypair on login (#12733) PuTTY uses its own keypair format which has the extension .ppk on Windows, hence PPK. The specification for the file format is here: https://the.earth.li/\~sgtatham/putty/0.76/htmldoc/AppendixC.html\#ppk --- api/client/credentials_test.go | 28 +++ api/constants/constants.go | 3 + api/profile/profile.go | 5 + api/utils/keypaths/keypaths.go | 11 + api/utils/sshutils/ppk/ppk.go | 213 +++++++++++++++++ api/utils/sshutils/ppk/ppk_test.go | 358 +++++++++++++++++++++++++++++ lib/client/interfaces.go | 9 + lib/client/keystore.go | 16 ++ 8 files changed, 643 insertions(+) create mode 100644 api/utils/sshutils/ppk/ppk.go create mode 100644 api/utils/sshutils/ppk/ppk_test.go diff --git a/api/client/credentials_test.go b/api/client/credentials_test.go index 7985d9a4b4a..0511ebbce83 100644 --- a/api/client/credentials_test.go +++ b/api/client/credentials_test.go @@ -234,6 +234,7 @@ func writeProfile(t *testing.T, p *profile.Profile) { require.NoError(t, os.WriteFile(p.KnownHostsPath(), sshCACert, 0600)) require.NoError(t, os.MkdirAll(p.SSHDir(), 0700)) require.NoError(t, os.WriteFile(p.SSHCertPath(), sshCert, 0600)) + require.NoError(t, os.WriteFile(p.PPKFilePath(), ppkFile, 0600)) } func getExpectedTLSConfig(t *testing.T) *tls.Config { @@ -347,4 +348,31 @@ Na6B0YR7mdrrL+lyzymnOr6UOrT5nUWRAB1QeY7dhBNnsvoZwaS3VLSc1KCk sshCert = []byte("ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8C10PShw+GxCadSlC4nFURIAyvDtgWRvHPabpL5wzDQAAAADAQABAAABAQDORRWgniufZcCLYcl4EgjGWx0w8bMuugm5v14cBWykC54MktKCpB24dOiDTVH0wABGhZTBtAs3QMhskUrvRSvARNdu5ERwbOoW9aU4Mtn+kxZBaP4eFk1luBkEojvAJewNrbkY3N5gJ5O9avpL6UGEu7Z5IZhQmBPIysTLZWBt/ceJEOm7ZZez/cyOl2b+UUa5c6gA7sGaRHC2FYtE4yE6j28d6w2U+JfhJrJYWqBvsROVbvhmFy5b8AfRP2pnzdWfSqbODm+iccbHvZI3jIq/ZsIjZAVlcoR/yxEwwPV2urE0Nnu+TGDO8lyS2DpgSleINe+kH9U9cnu2vxoJ+LdlAAAAAAAAAAAAAAABAAAADGFjY2Vzcy1hZG1pbgAAABAAAAAMYWNjZXNzLWFkbWluAAAAAGAtfCkAAAAAYC4lJQAAAAAAAACdAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnRlbGVwb3J0LXJvbGVzAAAALQAAACl7InZlcnNpb24iOiJ2MSIsInJvbGVzIjpbImFjY2Vzcy1hZG1pbiJdfQAAAA90ZWxlcG9ydC10cmFpdHMAAAATAAAAD3sibG9naW5zIjpudWxsfQAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQD3VbuNmR0h3tjYIkTVG+HfNByigp6tuNl8XVylIWx7a7ojRA1nJVzAtNs9QQMut8XY+7jxf4Ue83eIaE0e0QKA0GZlRdbSG0zaYzK8CDAcPVN6Ywt8jnGKuuMhBAckGkN/9nyuJHgTAKeHYgdgQgijPuW/D59s3Sk3vCRHryZzJfZDQ52i40B1q2zLvCcQa6UBvPblHAF3usRa08DnsNkgLey1EkkyvBazqt1amH2Epl3uJRHHUtRVSp2a+0597leT58RZNFfFfB9pccPJfD7cn+iiDmN62T/8YslLYl/O6xCJ43Or7wIRHwJ1tY5hq/Bw7LYn29zeBrIkxIvsH8WtAAABFAAAAAxyc2Etc2hhMi01MTIAAAEAhIz0X+wgA0B8Bi67ALpTEA3kHVWaQY3aT+Ig8obof9upq51H0YlySPJph8h6pVzfSJzQYtuGbmzQ/XAGRMn541mnSUGoy0WCHzscyCowaj9VgjFyVpct7Nz98dB3PnRocNTajGGla+AteZEU3d6KXv/CaA4NGwO3k0rYB+UfX0AAaatAwwxnzYehpCvwSqPdrq/OIyb0aljZHADoNRrcnmYDbB1V76WWY6eTCxYGXx1QyU4A8kH9U8pIZ1fVif/i8dSTbBTftTtv5bmO4WUbVscRw/xIqgZ8v6StNLGHPTt/+Zn+iUoiIrwcnpy+yQp2SRTv7+Lg2SSvJO818x3NNg==") sshCACert = []byte("@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMIgxZpT5362npj0x6NQA76IB73bcK85K8cEyKURuHtFC83RjBzvzqtUz6X02+6ohVZiR2MdmsXkCLznzwEIZ0NtoxgnLTZLmduPLeAuYW2vIFpd0G17y6Yog9vxhQ0BLdlhU5Y3JYjRYjmQMfe1iD/RXWD6rEvgWlz+c3HMQR33JqkVIEFH34upfkC2RQG3TXjMe5t14l3yCTtyF5YGzN7+6z/4+/EDto/F3zVtSEp+k8XE/m0ddTGo7usa8ErAom31RwrgkNRmgJmPleDwEflybEsgGKApJXkfFxmG2wu20JoEt/CFjY3fIIa/5aqIGJPpMH4aEdLcj/iyNCog8D type=host") + + ppkFile = []byte(`PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: test.com +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDORRWgniufZcCLYcl4EgjGWx0w8bMuugm5 +v14cBWykC54MktKCpB24dOiDTVH0wABGhZTBtAs3QMhskUrvRSvARNdu5ERwbOoW +9aU4Mtn+kxZBaP4eFk1luBkEojvAJewNrbkY3N5gJ5O9avpL6UGEu7Z5IZhQmBPI +ysTLZWBt/ceJEOm7ZZez/cyOl2b+UUa5c6gA7sGaRHC2FYtE4yE6j28d6w2U+Jfh +JrJYWqBvsROVbvhmFy5b8AfRP2pnzdWfSqbODm+iccbHvZI3jIq/ZsIjZAVlcoR/ +yxEwwPV2urE0Nnu+TGDO8lyS2DpgSleINe+kH9U9cnu2vxoJ+Ld +Private-Lines: 14 +AAABAE1Vk207wAksAgt/5yQwRr/vizs9czuSnnDYsbT5x6idfm0iYvB+DXKJyl7o +D1Ee5zuJe6NAGHBnxn0F4D1jBqs4ZDj8NjicbQucn4w5bIfIp7BwZ83p+KypYB/f +n11EGoNqXZpXvLv6Oqbqw9rQIjNcmWZC1TNqQQioFS5Y3NV/gw5uYCRXZlSLMsRC +vcX2+LN2EP76ZbkpIVpTCidC2TxwFPPbyMsG774Olfz4U2IDgX1mO+milF7RIa/v +PADSeHAX6tJHmZ13GsyP0GAdPbFa0Ls/uykeGi1uGPFkdkNEqbWlDf1Z9IG0dr/c +k2eh8G2X8E+VFgzsKp4kWtH9nGEAAACBAOQgKFCPDVQPRqgCX7O4ZBh0MKV9V9fi +aRYReHWSrFeNDUXqmitL3f5lk2I5TDjzuqKJaz6Ag1JUGFOqaCA7RJ3yeipGLizI +MWSp0tjpQ7YSqGSXvWlEwj9UYU1R8sgAUV2xoLTTChWJGd/AvfiTPl+U9HimUx3i +vsI4mXeefrJtAAAAgQDneUWh0uIpCNBHsihSYan4/qqesPA51TVF9P2Ox7fnE5v2 +1i9mzdeRRdT4wQYAxbU++ajW/3E6Nlt0VgH0j+0hhNLKNhA1oWOAkw8wtLHaqMzO +EVcBjl/y3bT8IG3ZXWrjppry1HaWX/9C9jiaq8lRpoHSmS5qwVsoxclwYp292QAA +AIBV1ZA8WqvC+xZrPwmtmN87BHwGjqpE52kbUfcD94k8IqqhPR9oN9uOlcoBzZiS +3SkunUpmzKlcXe63RQYOEqEVlTNOafcYNc5gW8NXKrgF7vBE91VsfmOGJvLt3pIv +k53lH1qmEOm9+vrhNwNzpHk4AqDkP+0YDG++B4n0BtJJpw== +Private-MAC: 8951bbe929e0714a61df01bc8fbc5223e3688f174aee29339931984fb9224c7d`) ) diff --git a/api/constants/constants.go b/api/constants/constants.go index 40abac7f260..056439c77e9 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -131,6 +131,9 @@ const ( // DatabaseCAMinVersion is the minimum Teleport version that supports Database Certificate Authority. DatabaseCAMinVersion = "10.0.0" + + // SSHRSAType is the string which specifies an "ssh-rsa" formatted keypair + SSHRSAType = "ssh-rsa" ) // SystemConnectors lists the names of the system-reserved connectors. diff --git a/api/profile/profile.go b/api/profile/profile.go index acd08fa99ab..9a46275bcdc 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -407,6 +407,11 @@ func (p *Profile) SSHCertPath() string { return keypaths.SSHCertPath(p.Dir, p.Name(), p.Username, p.SiteName) } +// PPKFilePath returns the path to the profile's PuTTY PPK-formatted keypair. +func (p *Profile) PPKFilePath() string { + return keypaths.PPKFilePath(p.Dir, p.Name(), p.Username) +} + // KnownHostsPath returns the path to the profile's ssh certificate authorities. func (p *Profile) KnownHostsPath() string { return keypaths.KnownHostsPath(p.Dir) diff --git a/api/utils/keypaths/keypaths.go b/api/utils/keypaths/keypaths.go index 7fe657653ee..3f8ccb4118a 100644 --- a/api/utils/keypaths/keypaths.go +++ b/api/utils/keypaths/keypaths.go @@ -38,6 +38,8 @@ const ( fileNameTLSCerts = "certs.pem" // fileExtCert is the suffix/extension of a file where an SSH Cert is stored. fileExtSSHCert = "-cert.pub" + // fileExtPPK is the suffix/extension of a file where an SSH keypair is stored in PuTTY PPK format. + fileExtPPK = ".ppk" // fileExtPub is the extension of a file where a public key is stored. fileExtPub = ".pub" // fileExtLocalCA is the extension of a file where a self-signed localhost CA cert is stored. @@ -64,6 +66,7 @@ const ( // │ ├── certs.pem --> TLS CA certs for the Teleport CA // │ ├── foo --> RSA Private Key for user "foo" // │ ├── foo.pub --> Public Key +// │ ├── foo.ppk --> PuTTY PPK-formatted keypair for user "foo" // │ ├── 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" @@ -171,6 +174,14 @@ func SSHDir(baseDir, proxy, username string) string { return filepath.Join(ProxyKeyDir(baseDir, proxy), username+sshDirSuffix) } +// PPKFilePath returns the path to the user's PuTTY PPK-formatted keypair +// for the given proxy and cluster. +// +// /keys//.ppk +func PPKFilePath(baseDir, proxy, username string) string { + return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtPPK) +} + // SSHCertPath returns the path to the users's SSH certificate // for the given proxy and cluster. // diff --git a/api/utils/sshutils/ppk/ppk.go b/api/utils/sshutils/ppk/ppk.go new file mode 100644 index 00000000000..6c69e6c071b --- /dev/null +++ b/api/utils/sshutils/ppk/ppk.go @@ -0,0 +1,213 @@ +/* +Copyright 2021 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package ppk provides functions implementing conversion between Teleport's native RSA +// keypairs and PuTTY's PPK format. It also provides functions for working with RFC4251-formatted +// mpints and strings. +package ppk + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + + "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/trace" +) + +// 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 +func ConvertToPPK(priv []byte, pub []byte) ([]byte, error) { + // decode the private key from PEM format and extract the exponents + privateKeyPemBlock, rest := pem.Decode(priv) + if len(rest) > 0 { + return nil, trace.Errorf("failed to decode private key, %v bytes left over", len(rest)) + } + privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes) + if err != nil { + return nil, trace.Errorf("failed to parse private key: %v", err) + } + + // 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 + // hashes other than SHA-1. The public key data has already provided the key modulus and the public encoding exponent. The private data stores: + // mpint: the private decoding exponent of the key. + // mpint: one prime factor p of the key. + // mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.) + // mpint: the multiplicative inverse of q modulo p. + ppkPrivateKey := new(bytes.Buffer) + + // mpint: the private decoding exponent of the key. + // this is known as 'D' + binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(privateKey.D)) + + // mpint: one prime factor p of the key. + // this is known as 'P' + // the RSA standard dictates that P > Q + // for some reason what PuTTY names 'P' is Primes[1] to Go, and what PuTTY names 'Q' is Primes[0] to Go + P, Q := privateKey.Primes[1], privateKey.Primes[0] + binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(P)) + + // mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.) + // this is known as 'Q' + binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(Q)) + + // mpint: the multiplicative inverse of q modulo p. + // this is known as 'iqmp' + iqmp := new(big.Int).ModInverse(Q, P) + binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(iqmp)) + + // now we need to base64-encode the PPK-formatted private key which is made up of the above values + ppkPrivateKeyBase64 := make([]byte, base64.StdEncoding.EncodedLen(ppkPrivateKey.Len())) + base64.StdEncoding.Encode(ppkPrivateKeyBase64, ppkPrivateKey.Bytes()) + + // read Teleport public key + // fortunately, this is the one thing that's in exactly the same format that the PPK file uses, so we can just copy it verbatim + // remove ssh-rsa plus additional space from beginning of string if present + if !bytes.HasPrefix(pub, []byte(constants.SSHRSAType+" ")) { + return nil, trace.BadParameter("pub does not appear to be an ssh-rsa public key") + } + pub = bytes.TrimSuffix(bytes.TrimPrefix(pub, []byte(constants.SSHRSAType+" ")), []byte("\n")) + + // the PPK file contains an anti-tampering MAC which is made up of various values which appear in the file. + // copied from Section C.3 of https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk: + // hex-mac-data is a hexadecimal-encoded value, 64 digits long (i.e. 32 bytes), generated using the HMAC-SHA-256 algorithm with the following binary data as input: + // string: the algorithm-name header field. + // string: the encryption-type header field. + // string: the key-comment-string header field. + // string: the binary public key data, as decoded from the base64 lines after the 'Public-Lines' header. + // string: the plaintext of the binary private key data, as decoded from the base64 lines after the 'Private-Lines' header. + + // these values are also used in the MAC generation, so we declare them as variables + keyType := constants.SSHRSAType + encryptionType := "none" + // as work for the future, it'd be nice to get the proxy/user pair name in here to make the name more + // of a unique identifier. this has to be done at generation time because the comment is part of the MAC + fileComment := "teleport-generated-ppk" + + // string: the algorithm-name header field. + macKeyType := getRFC4251String([]byte(keyType)) + // create a buffer to hold the elements needed to generate the MAC + macInput := new(bytes.Buffer) + binary.Write(macInput, binary.LittleEndian, macKeyType) + + // string: the encryption-type header field. + macEncryptionType := getRFC4251String([]byte(encryptionType)) + binary.Write(macInput, binary.BigEndian, macEncryptionType) + + // string: the key-comment-string header field. + macComment := getRFC4251String([]byte(fileComment)) + binary.Write(macInput, binary.BigEndian, macComment) + + // base64-decode the Teleport public key, as we need its binary representation to generate the MAC + decoded := make([]byte, base64.StdEncoding.EncodedLen(len(pub))) + n, err := base64.StdEncoding.Decode(decoded, pub) + if err != nil { + return nil, trace.Errorf("could not base64-decode public key: %v, got %v bytes successfully", err, n) + } + decoded = decoded[:n] + // append the decoded public key bytes to the MAC buffer + macPublicKeyData := getRFC4251String(decoded) + binary.Write(macInput, binary.BigEndian, macPublicKeyData) + + // append our PPK-formatted private key bytes to the MAC buffer + macPrivateKeyData := getRFC4251String(ppkPrivateKey.Bytes()) + binary.Write(macInput, binary.BigEndian, macPrivateKeyData) + + // as per the PPK spec, the key for the MAC is blank when the PPK file is unencrypted. + // therefore, the key is a zero-length byte slice. + hmacHash := hmac.New(sha256.New, []byte{}) + // generate the MAC using HMAC-SHA-256 + hmacHash.Write(macInput.Bytes()) + macString := hex.EncodeToString(hmacHash.Sum(nil)) + + // build the string-formatted output PPK file + ppk := new(bytes.Buffer) + fmt.Fprintf(ppk, "PuTTY-User-Key-File-3: %v\n", keyType) + fmt.Fprintf(ppk, "Encryption: %v\n", encryptionType) + fmt.Fprintf(ppk, "Comment: %v\n", fileComment) + // chunk the Teleport-formatted public key into 64-character length lines + chunkedPublicKey := chunk(string(pub), 64) + fmt.Fprintf(ppk, "Public-Lines: %v\n", len(chunkedPublicKey)) + for _, r := range chunkedPublicKey { + fmt.Fprintf(ppk, "%s\n", r) + } + // chunk the PPK-formatted private key into 64-character length lines + chunkedPrivateKey := chunk(string(ppkPrivateKeyBase64), 64) + fmt.Fprintf(ppk, "Private-Lines: %v\n", len(chunkedPrivateKey)) + for _, r := range chunkedPrivateKey { + fmt.Fprintf(ppk, "%s\n", r) + } + fmt.Fprintf(ppk, "Private-MAC: %v\n", macString) + + return ppk.Bytes(), nil +} + +// chunk converts a string into a []string with chunks of size chunkSize; +// used to split base64-encoded strings across multiple lines with an even width. +// note: this function operates on Unicode code points rather than bytes, therefore +// using it with multi-byte characters will result in unevenly chunked strings. +// it's intended usage is only for chunking base64-encoded strings. +func chunk(s string, size int) []string { + var chunks []string + for b := []byte(s); len(b) > 0; { + n := size + if n > len(b) { + n = len(b) + } + chunks = append(chunks, string(b[:n])) + b = b[n:] + } + return chunks +} + +// getRFC4251Mpint returns a stream of bytes representing a mixed-precision integer (a big.Int in Go) +// prepended with a big-endian uint32 expressing the length of the data following. +// This is the 'mpint' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5) +func getRFC4251Mpint(n *big.Int) []byte { + buf := new(bytes.Buffer) + b := n.Bytes() + // RFC4251: If the most significant bit would be set for a positive number, the number MUST be preceded by a zero byte. + if b[0]&0x80 > 0 { + b = append([]byte{0}, b...) + } + // write a uint32 with the length of the byte stream to the buffer + binary.Write(buf, binary.BigEndian, uint32(len(b))) + // write the byte stream representing of the rest of the integer to the buffer + binary.Write(buf, binary.BigEndian, b) + return buf.Bytes() +} + +// getRFC4251String returns a stream of bytes representing a string prepended with a big-endian unit32 +// expressing the length of the data following. +// This is the 'string' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5) +func getRFC4251String(data []byte) []byte { + buf := new(bytes.Buffer) + // write a uint32 with the length of the byte stream to the buffer + binary.Write(buf, binary.BigEndian, uint32(len(data))) + // write the byte stream representing of the rest of the data to the buffer + for _, v := range data { + binary.Write(buf, binary.BigEndian, v) + } + return buf.Bytes() +} diff --git a/api/utils/sshutils/ppk/ppk_test.go b/api/utils/sshutils/ppk/ppk_test.go new file mode 100644 index 00000000000..a9cb7d1d212 --- /dev/null +++ b/api/utils/sshutils/ppk/ppk_test.go @@ -0,0 +1,358 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package ppk_test provides tests for the ppk package +package ppk_test + +import ( + "testing" + + "github.com/gravitational/teleport/api/utils/sshutils/ppk" + "github.com/stretchr/testify/require" +) + +func TestConvertToPPK(t *testing.T) { + tests := []struct { + desc string + priv []byte + pub []byte + output []byte + wantErr bool + }{ + { + desc: "valid private and public keys 1", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3U4OOAi+F1Ct1n8HZIs1P39CWB0mKLvshouuklenZug27SuI +14rjE+hOTNHYz/Pkvk5mmKuIdegMCe8FHAF6chygcEC9BDkowLO+2+f3sazGsu4A +9H4pDuUkuIM9MwmZV7A4TJ19rRAgha+6JKKR5KeEosfiLvAtOu2Pjqz8ZrOrUUqQ +1AJ71SkWMPTJFksTNmgaH7a0SgJ4vVYMlYIAeyoAgqn6Qvu5Kez5ROfeKD4zys/+ +iFenrgbJrC38GNe2rxtb8/gfy03023FlPAQjGd1VLjxm8jhcJFqgM+uHTGRckgjv +d+VIkCbvTwpPWvvZxQcRtk073P9G8xpiNz2qbwIDAQABAoIBAQCFv37obqA0BxaI +5AzbvyZXUdoO1s8RH0I7rn+7Ai6yCvXnMMBrRA0pIuTvmIOoaoZ8XXW0HzdByxQ7 +jLFR07Lk9Fgif328566xh/B5hyAzyW/tA9qf6P93eRVQTkDWb561WFMuOqCRz4VY +RnQBYB88SeHnX1Zbd9xeGOUCHZoNlrilVpgjscGcFNxyDP72qvI79z1vV+R6dhaf +YI2v1D6aqx9qM988ytOokNi79wYvSUxqitz3IOD5nBd9ZNBC0fDeVmHqqbHSvLrr +LouF7PiUuVA2LaWfVCy5dVtLkS16qbsfqzUA4B8Eg/oF0vPpJ7QMVxKI5j2//ScL +lQ9h6gUBAoGBAOQ0t9gGuHKOMcp3H9C2fzNVbbWTubJoUyzFGyx+U2aJ4byRbxS3 +5d9cVu1GpS2ZgW6izCmxTG61Q0qQd4iT8e5cnFRU1Q3aK29TTK5hptthknXwKkVN +vUtlYKRM3TPYeTJ3WMQCY/Lzm2uVhT2ZGkpu0NaA5qiWllyPm7HlyQA/AoGBAPhC +KzioaPlqzwNKtHCsDSyeXsxU1aJCuMCIcgOB1yzmaaeL95CwMouMgouFyQ/CtLtO +pQEjymGzVynwC15s1vh1nCOWlQCx6Cjs9ko9bmecqziyyWg94gn82yLU7gClQH6v ++ezQ1n7/pb1DO/8dytO3+BZKSQH9lobzravGTcnRAoGAL8nKZfaiUXrtelSP2Qke +ggV1v/x7epzWLh3ontylYmelWfOqq1AHV0ri+TU+CdqHfD+jOWfjdZuHx+mQ3oz8 +sMm8Avzw0MHLLrjm6e2RH4fDP+dXMsQgy9Ui88UU3XKLjsHnWMSXYZ0aAuGA0XFq +TAQAv6qmos9GFYQNOqe/+8kCgYEAv88H69eae5J9bTKr5R3Zc+7MmZy2Do70hbUm +OfV4lbVUTmJDHWQ1OUKPnlL4fJfX4Zwquo23kPLqVnmjnwoCsabUw15Vs1rBX9Vt +mQCLq7wNQlpIaKTfXw4hFXFkjdUf1oIKXGEiSK8mk+s9kKepDRlnsXklnUcbpRri +xQQLF/ECgYAmKBSQtPuyA9d3dAZj96HhYZzDjD2EtAhSUyx31vgqr8C7mmShQXLh +kFap4eAldBxySXp/5af7H1Xf4BIfbbc1prMM1vIRFTN6l6rbircak7bb9a/dgWmX +iukFsFq0G0Y2zt9oHOB7pKV/Kff4o1WQ0hcCBD6pZGhbsVxXBi4Oaw== +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdTg44CL4XUK3WfwdkizU/f0JYHSYou+yGi66SV6dm6DbtK4jXiuMT6E5M0djP8+S+TmaYq4h16AwJ7wUcAXpyHKBwQL0EOSjAs77b5/exrMay7gD0fikO5SS4gz0zCZlXsDhMnX2tECCFr7okopHkp4Six+Iu8C067Y+OrPxms6tRSpDUAnvVKRYw9MkWSxM2aBoftrRKAni9VgyVggB7KgCCqfpC+7kp7PlE594oPjPKz/6IV6euBsmsLfwY17avG1vz+B/LTfTbcWU8BCMZ3VUuPGbyOFwkWqAz64dMZFySCO935UiQJu9PCk9a+9nFBxG2TTvc/0bzGmI3Papv`), + output: []byte(`PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: teleport-generated-ppk +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDdTg44CL4XUK3WfwdkizU/f0JYHSYou+yG +i66SV6dm6DbtK4jXiuMT6E5M0djP8+S+TmaYq4h16AwJ7wUcAXpyHKBwQL0EOSjA +s77b5/exrMay7gD0fikO5SS4gz0zCZlXsDhMnX2tECCFr7okopHkp4Six+Iu8C06 +7Y+OrPxms6tRSpDUAnvVKRYw9MkWSxM2aBoftrRKAni9VgyVggB7KgCCqfpC+7kp +7PlE594oPjPKz/6IV6euBsmsLfwY17avG1vz+B/LTfTbcWU8BCMZ3VUuPGbyOFwk +WqAz64dMZFySCO935UiQJu9PCk9a+9nFBxG2TTvc/0bzGmI3Papv +Private-Lines: 14 +AAABAQCFv37obqA0BxaI5AzbvyZXUdoO1s8RH0I7rn+7Ai6yCvXnMMBrRA0pIuTv +mIOoaoZ8XXW0HzdByxQ7jLFR07Lk9Fgif328566xh/B5hyAzyW/tA9qf6P93eRVQ +TkDWb561WFMuOqCRz4VYRnQBYB88SeHnX1Zbd9xeGOUCHZoNlrilVpgjscGcFNxy +DP72qvI79z1vV+R6dhafYI2v1D6aqx9qM988ytOokNi79wYvSUxqitz3IOD5nBd9 +ZNBC0fDeVmHqqbHSvLrrLouF7PiUuVA2LaWfVCy5dVtLkS16qbsfqzUA4B8Eg/oF +0vPpJ7QMVxKI5j2//ScLlQ9h6gUBAAAAgQD4Qis4qGj5as8DSrRwrA0snl7MVNWi +QrjAiHIDgdcs5mmni/eQsDKLjIKLhckPwrS7TqUBI8phs1cp8AtebNb4dZwjlpUA +sego7PZKPW5nnKs4ssloPeIJ/Nsi1O4ApUB+r/ns0NZ+/6W9Qzv/HcrTt/gWSkkB +/ZaG862rxk3J0QAAAIEA5DS32Aa4co4xyncf0LZ/M1VttZO5smhTLMUbLH5TZonh +vJFvFLfl31xW7UalLZmBbqLMKbFMbrVDSpB3iJPx7lycVFTVDdorb1NMrmGm22GS +dfAqRU29S2VgpEzdM9h5MndYxAJj8vOba5WFPZkaSm7Q1oDmqJaWXI+bseXJAD8A +AACBAM6/w3llPMNA/ZRm8wIXXssPgAZCN79zYtVu6n4KMqBzi7qj1er4gzsLZpKS +hpfdO/mDPhA3eFwU3XjYCKlHiJJYk53mc5sWwvbsfibAZSZAII/V4xWvRUUPE9EX +INDa/8cd4YSy3PiZnUTNLVb2SmRFhnlB8ZBk3CyGEvcHskir +Private-MAC: 2697903ac84b70273afc7adaa4e3ebb14536cdaf69654d40e3d46a5ba997ffb0 +`), + wantErr: false, + }, + { + desc: "valid private and public keys 2", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAve2um90K1SkpJD1vcjm2zUYUh5ZU7q1cmO7F0J/6MCEcq3vH +fDPpPZ4uGLB9jPKzs6FYWhwFNW2oAsDvWSrwwxy5gl1dAdqp1wIm86gafShR0se5 +rSdhWKP40H2lHOysRC5Jr8cvVLgflvZ4PDMqr/63BKwwkT1vN2PuenYRAAIT77X9 +O0fumGQPKIxRGn5OPKEt1LzQ0+e/QlWrqZwzWDx5jqG3jbxibdcR/mHS60XdvusL +UqxxWPjhVlDsKfvh2lt5sqsjulWW/GyNtlCfaTn2uu0nV8nbT2OvEO+oM/uyHos5 +7aIyePcOzCVM4dug6xJinqYTaVUsskKjPGUV6QIDAQABAoIBAQCBfis9k6DOIukt +D0IL5DOxk2Vt6F5x+PsYPjva+SfwZrMQbC1fjlkpLM8LAFIpplRFVe1SSqZ2fhQ+ +BGNsLS3IKa6FprhCCl8f/BSoreWZjcLz7j63QxFJCUscg33u0aLGPbT5xtmLbpoD +KHpjuRMSuZz475mRfQx1/IldL2B52sIAD6XRTgFoRG+mLu2iNVvuE0RVbASiyOUs +lVwrGRI+5GuH8G6fDCJqpYzcm/S8VXmQc2jrbo/gQ76MkFxULqEMzadjN+XMXms7 +pGZLX6Hatubn1kmhl8l6+1GYLf1HVmWXoL+hgWwbfIn6WV9y/xpnoeoJfWcFLJli +yABDx/mBAoGBAPhw3thyEP+5jdH2n1vz4X76yUbNJXaJGXozdoNfFKNOrYjFCLnD +CzHJEQmDJoFCtF6TwgvFb90HNvtNLkbC81yotQ8rfDzNTlixUhycaSsCJBqw0loU +wXoYQZiXpbfqT9Y7x7pwMxzRtkQYvyaowc7qF1xwJHhyCjDx38jAGnZxAoGBAMO1 +DXUpca09h+FujJkziyJStYq0YKqsuKXW7CuAq2iY70lzhv+SIPErqcYIWwi8JNv9 +EwBlEmSltFyGtxpeIVl6MJTil3vQ6eOSBCwt/E1YKvZoLv6mDf52Lc/wKtlecRPG +Q7G2C1ioTD9lDiYysUDmkpfitiatFwEj+y606wL5AoGBAMlQJLM9Ets1D19QuWb4 +YwPS0aBGgZHgnD1yUBk5xW5jRajrCBwGmR6Zb+3GUUAyvhdZIccKEJAI1Zuiudnr +BOpTZovJT92w+0hRP1khwPJxxLHAEGOgJ/r4hsbQMx+phVHylPBVFIXIxSm+5726 +x3kUJSPpVxQmTG3GwPBaAddxAoGALq+4QCTc22j8S0jl/X4QSOXWLPqOvOhrPBSj +TlVpjpA9NRZ8M+eWODIkU/uWS+UmHdyndcamtp/ZAOGaOI4QApplkH7liEH0Kbeh +izCFKaZIyXNdEp5mZDepAhvW/PfMnd0ENRaqakHrvovK7k3VfxgCDH2m2l8cR8df +mmrKTXECgYEA300gTnT46pMU1Wr1Zq4vGauWzk3U4J9HUu3vNy+sg4EEZ9CoiNTw +0a3f8u8gNQjB30koGW/5jYex3fUcnjTPqEGaiiGjI4oxMhquzqkVQ8FwnBAXJgT8 +nQVO8MZw8iFeSap0ILum8t60sp1/u9aCWJbjPtb/fhx0q7SLdjFEw8s= +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC97a6b3QrVKSkkPW9yObbNRhSHllTurVyY7sXQn/owIRyre8d8M+k9ni4YsH2M8rOzoVhaHAU1bagCwO9ZKvDDHLmCXV0B2qnXAibzqBp9KFHSx7mtJ2FYo/jQfaUc7KxELkmvxy9UuB+W9ng8Myqv/rcErDCRPW83Y+56dhEAAhPvtf07R+6YZA8ojFEafk48oS3UvNDT579CVaupnDNYPHmOobeNvGJt1xH+YdLrRd2+6wtSrHFY+OFWUOwp++HaW3myqyO6VZb8bI22UJ9pOfa67SdXydtPY68Q76gz+7IeizntojJ49w7MJUzh26DrEmKephNpVSyyQqM8ZRXp`), + output: []byte(`PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: teleport-generated-ppk +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQC97a6b3QrVKSkkPW9yObbNRhSHllTurVyY +7sXQn/owIRyre8d8M+k9ni4YsH2M8rOzoVhaHAU1bagCwO9ZKvDDHLmCXV0B2qnX +AibzqBp9KFHSx7mtJ2FYo/jQfaUc7KxELkmvxy9UuB+W9ng8Myqv/rcErDCRPW83 +Y+56dhEAAhPvtf07R+6YZA8ojFEafk48oS3UvNDT579CVaupnDNYPHmOobeNvGJt +1xH+YdLrRd2+6wtSrHFY+OFWUOwp++HaW3myqyO6VZb8bI22UJ9pOfa67SdXydtP +Y68Q76gz+7IeizntojJ49w7MJUzh26DrEmKephNpVSyyQqM8ZRXp +Private-Lines: 14 +AAABAQCBfis9k6DOIuktD0IL5DOxk2Vt6F5x+PsYPjva+SfwZrMQbC1fjlkpLM8L +AFIpplRFVe1SSqZ2fhQ+BGNsLS3IKa6FprhCCl8f/BSoreWZjcLz7j63QxFJCUsc +g33u0aLGPbT5xtmLbpoDKHpjuRMSuZz475mRfQx1/IldL2B52sIAD6XRTgFoRG+m +Lu2iNVvuE0RVbASiyOUslVwrGRI+5GuH8G6fDCJqpYzcm/S8VXmQc2jrbo/gQ76M +kFxULqEMzadjN+XMXms7pGZLX6Hatubn1kmhl8l6+1GYLf1HVmWXoL+hgWwbfIn6 +WV9y/xpnoeoJfWcFLJliyABDx/mBAAAAgQDDtQ11KXGtPYfhboyZM4siUrWKtGCq +rLil1uwrgKtomO9Jc4b/kiDxK6nGCFsIvCTb/RMAZRJkpbRchrcaXiFZejCU4pd7 +0OnjkgQsLfxNWCr2aC7+pg3+di3P8CrZXnETxkOxtgtYqEw/ZQ4mMrFA5pKX4rYm +rRcBI/sutOsC+QAAAIEA+HDe2HIQ/7mN0fafW/PhfvrJRs0ldokZejN2g18Uo06t +iMUIucMLMckRCYMmgUK0XpPCC8Vv3Qc2+00uRsLzXKi1Dyt8PM1OWLFSHJxpKwIk +GrDSWhTBehhBmJelt+pP1jvHunAzHNG2RBi/JqjBzuoXXHAkeHIKMPHfyMAadnEA +AACAE820IDiCymxsVqgmBSNJttApBaSl3ljTzWWeJQR7ksIm9kBvy30j1682v0yq +RyPuY1EmQ3DJ3LqXbFq4qK12R/tALasyYyDYsJTt1xh+peFv23OSF8kDlG4MOdUp +3WPivAMSPR0QR192Emb0caXEkyAhvQLHKGoi8/TgbfMG6Gc= +Private-MAC: b5ede95d052e23815c8e8d816c758fb16370fc3178e1613fee61ec158900fd64 +`), + wantErr: false, + }, + { + desc: "valid public and private keys 3", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAz5J/f572H95c9DDZLrXT0kmjytznkvntSOjxmJM44fL8DQz2 +NINFi4awTNYD1eIIzaO4LLw+uXFWKD2P9LgtJ/Cxdb9LRi1OZ5Qrw/jj173zf/g+ +wpItjoakgAzerHxKAPj3DB8iHFfPq+3MfdY36SZHT0GOU7QIhnYULKWWuVfexx25 +VtgdGsmL9jwfAftzh00aCIej9zi2eSfGYfcIeRlSh9wvoYldrZbRvLTeMbW+YznW +kH4W9taCGofrq/t8tN0beh9B7z2hMGxOLLnsxu3gQc2KIUqU5l1myL0rVncvSwZw +ppQudZYtRyzmLOOm9PEvJHWvgu6KQBj5F24xrwIDAQABAoIBAQC0BgOMJMqjkxAd +POxvhYUjoXhr7bDuGNKB5H38bNrto/aUPwSdQKilPPhUe1yyOCqYZwDJ06222aP2 +nIXooX+QX0EZtQHM6GhSjwByI78/kl/IQf30dCEMtpue7wqEn/ry4vooSiwkVsgm +/cPX811kWS2JgHq2/7JRI8GVgzu4m/wLtOVUIUiSG/zNZWx/ThEvvE/528z5MZG5 +zGuQobHH+zfGYqk9IABcpNMH+4S353oPXAej2bCsQU6x+alM5z0fi+PuWIWtaDIb +e/Va9WN2fghXF5lxu/+sCv8QkoPotbRfh0nLO0nTt4MUIFR0X/mVXbVWn+5SBhWC +YUgcjychAoGBAOtLKKqkYzuOIyB2E3b7dPJ1XuzHOXj0Co5DoVNNs8TyEggoQPuj +cTLUQaIN+M+MyNmtLi4GaF1dXRrJg7qZoJ681Vz0P+w+pso1UTQcja5G8iOwiKAD +MIkyH9t9iW8yDN+J0dEzTqAgOPIDxkwDWuvwvsBleJ2EAV6qdecjLpIRAoGBAOHW +0NGHYe4GCbt/gA5UVUYXehx9mckcLwyZJJThjTZXYr1kglRYa4de5YRMk9oPCHUu +ODKqxL8CTcKyIijj1fJGDVcqTPFXlS4UZ31RLMvVnDaMID7V2zx+wxJ9onwhj798 +1k3fVahH2vXOFH9AogeHKDNyD1RdwDNOhBy95Me/AoGALV+bAf0dXbi1MWdTrZgk +HzVfDs4EWTzGZFTKYWQUjKAZthT9IwmLpL+lwHhtSKjfeoqY4ys9KPP+JlJB4tQJ +U1Ma2ggH46jZRRkvBZuT/s2TmCpMzn6O94YA+rSkshq2vMy491yrhtlv4cu0i6gB ++om8XyGyNr3j/btlbSMtseECgYB66UL1Bk2SEc8yMI4tPlC6uQRIhUMxZRlmLeLu +9GK6dIzUruMPrJ+5KTiY7GR7hTsBK4qCaNZzbnmLwQ8+WeGS3fVcvzTpFNWoIorA +dXF/7l36ggD6scGEByl74syP6mQlv3eTIj2oPJM6vFIDf9WvayvB9A3LyMpWIiFc +0yy0WQKBgQDCPCUvQhiOJyQ63n3pjFl5/YOtadl9KUD/CmdyUkCt69QoFgG0wTAV +qalC9sysLQ1QI8A8GHNoNPjqMi7SWvzSgYN9TDRjS5GRlH13EALzP7AhWJWDoLYU +9DXNAEQrPMtX4Lzre7FmrYqEYqwdcac+vyXVgDA7ti1LhDhj8mm3Sg== +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDPkn9/nvYf3lz0MNkutdPSSaPK3OeS+e1I6PGYkzjh8vwNDPY0g0WLhrBM1gPV4gjNo7gsvD65cVYoPY/0uC0n8LF1v0tGLU5nlCvD+OPXvfN/+D7Cki2OhqSADN6sfEoA+PcMHyIcV8+r7cx91jfpJkdPQY5TtAiGdhQspZa5V97HHblW2B0ayYv2PB8B+3OHTRoIh6P3OLZ5J8Zh9wh5GVKH3C+hiV2tltG8tN4xtb5jOdaQfhb21oIah+ur+3y03Rt6H0HvPaEwbE4suezG7eBBzYohSpTmXWbIvStWdy9LBnCmlC51li1HLOYs46b08S8kda+C7opAGPkXbjGv`), + output: []byte(`PuTTY-User-Key-File-3: ssh-rsa +Encryption: none +Comment: teleport-generated-ppk +Public-Lines: 6 +AAAAB3NzaC1yc2EAAAADAQABAAABAQDPkn9/nvYf3lz0MNkutdPSSaPK3OeS+e1I +6PGYkzjh8vwNDPY0g0WLhrBM1gPV4gjNo7gsvD65cVYoPY/0uC0n8LF1v0tGLU5n +lCvD+OPXvfN/+D7Cki2OhqSADN6sfEoA+PcMHyIcV8+r7cx91jfpJkdPQY5TtAiG +dhQspZa5V97HHblW2B0ayYv2PB8B+3OHTRoIh6P3OLZ5J8Zh9wh5GVKH3C+hiV2t +ltG8tN4xtb5jOdaQfhb21oIah+ur+3y03Rt6H0HvPaEwbE4suezG7eBBzYohSpTm +XWbIvStWdy9LBnCmlC51li1HLOYs46b08S8kda+C7opAGPkXbjGv +Private-Lines: 14 +AAABAQC0BgOMJMqjkxAdPOxvhYUjoXhr7bDuGNKB5H38bNrto/aUPwSdQKilPPhU +e1yyOCqYZwDJ06222aP2nIXooX+QX0EZtQHM6GhSjwByI78/kl/IQf30dCEMtpue +7wqEn/ry4vooSiwkVsgm/cPX811kWS2JgHq2/7JRI8GVgzu4m/wLtOVUIUiSG/zN +ZWx/ThEvvE/528z5MZG5zGuQobHH+zfGYqk9IABcpNMH+4S353oPXAej2bCsQU6x ++alM5z0fi+PuWIWtaDIbe/Va9WN2fghXF5lxu/+sCv8QkoPotbRfh0nLO0nTt4MU +IFR0X/mVXbVWn+5SBhWCYUgcjychAAAAgQDh1tDRh2HuBgm7f4AOVFVGF3ocfZnJ +HC8MmSSU4Y02V2K9ZIJUWGuHXuWETJPaDwh1LjgyqsS/Ak3CsiIo49XyRg1XKkzx +V5UuFGd9USzL1Zw2jCA+1ds8fsMSfaJ8IY+/fNZN31WoR9r1zhR/QKIHhygzcg9U +XcAzToQcveTHvwAAAIEA60soqqRjO44jIHYTdvt08nVe7Mc5ePQKjkOhU02zxPIS +CChA+6NxMtRBog34z4zI2a0uLgZoXV1dGsmDupmgnrzVXPQ/7D6myjVRNByNrkby +I7CIoAMwiTIf232JbzIM34nR0TNOoCA48gPGTANa6/C+wGV4nYQBXqp15yMukhEA +AACAJ2iqIoXMYc0w3sXBQJ2BJyRYFBlZ0Czrz7xZEaBXrK5BcZjCARnmAp2Hfuvx +i0lz0PHAz9f6hpjZuLEGLO7f3kGMcyEquYd89FHvP1yLxggYiXGKNDYSDZRK8Yy7 +MipqcnT4j5zDuFi744aO5fIchKp02z+ttGVt/i5zuGNh+do= +Private-MAC: a9b12c6450e46fd7abbaaff5841f8a64f9597c7b2b59bd69d6fd3ceee0ca61ea +`), + }, + { + desc: "invalid private key", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3U4OOAi+F1Ct1n8HZIs1P39CWB0mKLvshouuklenZug27SuI +14rjE+hOTNHYz/Pkvk5mmKuIdegMCe8FHAF6chygcEC9BDkowLO+2+f3sazGsu4A +9H4pDuUkuIM9MwmZV7A4TJ19rRAgha+6JKKR5KeEosfiLvAtOu2Pjqz8ZrOrUUqQ +1AJ71SkWMPTJFksTNmgaH7a0SgJ4vVYMlYIAeyoAgqn6Qvu5Kez5ROfeKD4zys/+ +iFenrgbJrC38GNe2rxtb8/gfy03023FlPAQjGd1VLjxm8jhcJFqgM+uHTGRckgjv +d+VIkCbvTwpPWvvZxQcRtk073P9G8xpiNz2qbwIDAQABAoIBAQCFv37obqA0BxaI +5AzbvyZXUdoO1s8RH0I7rn+7Ai6yCvXnMMBrRA0pIuTvmIOoaoZ8XXW0HzdByxQ7 +jLFR07Lk9Fgif328566xh/B5hyAzyW/tA9qf6P93eRVQTkDWb561WFMuOqCRz4VY +RnQBYB88SeHnX1Zbd9xeGOUCHZoNlrilVpgjscGcFNxyDP72qvI79z1vV+R6dhaf +YI2v1D6aqx9qM988ytOokNi79wYvSUxqitz3IOD5nBd9ZNBC0fDeVmHqqbHSvLrr +LouF7PiUuVA2LaWfVCy5dVtLkS16qbsfqzUA4B8Eg/oF0vPpJ7QMVxKI5j2//ScL +lQ9h6gUBAoGBAOQ0t9gGuHKOMcp3H9C2fzNVbbWTubJoUyzFGyx+U2aJ4byRbxS3 +5d9cVu1GpS2ZgW6izCmxTG61Q0qQd4iT8e5cnFRU1Q3aK29TTK5hptthknXwKkVN +aUtlYKRM3TPYeTJ3WMQCY/Lzm2uVhT2ZGkpu0NaA5qiWllyPm7HlyQA/AoGBAPhC +bzioaPlqzwNKtHCsDSyeXsxU1aJCuMCIcgOB1yzmaaeL95CwMouMgouFyQ/CtLtO +cQEjymGzVynwC15s1vh1nCOWlQCx6Cjs9ko9bmecqziyyWg94gn82yLU7gClQH6v +ezQ1n7/pb1DO/8dytO3+BZKSQH9lobzravGTcnRAoGAL8nKZfaiUXrtelSP2Qke; +ggV1v/x7epzWLh3ontylYmelWfOqq1AHV0ri+TU+CdqHfD+jOWfjdZuHx+mQ3oz8 +sMm8Avzw0MHLLrjm6e2RH4fDP+dXMsQgy9Ui88UU3XKLjsHnWMSXYZ0aAuGA0XFq +TAQAv6qmos9GFYQNOqe/+8kCgYEAv88H69eae5J9bTKr5R3Zc+7MmZy2Do70hbUm +OfV4lbVUTmJDHWQ1OUKPnlL4fJfX4Zwquo23kPLqVnmjnwoCsabUw15Vs1rBX9Vt +mQCLq7wNQlpIaKTfXw4hFXFkjdUf1oIKXGEiSK8mk+s9kKepDRlnsXklnUcbpRri +xQQLF/ECgYAmKBSQtPuyA9d3dAZj96HhYZzDjD2EtAhSUyx31vgqr8C7mmShQXLh +kFap4eAldBxySXp/5af7H1Xf4BIfbbc1prMM1vIRFTN6l6rbircak7bb9a/dgWmX +iukFsFq0G0Y2zt9oHOB7pKV/Kff4o1WQ0hcCBD6pZGhbsVxXBi4Oaw== +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDdTg44CL4XUK3WfwdkizU/f0JYHSYou+yGi66SV6dm6DbtK4jXiuMT6E5M0djP8+S+TmaYq4h16AwJ7wUcAXpyHKBwQL0EOSjAs77b5/exrMay7gD0fikO5SS4gz0zCZlXsDhMnX2tECCFr7okopHkp4Six+Iu8C067Y+OrPxms6tRSpDUAnvVKRYw9MkWSxM2aBoftrRKAni9VgyVggB7KgCCqfpC+7kp7PlE594oPjPKz/6IV6euBsmsLfwY17avG1vz+B/LTfTbcWU8BCMZ3VUuPGbyOFwkWqAz64dMZFySCO935UiQJu9PCk9a+9nFBxG2TTvc/0bzGmI3Papv`), + wantErr: true, + }, + { + desc: "invalid public key", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAve2um90K1SkpJD1vcjm2zUYUh5ZU7q1cmO7F0J/6MCEcq3vH +fDPpPZ4uGLB9jPKzs6FYWhwFNW2oAsDvWSrwwxy5gl1dAdqp1wIm86gafShR0se5 +rSdhWKP40H2lHOysRC5Jr8cvVLgflvZ4PDMqr/63BKwwkT1vN2PuenYRAAIT77X9 +O0fumGQPKIxRGn5OPKEt1LzQ0+e/QlWrqZwzWDx5jqG3jbxibdcR/mHS60XdvusL +UqxxWPjhVlDsKfvh2lt5sqsjulWW/GyNtlCfaTn2uu0nV8nbT2OvEO+oM/uyHos5 +7aIyePcOzCVM4dug6xJinqYTaVUsskKjPGUV6QIDAQABAoIBAQCBfis9k6DOIukt +D0IL5DOxk2Vt6F5x+PsYPjva+SfwZrMQbC1fjlkpLM8LAFIpplRFVe1SSqZ2fhQ+ +BGNsLS3IKa6FprhCCl8f/BSoreWZjcLz7j63QxFJCUscg33u0aLGPbT5xtmLbpoD +KHpjuRMSuZz475mRfQx1/IldL2B52sIAD6XRTgFoRG+mLu2iNVvuE0RVbASiyOUs +lVwrGRI+5GuH8G6fDCJqpYzcm/S8VXmQc2jrbo/gQ76MkFxULqEMzadjN+XMXms7 +pGZLX6Hatubn1kmhl8l6+1GYLf1HVmWXoL+hgWwbfIn6WV9y/xpnoeoJfWcFLJli +yABDx/mBAoGBAPhw3thyEP+5jdH2n1vz4X76yUbNJXaJGXozdoNfFKNOrYjFCLnD +CzHJEQmDJoFCtF6TwgvFb90HNvtNLkbC81yotQ8rfDzNTlixUhycaSsCJBqw0loU +wXoYQZiXpbfqT9Y7x7pwMxzRtkQYvyaowc7qF1xwJHhyCjDx38jAGnZxAoGBAMO1 +DXUpca09h+FujJkziyJStYq0YKqsuKXW7CuAq2iY70lzhv+SIPErqcYIWwi8JNv9 +EwBlEmSltFyGtxpeIVl6MJTil3vQ6eOSBCwt/E1YKvZoLv6mDf52Lc/wKtlecRPG +Q7G2C1ioTD9lDiYysUDmkpfitiatFwEj+y606wL5AoGBAMlQJLM9Ets1D19QuWb4 +YwPS0aBGgZHgnD1yUBk5xW5jRajrCBwGmR6Zb+3GUUAyvhdZIccKEJAI1Zuiudnr +BOpTZovJT92w+0hRP1khwPJxxLHAEGOgJ/r4hsbQMx+phVHylPBVFIXIxSm+5726 +x3kUJSPpVxQmTG3GwPBaAddxAoGALq+4QCTc22j8S0jl/X4QSOXWLPqOvOhrPBSj +TlVpjpA9NRZ8M+eWODIkU/uWS+UmHdyndcamtp/ZAOGaOI4QApplkH7liEH0Kbeh +izCFKaZIyXNdEp5mZDepAhvW/PfMnd0ENRaqakHrvovK7k3VfxgCDH2m2l8cR8df +mmrKTXECgYEA300gTnT46pMU1Wr1Zq4vGauWzk3U4J9HUu3vNy+sg4EEZ9CoiNTw +0a3f8u8gNQjB30koGW/5jYex3fUcnjTPqEGaiiGjI4oxMhquzqkVQ8FwnBAXJgT8 +nQVO8MZw8iFeSap0ILum8t60sp1/u9aCWJbjPtb/fhx0q7SLdjFEw8s= +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`AAAAB3NzaC1yc2EAAAADAQABAAABAQC9806b3QrVKSkkPW9yObbNRhSHllTurVyY7sXQn/owIRyre8d8M+k9ni4YsH2M8rOzoVhaHAU1bagCwO9ZKvDDHLmCXV0B2qnXAibzqBp9KFHSx7mtJ2FYo/jQfaUc7KxELkmvxy9UuB+W9ng8Myqv/rcErDCRPW83Y+56dhEAAhPvtf07R+6YZA8ojFEafk48oS3UvNDT579CVaupnDNYPHmOobeNvGJt1xH+YdLrRd2+6wtSrHFY+OFWUOwp++HaW3myqyO6VZb8bI22UJ9pOfa67SdXydtPY68Q76gz+7IeizntojJ49w7MJUzh26DrEmKephNpVSyyQqM8ZRXp`), + wantErr: true, + }, + { + desc: "private and public keys too short", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAve2um90K1SkpJD1vcjm2zUYUh5ZU7q1cmO7F0J/6MCEcq3vH +fDPpPZ4uGLB9jPKzs6FYWhwFNW2oAsDvWSrwwxy5gl1dAdqp1wIm86gafShR0se5 +rSdhWKP40H2lHOysRC5Jr8cvVLgflvZ4PDMqr/63BKwwkT1vN2PuenYRAAIT77X9 +O0fumGQPKIxRGn5OPKEt1LzQ0+e/QlWrqZwzWDx5jqG3jbxibdcR/mHS60XdvusL +UqxxWPjhVlDsKfvh2lt5sqsjulWW/GyNtlCfaTn2uu0nV8nbT2OvEO+oM/uyHos5 +7aIyePcOzCVM4dug6xJinqYTaVUsskKjPGUV6QIDAQABAoIBAQCBfis9k6DOIukt +D0IL5DOxk2Vt6F5x+PsYPjva+SfwZrMQbC1fjlkpLM8LAFIpplRFVe1SSqZ2fhQ+ +BGNsLS3IKa6FprhCCl8f/BSoreWZjcLz7j63QxFJCUscg33u0aLGPbT5xtmLbpoD +KHpjuRMSuZz475mRfQx1/IldL2B52sIAD6XRTgFoRG+mLu2iNVvuE0RVbASiyOUs +lVwrGRI+5GuH8G6fDCJqpYzcm/S8VXmQc2jrbo/gQ76MkFxULqEMzadjN+XMXms7 +pGZLX6Hatubn1kmhl8l6+1GYLf1HVmWXoL+hgWwbfIn6WV9y/xpnoeoJfWcFLJli +yABDx/mBAoGBAPhw3thyEP+5jdH2n1vz4X76yUbNJXaJGXozdoNfFKNOrYjFCLnD +CzHJEQmDJoFCtF6TwgvFb90HNvtNLkbC81yotQ8rfDzNTlixUhycaSsCJBqw0loU +wXoYQZiXpbfqT9Y7x7pwMxzRtkQYvyaowc7qF1xwJHhyCjDx38jAGnZxAoGBAMO1 +DXUpca09h+FujJkziyJStYq0YKqsuKXW7CuAq2iY70lzhv+SIPErqcYIWwi8JNv9 +EwBlEmSltFyGtxpeIVl6MJTil3vQ6eOSBCwt/E1YKvZoLv6mDf52Lc/wKtlecRPG +Q7G2C1ioTD9lDiYysUDmkpfitiatFwEj+y606wL5AoGBAMlQJLM9Ets1D19QuWb4 +YwPS0aBGgZHgnD1yUBk5xW5jRajrCBwGmR6Zb+3GUUAyvhdZIccKEJAI1Zuiudnr +BOpTZovJT92w+0hRP1khwPJxxLHAEGOgJ/r4hsbQMx+phVHylPBVFIXIxSm+5726 +x3kUJSPpVxQmTG3GwPBaAddxAoGALq+4QCTc22j8S0jl/X4QSOXWLPqOvOhrPBSj +TlVpjpA9NRZ8M+eWODIkU/uWS+UmHdyndcamtp/ZAOGaOI4QApplkH7liEH0Kbeh +izCFKaZIyXNdEp5mZDepAhvW/PfMnd0ENRaqakHrvovK7k3VfxgCDH2m2l8cR8df +mmrKTXECgYEA300gTnT46pMU1Wr1Zq4vGauWzk3U4J9HUu3vNy+sg4EEZ9CoiNTw +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`AAAAB3NzaC1yc2EAAAADAQABAAABAQC9806b3QrVKSkkPW9yObbNRhSHllTurVyY7sXQn/owIRyre8d8M+k9ni4YsH2M8rOzoVhaHAU1bagCwO9ZKvDDHLmCXV0B2qnXAibzqBp9KFHSx7mtJ2FYo/jQfaUc7KxELkmvxy9UuB+W9ng8Myqv/rcErDCRPW83Y+56dhEAAhPvtf07R+6YZA8ojFEafk48oS3UvNDT579CVaupnDNYPHmOobeNvGJt1xH+YdLrRd2+6wtSrHFY+OFWUOwp++HaW3myqyO6VZb8bI22UJ9pOfa67SdXydtPY68Q76gz+7IeizntojJ49w7MJUzh26DrEmKephNpVSyyQqM8`), + wantErr: true, + }, + { + desc: "private and public keys too long", + priv: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAz5J/f572H95c9DDZLrXT0kmjytznkvntSOjxmJM44fL8DQz2 +NINFi4awTNYD1eIIzaO4LLw+uXFWKD2P9LgtJ/Cxdb9LRi1OZ5Qrw/jj173zf/g+ +wpItjoakgAzerHxKAPj3DB8iHFfPq+3MfdY36SZHT0GOU7QIhnYULKWWuVfexx25 +VtgdGsmL9jwfAftzh00aCIej9zi2eSfGYfcIeRlSh9wvoYldrZbRvLTeMbW+YznW +kH4W9taCGofrq/t8tN0beh9B7z2hMGxOLLnsxu3gQc2KIUqU5l1myL0rVncvSwZw +ppQudZYtRyzmLOOm9PEvJHWvgu6KQBj5F24xrwIDAQABAoIBAQC0BgOMJMqjkxAd +POxvhYUjoXhr7bDuGNKB5H38bNrto/aUPwSdQKilPPhUe1yyOCqYZwDJ06222aP2 +nIXooX+QX0EZtQHM6GhSjwByI78/kl/IQf30dCEMtpue7wqEn/ry4vooSiwkVsgm +/cPX811kWS2JgHq2/7JRI8GVgzu4m/wLtOVUIUiSG/zNZWx/ThEvvE/528z5MZG5 +zGuQobHH+zfGYqk9IABcpNMH+4S353oPXAej2bCsQU6x+alM5z0fi+PuWIWtaDIb +e/Va9WN2fghXF5lxu/+sCv8QkoPotbRfh0nLO0nTt4MUIFR0X/mVXbVWn+5SBhWC +YUgcjychAoGBAOtLKKqkYzuOIyB2E3b7dPJ1XuzHOXj0Co5DoVNNs8TyEggoQPuj +cTLUQaIN+M+MyNmtLi4GaF1dXRrJg7qZoJ681Vz0P+w+pso1UTQcja5G8iOwiKAD +MIkyH9t9iW8yDN+J0dEzTqAgOPIDxkwDWuvwvsBleJ2EAV6qdecjLpIRAoGBAOHW +0NGHYe4GCbt/gA5UVUYXehx9mckcLwyZJJThjTZXYr1kglRYa4de5YRMk9oPCHUu +ODKqxL8CTcKyIijj1fJGDVcqTPFXlS4UZ31RLMvVnDaMID7V2zx+wxJ9onwhj798 +1k3fVahH2vXOFH9AogeHKDNyD1RdwDNOhBy95Me/AoGALV+bAf0dXbi1MWdTrZgk +HzVfDs4EWTzGZFTKYWQUjKAZthT9IwmLpL+lwHhtSKjfeoqY4ys9KPP+JlJB4tQJ +U1Ma2ggH46jZRRkvBZuT/s2TmCpMzn6O94YA+rSkshq2vMy491yrhtlv4cu0i6gB ++om8XyGyNr3j/btlbSMtseECgYB66UL1Bk2SEc8yMI4tPlC6uQRIhUMxZRlmLeLu +9GK6dIzUruMPrJ+5KTiY7GR7hTsBK4qCaNZzbnmLwQ8+WeGS3fVcvzTpFNWoIorA +dXF/7l36ggD6scGEByl74syP6mQlv3eTIj2oPJM6vFIDf9WvayvB9A3LyMpWIiFc +0yy0WQKBgQDCPCUvQhiOJyQ63n3pjFl5/YOtadl9KUD/CmdyUkCt69QoFgG0wTAV +qalC9sysLQ1QI8A8GHNoNPjqMi7SWvzSgYN9TDRjS5GRlH13EALzP7AhWJWDoLYU +9DXNAEQrPMtX4Lzre7FmrYqEYqwdcac+vyXVgDA7ti1LhDhj8mm3Sg12371237== +-----END RSA PRIVATE KEY----- +`), + pub: []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDPkn9/nvYf3lz0MNkutdPSSaPK3OeS+e1I6PGYkzjh8vwNDPY0g0WLhrBM1gPV4gjNo7gsvD65cVYoPY/0uC0n8LF1v0tGLU5nlCvD+OPXvfN/+D7Cki2OhqSADN6sfEoA+PcMHyIcV8+r7cx91jfpJkdPQY5TtAiGdhQspZa5V97HHblW2B0ayYv2PB8B+3OHTRoIh6P3OLZ5J8Zh9wh5GVKH3C+hiV2tltG8tN4xtb5jOdaQfhb21oIah+ur+3y03Rt6H0HvPaEwbE4suezG7eBBzYohSpTmXWbIvStWdy9LBnCmlC51li1HLOYs46b08S8kda+C7opAGPkXbjGvkj121`), + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + if tc.wantErr { + _, err := ppk.ConvertToPPK(tc.priv, tc.pub) + require.Error(t, err) + } else { + output, err := ppk.ConvertToPPK(tc.priv, tc.pub) + require.NoError(t, err) + require.Equal(t, output, tc.output) + } + }) + } +} diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index d7e4eb56eb2..42f63e05625 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/teleport/api/identityfile" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/sshutils" + "github.com/gravitational/teleport/api/utils/sshutils/ppk" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/services" @@ -72,6 +73,8 @@ type Key struct { Priv []byte `json:"Priv,omitempty"` // Pub is a public key Pub []byte `json:"Pub,omitempty"` + // PPK is a PuTTY PPK-formatted keypair + PPK []byte `json:"PPK,omitempty"` // Cert is an SSH client certificate Cert []byte `json:"Cert,omitempty"` // TLSCert is a PEM encoded client TLS x509 certificate. @@ -101,9 +104,15 @@ func NewKey() (key *Key, err error) { return nil, trace.Wrap(err) } + ppkFile, err := ppk.ConvertToPPK(priv, pub) + if err != nil { + return nil, trace.Wrap(err) + } + return &Key{ Priv: priv, Pub: pub, + PPK: ppkFile, KubeTLSCerts: make(map[string][]byte), DBTLSCerts: make(map[string][]byte), }, nil diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 47876a3c252..754b625fca3 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -24,11 +24,13 @@ import ( osfs "io/fs" "os" "path/filepath" + "runtime" "strings" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/lib/auth" @@ -140,6 +142,11 @@ func (fs *FSLocalKeyStore) AddKey(key *Key) error { if err := fs.writeBytes(key.TLSCert, fs.tlsCertPath(key.KeyIndex)); err != nil { return trace.Wrap(err) } + if runtime.GOOS == constants.WindowsOS { + if err := fs.writeBytes(key.PPK, fs.PPKFilePath(key.KeyIndex)); err != nil { + return trace.Wrap(err) + } + } // Store per-cluster key data. if len(key.Cert) > 0 { @@ -197,6 +204,10 @@ func (fs *FSLocalKeyStore) DeleteKey(idx KeyIndex) error { fs.sshCAsPath(idx), fs.tlsCertPath(idx), } + // we also need to delete the extra PuTTY-formatted .ppk file when running on Windows + if runtime.GOOS == constants.WindowsOS { + files = append(files, fs.PPKFilePath(idx)) + } for _, fn := range files { if err := os.Remove(fn); err != nil { return trace.ConvertSystemError(err) @@ -530,6 +541,11 @@ func (fs *fsLocalNonSessionKeyStore) sshCertPath(idx KeyIndex) string { return keypaths.SSHCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName) } +// PPKFilePath returns the PPK (PuTTY-formatted) keypair path for the given KeyIndex. +func (fs *fsLocalNonSessionKeyStore) PPKFilePath(idx KeyIndex) string { + return keypaths.PPKFilePath(fs.KeyDir, idx.ProxyHost, idx.Username) +} + // sshCAsPath returns the SSH CA certificates path for the given KeyIndex. func (fs *fsLocalNonSessionKeyStore) sshCAsPath(idx KeyIndex) string { return keypaths.SSHCAsPath(fs.KeyDir, idx.ProxyHost, idx.Username)