diff --git a/api/utils/keys/privatekey.go b/api/utils/keys/privatekey.go index 57da1fa0474fb..1255acb6f7df2 100644 --- a/api/utils/keys/privatekey.go +++ b/api/utils/keys/privatekey.go @@ -173,15 +173,8 @@ func TLSCertificateForSigner(signer crypto.Signer, certPEMBlock []byte) (tls.Cer // PPKFile returns a PuTTY PPK-formatted keypair func (k *PrivateKey) PPKFile() ([]byte, error) { - rsaKey, ok := k.Signer.(*rsa.PrivateKey) - if !ok { - 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 { - return nil, trace.Wrap(err) - } - return ppkFile, nil + ppkFile, err := ppk.ConvertToPPK(k.Signer, k.sshPub) + return ppkFile, trace.Wrap(err) } // SoftwarePrivateKeyPEM returns the PEM encoding of the private key. If the key diff --git a/api/utils/sshutils/ppk/ppk.go b/api/utils/sshutils/ppk/ppk.go index 06048ee567cac..c25dbd0ec1ce6 100644 --- a/api/utils/sshutils/ppk/ppk.go +++ b/api/utils/sshutils/ppk/ppk.go @@ -21,6 +21,9 @@ package ppk import ( "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" "crypto/hmac" "crypto/rsa" "crypto/sha256" @@ -28,125 +31,49 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "io" "math/big" "github.com/gravitational/trace" + "golang.org/x/crypto/ssh" +) - "github.com/gravitational/teleport/api/constants" +const ( + 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" ) -// ConvertToPPK takes a regular RSA-formatted keypair and converts it into the PPK file format used by the PuTTY SSH client. +// ConvertToPPK takes a regular SSH 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 - // 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") +func ConvertToPPK(privateKey crypto.Signer, pub ssh.PublicKey) ([]byte, error) { + var ppkPrivateKey bytes.Buffer + if err := writePrivateKey(&ppkPrivateKey, privateKey); err != nil { + return nil, trace.Wrap(err) } - 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) + ppkPrivateKeyBase64 := base64.StdEncoding.EncodeToString(ppkPrivateKey.Bytes()) + ppkPublicKeyBase64 := base64.StdEncoding.EncodeToString(pub.Marshal()) - // 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) + // Compute the anti-tampering MAC. + macString, err := computeMAC(pub, ppkPrivateKey.Bytes()) if err != nil { - return nil, trace.Errorf("could not base64-decode public key: %v, got %v bytes successfully", err, n) + return nil, trace.Wrap(err) } - 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 + + // 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, "PuTTY-User-Key-File-3: %v\n", pub.Type()) 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) + // Chunk the base64-encoded public key into 64-character length lines. + chunkedPublicKey := chunk(ppkPublicKeyBase64, 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) + // Chunk the PPK-formatted private key into 64-character length lines. + chunkedPrivateKey := chunk(ppkPrivateKeyBase64, 64) fmt.Fprintf(ppk, "Private-Lines: %v\n", len(chunkedPrivateKey)) for _, r := range chunkedPrivateKey { fmt.Fprintf(ppk, "%s\n", r) @@ -156,6 +83,135 @@ func ConvertToPPK(privateKey *rsa.PrivateKey, pub []byte) ([]byte, error) { return ppk.Bytes(), nil } +// computeMAC computes an anti-tampering MAC which is made up of various values which appear in the PPK file. +// Copied from Section C.2 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. +func computeMAC(pub ssh.PublicKey, rawPrivateKey []byte) (string, error) { + // Generate the MAC using HMAC-SHA-256. As per the PPK spec, the key for the + // MAC is blank when the PPK file is unencrypted. + var hmacKey []byte + hmacHash := hmac.New(sha256.New, hmacKey) + if err := writeRFC4251Strings(hmacHash, + []byte(pub.Type()), // the algorithm-name header field + []byte(encryptionType), // the encryption-type header field + []byte(fileComment), // the key-comment-string header field + pub.Marshal(), // the binary public-key data + rawPrivateKey, // the plaintext of the binary private key data + ); err != nil { + return "", trace.Wrap(err) + } + return hex.EncodeToString(hmacHash.Sum(nil)), nil +} + +func writePrivateKey(w io.Writer, signer crypto.Signer) error { + switch k := signer.(type) { + case *rsa.PrivateKey: + return trace.Wrap(writeRSAPrivateKey(w, k)) + case *ecdsa.PrivateKey: + return trace.Wrap(writeECDSAPrivateKey(w, k)) + case ed25519.PrivateKey: + return trace.Wrap(writeEd25519PrivateKey(w, k)) + } + return trace.BadParameter("unsupported private key type %T", signer) +} + +func writeRSAPrivateKey(w io.Writer, privateKey *rsa.PrivateKey) 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 + // 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. + + // For some reason what PuTTY names 'P' is Primes[1] to Go, and what PuTTY + // names 'Q' is Primes[0] to Go. RSA keys stored in this format are + // expected to have exactly two prime factors. + P, Q := privateKey.Primes[1], privateKey.Primes[0] + // The multiplicative inverse of q modulo p. + iqmp := new(big.Int).ModInverse(Q, P) + return trace.Wrap(writeRFC4251Mpints(w, privateKey.D, P, Q, iqmp)) +} + +func writeECDSAPrivateKey(w io.Writer, privateKey *ecdsa.PrivateKey) error { + // https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk + // NIST elliptic-curve keys are stored using one of the following + // algorithm-name values, each corresponding to a different elliptic curve + // and key size: + // - ‘ecdsa-sha2-nistp256’ + // - ‘ecdsa-sha2-nistp384’ + // - ‘ecdsa-sha2-nistp521’ + // The public key data has already provided the public elliptic curve point. The private key stores: + // mpint: the private exponent, which is the discrete log of the public point. + // + // crypto/ecdsa calls this D. + return trace.Wrap(writeRFC4251Mpint(w, privateKey.D)) +} + +func writeEd25519PrivateKey(w io.Writer, privateKey ed25519.PrivateKey) error { + // https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk + // EdDSA elliptic-curve keys are stored using one of the following + // algorithm-name values, each corresponding to a different elliptic curve + // and key size: + // - ‘ssh-ed25519’ + // - ‘ssh-ed448’ + // The public key data has already provided the public elliptic curve point. The private key stores: + // mpint: the private exponent, which is the discrete log of the public point. + // + // crypto/ed25519 calls the private exponent the seed. + return trace.Wrap(writeRFC4251Mpint(w, new(big.Int).SetBytes(privateKey.Seed()))) +} + +func writeRFC4251Mpints(w io.Writer, ints ...*big.Int) error { + for _, n := range ints { + if err := writeRFC4251Mpint(w, n); err != nil { + return trace.Wrap(err) + } + } + return nil +} + +// writeRFC4251Mpint writes a stream of bytes representing a big-endian +// mixed-precision integer (a big.Int in Go) in the 'mpint' format described in +// RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5) +func writeRFC4251Mpint(w io.Writer, n *big.Int) error { + 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 n.Sign() == 1 && b[0]&0x80 != 0 { + b = append([]byte{0}, b...) + } + return trace.Wrap(writeRFC4251String(w, b)) +} + +func writeRFC4251Strings(w io.Writer, strs ...[]byte) error { + for _, s := range strs { + if err := writeRFC4251String(w, s); err != nil { + return trace.Wrap(err) + } + } + return nil +} + +// writeRFC4251String writes a stream of bytes prepended with a big-endian +// uint32 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 writeRFC4251String(w io.Writer, s []byte) error { + // Write a uint32 with the length of the byte stream to the buffer. + if err := binary.Write(w, binary.BigEndian, uint32(len(s))); err != nil { + return trace.Wrap(err) + } + // Write the byte stream representing of the rest of the data to the buffer. + if _, err := io.Copy(w, bytes.NewReader(s)); err != nil { + return trace.Wrap(err) + } + return 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 @@ -173,34 +229,3 @@ func chunk(s string, size int) []string { } 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 index 5953787ca427b..97a1d4f682ed7 100644 --- a/api/utils/sshutils/ppk/ppk_test.go +++ b/api/utils/sshutils/ppk/ppk_test.go @@ -18,13 +18,11 @@ limitations under the License. package ppk_test import ( - "crypto/rsa" "testing" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/utils/keys" - "github.com/gravitational/teleport/api/utils/sshutils/ppk" ) func TestConvertToPPK(t *testing.T) { @@ -35,7 +33,7 @@ func TestConvertToPPK(t *testing.T) { output []byte }{ { - desc: "valid private and public keys 1", + desc: "RSA key 1", priv: []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA3U4OOAi+F1Ct1n8HZIs1P39CWB0mKLvshouuklenZug27SuI 14rjE+hOTNHYz/Pkvk5mmKuIdegMCe8FHAF6chygcEC9BDkowLO+2+f3sazGsu4A @@ -64,7 +62,6 @@ 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 @@ -94,7 +91,7 @@ Private-MAC: 2697903ac84b70273afc7adaa4e3ebb14536cdaf69654d40e3d46a5ba997ffb0 `), }, { - desc: "valid private and public keys 2", + desc: "RSA key 2", priv: []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAve2um90K1SkpJD1vcjm2zUYUh5ZU7q1cmO7F0J/6MCEcq3vH fDPpPZ4uGLB9jPKzs6FYWhwFNW2oAsDvWSrwwxy5gl1dAdqp1wIm86gafShR0se5 @@ -123,7 +120,6 @@ mmrKTXECgYEA300gTnT46pMU1Wr1Zq4vGauWzk3U4J9HUu3vNy+sg4EEZ9CoiNTw 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 @@ -153,7 +149,7 @@ Private-MAC: b5ede95d052e23815c8e8d816c758fb16370fc3178e1613fee61ec158900fd64 `), }, { - desc: "valid public and private keys 3", + desc: "RSA key 3", priv: []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAz5J/f572H95c9DDZLrXT0kmjytznkvntSOjxmJM44fL8DQz2 NINFi4awTNYD1eIIzaO4LLw+uXFWKD2P9LgtJ/Cxdb9LRi1OZ5Qrw/jj173zf/g+ @@ -182,7 +178,6 @@ 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 @@ -209,6 +204,49 @@ AACAJ2iqIoXMYc0w3sXBQJ2BJyRYFBlZ0Czrz7xZEaBXrK5BcZjCARnmAp2Hfuvx i0lz0PHAz9f6hpjZuLEGLO7f3kGMcyEquYd89FHvP1yLxggYiXGKNDYSDZRK8Yy7 MipqcnT4j5zDuFi744aO5fIchKp02z+ttGVt/i5zuGNh+do= Private-MAC: a9b12c6450e46fd7abbaaff5841f8a64f9597c7b2b59bd69d6fd3ceee0ca61ea +`), + }, + { + desc: "ed25519 key", + priv: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz +c2gtZWQyNTUxOQAAACBj4UfPX3B2yLRkt8ABGWiQGME1oY7N7K8yMTECt4HTvgAA +AIjWpv6D1qb+gwAAAAtzc2gtZWQyNTUxOQAAACBj4UfPX3B2yLRkt8ABGWiQGME1 +oY7N7K8yMTECt4HTvgAAAEBW11q/rO8oWVkJGVV0md/Q7MQMkoisjyqKdk/aFQpl +U2PhR89fcHbItGS3wAEZaJAYwTWhjs3srzIxMQK3gdO+AAAAAAECAwQF +-----END OPENSSH PRIVATE KEY-----`), + output: []byte(`PuTTY-User-Key-File-3: ssh-ed25519 +Encryption: none +Comment: teleport-generated-ppk +Public-Lines: 2 +AAAAC3NzaC1lZDI1NTE5AAAAIGPhR89fcHbItGS3wAEZaJAYwTWhjs3srzIxMQK3 +gdO+ +Private-Lines: 1 +AAAAIFbXWr+s7yhZWQkZVXSZ39DsxAySiKyPKop2T9oVCmVT +Private-MAC: 69e26c50e92d520bef9a19913b54b9585bcadbc3ba8eb01eadf95c9c4e5e5f4e +`), + }, + { + desc: "ecdsa key", + priv: []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNl +Y2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQR46dTQpQmnDizIvtPH +rQ9bOtCD73Jt98YCundWBx2wZxvtAi3OT15Ku/R65Qu2E/6psMdYeADta7DgKtmy +HT3AAAAAoKbt/b2m7f29AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy +NTYAAABBBHjp1NClCacOLMi+08etD1s60IPvcm33xgK6d1YHHbBnG+0CLc5PXkq7 +9HrlC7YT/qmwx1h4AO1rsOAq2bIdPcAAAAAhAObTnBS3qFRxz272PVnDJ37EVyH2 +Ryfdptn0Kw5TyRq7AAAAAAECAwQFBgc= +-----END OPENSSH PRIVATE KEY-----`), + output: []byte(`PuTTY-User-Key-File-3: ecdsa-sha2-nistp256 +Encryption: none +Comment: teleport-generated-ppk +Public-Lines: 3 +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHjp1NClCacO +LMi+08etD1s60IPvcm33xgK6d1YHHbBnG+0CLc5PXkq79HrlC7YT/qmwx1h4AO1r +sOAq2bIdPcA= +Private-Lines: 1 +AAAAIQDm05wUt6hUcc9u9j1Zwyd+xFch9kcn3abZ9CsOU8kauw== +Private-MAC: 6e788dafd452d27c17d062add28113d59d03a20898ea89046e3809fe38832861 `), }, } @@ -218,12 +256,7 @@ Private-MAC: a9b12c6450e46fd7abbaaff5841f8a64f9597c7b2b59bd69d6fd3ceee0ca61ea priv, err := keys.ParsePrivateKey(tc.priv) require.NoError(t, err) - rsaPriv, ok := priv.Signer.(*rsa.PrivateKey) - require.True(t, ok) - // Without this line, the linter thinks that "crypto/rsa" is unused... - require.IsType(t, &rsa.PrivateKey{}, rsaPriv) - - output, err := ppk.ConvertToPPK(rsaPriv, tc.pub) + output, err := priv.PPKFile() require.NoError(t, err) require.Equal(t, output, tc.output) }) diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 37e0a688b8116..250689ff97c28 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -217,9 +217,7 @@ func (fs *FSKeyStore) AddKeyRing(keyRing *KeyRing) error { // We only generate PPK files for use by PuTTY when running tsh on Windows. if runtime.GOOS == constants.WindowsOS { 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) { + if err != nil { fs.log.Debugf("Cannot convert private key to PPK-formatted keypair: %v", err) } else { if err := fs.writeBytes(ppkFile, fs.ppkFilePath(keyRing.KeyRingIndex)); err != nil { diff --git a/lib/puttyhosts/puttyhosts.go b/lib/puttyhosts/puttyhosts.go index e442e4105152f..9c794754ec3c6 100644 --- a/lib/puttyhosts/puttyhosts.go +++ b/lib/puttyhosts/puttyhosts.go @@ -20,6 +20,7 @@ package puttyhosts import ( "context" + "encoding/base64" "fmt" "regexp" "slices" @@ -29,7 +30,6 @@ import ( "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/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" @@ -214,7 +214,7 @@ func ProcessHostCAPublicKeys(tc *client.TeleportClient, cfContext context.Contex return nil, trace.Wrap(err) } - hostCAPublicKey := strings.TrimPrefix(strings.TrimSpace(string(ssh.MarshalAuthorizedKey(hostCABytes))), constants.SSHRSAType+" ") + hostCAPublicKey := base64.StdEncoding.EncodeToString(hostCABytes.Marshal()) hostCAPublicKeys[ca.GetName()] = append(hostCAPublicKeys[ca.GetName()], hostCAPublicKey) } } diff --git a/lib/puttyhosts/puttyhosts_test.go b/lib/puttyhosts/puttyhosts_test.go index 940a404483a84..724808cdb8f50 100644 --- a/lib/puttyhosts/puttyhosts_test.go +++ b/lib/puttyhosts/puttyhosts_test.go @@ -204,8 +204,10 @@ func TestFormatHostCAPublicKeysForRegistry(t *testing.T) { { inputMap: map[string][]string{ "teleport.example.com": { - `AAAAB3NzaC1yc2EAAAADAQABAAABAQDNbSbDa+bAjeH6wQPMfcUoyKHOTOwBRc1Lr+5Vy6aHOz+lWsovldH0r4mGFv2mLyWmqax18YVWG/YY+5um9y19SxlIHcAZI/uqnV7lAOhVkni87CGZ+Noww512dlrtczYZDc4735mSYxcSYQyRZywwXOfSqA0Euc6P2a0e03hcdROeJxx50xQcDw/wjreot5swiVHOvOGIIauekPswP58Z+F4goIFaFk5i5gDDBfX4mvtFV5AOkYQlk4hzmwJZ2JpphUQ33YbwhDrEPat2/mLf1tUk6aY8qHFqE9g5bjFjuLQxeva3Y5in49Zt+pg701TbBwS+R8wbuQqDM8b7VgEV`, - `AAAAB3NzaC1yc2EAAAADAQABAAABAQDm0PWl5llSpFArdHkXv8xXgsO9qEAbjvIAjMaoUbr79d03pBlmCCU7Zm3X9NkiLL7om2KLSE7AA0oQI+S+VgrDX17S327uj8M3hNZkfkbKGvzY5NS17DubpEEuAoF1r8Of7GKMbAmQ9d8dF8iNkREaJ+FT8g2JmGtRwmQGf8c0v2FCdz7SbChE9nUxk4Q8f1Qjhx8Pgjga/ntqkB+JpwATVvCxkd/ld0yzh9T0l90dV1TYYwnmWVpQzes1nbotQoMK8vUO20dWBEMWVMxXXp/P4OaztYGLmGJ9YP9upxq8IoSUdef7URUuJZGPWEyCQ0Mk6GRYJHvlX5cNOSHxYDBt`, + `AAAAB3NzaC1yc2EAAAADAQABAAABAQDNbSbDa+bAjeH6wQPMfcUoyKHOTOwBRc1Lr+5Vy6aHOz+lWsovldH0r4mGFv2mLyWmqax18YVWG/YY+5um9y19SxlIHcAZI/uqnV7lAOhVkni87CGZ+Noww512dlrtczYZDc4735mSYxcSYQyRZywwXOfSqA0Euc6P2a0e03hcdROeJxx50xQcDw/wjreot5swiVHOvOGIIauekPswP58Z+F4goIFaFk5i5gDDBfX4mvtFV5AOkYQlk4hzmwJZ2JpphUQ33YbwhDrEPat2/mLf1tUk6aY8qHFqE9g5bjFjuLQxeva3Y5in49Zt+pg701TbBwS+R8wbuQqDM8b7VgEV`, // RSA + `AAAAB3NzaC1yc2EAAAADAQABAAABAQDm0PWl5llSpFArdHkXv8xXgsO9qEAbjvIAjMaoUbr79d03pBlmCCU7Zm3X9NkiLL7om2KLSE7AA0oQI+S+VgrDX17S327uj8M3hNZkfkbKGvzY5NS17DubpEEuAoF1r8Of7GKMbAmQ9d8dF8iNkREaJ+FT8g2JmGtRwmQGf8c0v2FCdz7SbChE9nUxk4Q8f1Qjhx8Pgjga/ntqkB+JpwATVvCxkd/ld0yzh9T0l90dV1TYYwnmWVpQzes1nbotQoMK8vUO20dWBEMWVMxXXp/P4OaztYGLmGJ9YP9upxq8IoSUdef7URUuJZGPWEyCQ0Mk6GRYJHvlX5cNOSHxYDBt`, // RSA + `AAAAC3NzaC1lZDI1NTE5AAAAICj/inr+V2oDyH39iESDof/jM4XcPzUZOVZ/Bm79CVGi`, // Ed25519 + `AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJp4V4vuk5BjiOXKhls02lsw61OZFhZ9Ya188inproU5FmaUhjYnjEvsGPLeMYu3o2AQ4/gsV6MW2H1bNnr5SvY=`, // ECDSA }, }, hostname: "test-hostname.example.com", @@ -221,6 +223,16 @@ func TestFormatHostCAPublicKeysForRegistry(t *testing.T) { PublicKey: "AAAAB3NzaC1yc2EAAAADAQABAAABAQDm0PWl5llSpFArdHkXv8xXgsO9qEAbjvIAjMaoUbr79d03pBlmCCU7Zm3X9NkiLL7om2KLSE7AA0oQI+S+VgrDX17S327uj8M3hNZkfkbKGvzY5NS17DubpEEuAoF1r8Of7GKMbAmQ9d8dF8iNkREaJ+FT8g2JmGtRwmQGf8c0v2FCdz7SbChE9nUxk4Q8f1Qjhx8Pgjga/ntqkB+JpwATVvCxkd/ld0yzh9T0l90dV1TYYwnmWVpQzes1nbotQoMK8vUO20dWBEMWVMxXXp/P4OaztYGLmGJ9YP9upxq8IoSUdef7URUuJZGPWEyCQ0Mk6GRYJHvlX5cNOSHxYDBt", Hostname: "test-hostname.example.com", }, + HostCAPublicKeyForRegistry{ + KeyName: "TeleportHostCA-teleport.example.com-2", + PublicKey: "AAAAC3NzaC1lZDI1NTE5AAAAICj/inr+V2oDyH39iESDof/jM4XcPzUZOVZ/Bm79CVGi", + Hostname: "test-hostname.example.com", + }, + HostCAPublicKeyForRegistry{ + KeyName: "TeleportHostCA-teleport.example.com-3", + PublicKey: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJp4V4vuk5BjiOXKhls02lsw61OZFhZ9Ya188inproU5FmaUhjYnjEvsGPLeMYu3o2AQ4/gsV6MW2H1bNnr5SvY=", + Hostname: "test-hostname.example.com", + }, }, }, },