github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/sshutils/ppk/ppk.go (about) 1 /* 2 Copyright 2021 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package ppk provides functions implementing conversion between Teleport's native RSA 18 // keypairs and PuTTY's PPK format. It also provides functions for working with RFC4251-formatted 19 // mpints and strings. 20 package ppk 21 22 import ( 23 "bytes" 24 "crypto/hmac" 25 "crypto/rsa" 26 "crypto/sha256" 27 "encoding/base64" 28 "encoding/binary" 29 "encoding/hex" 30 "fmt" 31 "math/big" 32 33 "github.com/gravitational/trace" 34 35 "github.com/gravitational/teleport/api/constants" 36 ) 37 38 // ConvertToPPK takes a regular RSA-formatted keypair and converts it into the PPK file format used by the PuTTY SSH client. 39 // The file format is described here: https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk 40 func ConvertToPPK(privateKey *rsa.PrivateKey, pub []byte) ([]byte, error) { 41 // https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk 42 // 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 43 // hashes other than SHA-1. The public key data has already provided the key modulus and the public encoding exponent. The private data stores: 44 // mpint: the private decoding exponent of the key. 45 // mpint: one prime factor p of the key. 46 // mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.) 47 // mpint: the multiplicative inverse of q modulo p. 48 ppkPrivateKey := new(bytes.Buffer) 49 50 // mpint: the private decoding exponent of the key. 51 // this is known as 'D' 52 binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(privateKey.D)) 53 54 // mpint: one prime factor p of the key. 55 // this is known as 'P' 56 // the RSA standard dictates that P > Q 57 // for some reason what PuTTY names 'P' is Primes[1] to Go, and what PuTTY names 'Q' is Primes[0] to Go 58 P, Q := privateKey.Primes[1], privateKey.Primes[0] 59 binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(P)) 60 61 // mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.) 62 // this is known as 'Q' 63 binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(Q)) 64 65 // mpint: the multiplicative inverse of q modulo p. 66 // this is known as 'iqmp' 67 iqmp := new(big.Int).ModInverse(Q, P) 68 binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(iqmp)) 69 70 // now we need to base64-encode the PPK-formatted private key which is made up of the above values 71 ppkPrivateKeyBase64 := make([]byte, base64.StdEncoding.EncodedLen(ppkPrivateKey.Len())) 72 base64.StdEncoding.Encode(ppkPrivateKeyBase64, ppkPrivateKey.Bytes()) 73 74 // read Teleport public key 75 // 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 76 // remove ssh-rsa plus additional space from beginning of string if present 77 if !bytes.HasPrefix(pub, []byte(constants.SSHRSAType+" ")) { 78 return nil, trace.BadParameter("pub does not appear to be an ssh-rsa public key") 79 } 80 pub = bytes.TrimSuffix(bytes.TrimPrefix(pub, []byte(constants.SSHRSAType+" ")), []byte("\n")) 81 82 // the PPK file contains an anti-tampering MAC which is made up of various values which appear in the file. 83 // copied from Section C.3 of https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk: 84 // 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: 85 // string: the algorithm-name header field. 86 // string: the encryption-type header field. 87 // string: the key-comment-string header field. 88 // string: the binary public key data, as decoded from the base64 lines after the 'Public-Lines' header. 89 // string: the plaintext of the binary private key data, as decoded from the base64 lines after the 'Private-Lines' header. 90 91 // these values are also used in the MAC generation, so we declare them as variables 92 keyType := constants.SSHRSAType 93 encryptionType := "none" 94 // as work for the future, it'd be nice to get the proxy/user pair name in here to make the name more 95 // of a unique identifier. this has to be done at generation time because the comment is part of the MAC 96 fileComment := "teleport-generated-ppk" 97 98 // string: the algorithm-name header field. 99 macKeyType := getRFC4251String([]byte(keyType)) 100 // create a buffer to hold the elements needed to generate the MAC 101 macInput := new(bytes.Buffer) 102 binary.Write(macInput, binary.LittleEndian, macKeyType) 103 104 // string: the encryption-type header field. 105 macEncryptionType := getRFC4251String([]byte(encryptionType)) 106 binary.Write(macInput, binary.BigEndian, macEncryptionType) 107 108 // string: the key-comment-string header field. 109 macComment := getRFC4251String([]byte(fileComment)) 110 binary.Write(macInput, binary.BigEndian, macComment) 111 112 // base64-decode the Teleport public key, as we need its binary representation to generate the MAC 113 decoded := make([]byte, base64.StdEncoding.EncodedLen(len(pub))) 114 n, err := base64.StdEncoding.Decode(decoded, pub) 115 if err != nil { 116 return nil, trace.Errorf("could not base64-decode public key: %v, got %v bytes successfully", err, n) 117 } 118 decoded = decoded[:n] 119 // append the decoded public key bytes to the MAC buffer 120 macPublicKeyData := getRFC4251String(decoded) 121 binary.Write(macInput, binary.BigEndian, macPublicKeyData) 122 123 // append our PPK-formatted private key bytes to the MAC buffer 124 macPrivateKeyData := getRFC4251String(ppkPrivateKey.Bytes()) 125 binary.Write(macInput, binary.BigEndian, macPrivateKeyData) 126 127 // as per the PPK spec, the key for the MAC is blank when the PPK file is unencrypted. 128 // therefore, the key is a zero-length byte slice. 129 hmacHash := hmac.New(sha256.New, []byte{}) 130 // generate the MAC using HMAC-SHA-256 131 hmacHash.Write(macInput.Bytes()) 132 macString := hex.EncodeToString(hmacHash.Sum(nil)) 133 134 // build the string-formatted output PPK file 135 ppk := new(bytes.Buffer) 136 fmt.Fprintf(ppk, "PuTTY-User-Key-File-3: %v\n", keyType) 137 fmt.Fprintf(ppk, "Encryption: %v\n", encryptionType) 138 fmt.Fprintf(ppk, "Comment: %v\n", fileComment) 139 // chunk the Teleport-formatted public key into 64-character length lines 140 chunkedPublicKey := chunk(string(pub), 64) 141 fmt.Fprintf(ppk, "Public-Lines: %v\n", len(chunkedPublicKey)) 142 for _, r := range chunkedPublicKey { 143 fmt.Fprintf(ppk, "%s\n", r) 144 } 145 // chunk the PPK-formatted private key into 64-character length lines 146 chunkedPrivateKey := chunk(string(ppkPrivateKeyBase64), 64) 147 fmt.Fprintf(ppk, "Private-Lines: %v\n", len(chunkedPrivateKey)) 148 for _, r := range chunkedPrivateKey { 149 fmt.Fprintf(ppk, "%s\n", r) 150 } 151 fmt.Fprintf(ppk, "Private-MAC: %v\n", macString) 152 153 return ppk.Bytes(), nil 154 } 155 156 // chunk converts a string into a []string with chunks of size chunkSize; 157 // used to split base64-encoded strings across multiple lines with an even width. 158 // note: this function operates on Unicode code points rather than bytes, therefore 159 // using it with multi-byte characters will result in unevenly chunked strings. 160 // it's intended usage is only for chunking base64-encoded strings. 161 func chunk(s string, size int) []string { 162 var chunks []string 163 for b := []byte(s); len(b) > 0; { 164 n := size 165 if n > len(b) { 166 n = len(b) 167 } 168 chunks = append(chunks, string(b[:n])) 169 b = b[n:] 170 } 171 return chunks 172 } 173 174 // getRFC4251Mpint returns a stream of bytes representing a mixed-precision integer (a big.Int in Go) 175 // prepended with a big-endian uint32 expressing the length of the data following. 176 // This is the 'mpint' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5) 177 func getRFC4251Mpint(n *big.Int) []byte { 178 buf := new(bytes.Buffer) 179 b := n.Bytes() 180 // RFC4251: If the most significant bit would be set for a positive number, the number MUST be preceded by a zero byte. 181 if b[0]&0x80 > 0 { 182 b = append([]byte{0}, b...) 183 } 184 // write a uint32 with the length of the byte stream to the buffer 185 binary.Write(buf, binary.BigEndian, uint32(len(b))) 186 // write the byte stream representing of the rest of the integer to the buffer 187 binary.Write(buf, binary.BigEndian, b) 188 return buf.Bytes() 189 } 190 191 // getRFC4251String returns a stream of bytes representing a string prepended with a big-endian unit32 192 // expressing the length of the data following. 193 // This is the 'string' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5) 194 func getRFC4251String(data []byte) []byte { 195 buf := new(bytes.Buffer) 196 // write a uint32 with the length of the byte stream to the buffer 197 binary.Write(buf, binary.BigEndian, uint32(len(data))) 198 // write the byte stream representing of the rest of the data to the buffer 199 for _, v := range data { 200 binary.Write(buf, binary.BigEndian, v) 201 } 202 return buf.Bytes() 203 }