├── examples ├── ssh_to_pem │ └── main.go └── pem_to_ssh │ └── main.go ├── LICENSE ├── encoding.go └── README.md /examples/ssh_to_pem/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ianmcmahon/encoding_ssh" 5 | 6 | "os" 7 | "fmt" 8 | "io/ioutil" 9 | "crypto/x509" 10 | "encoding/pem" 11 | ) 12 | 13 | func main() { 14 | 15 | // read in public key from file 16 | bytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa.pub") 17 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 18 | 19 | // decode string ssh-rsa format to native type 20 | pub_key, err := ssh.DecodePublicKey(string(bytes)) 21 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 22 | // pub_key is of type *rsa.PublicKey from 'crypto/rsa' 23 | 24 | // Marshal to ASN.1 DER encoding 25 | pkix, err := x509.MarshalPKIXPublicKey(pub_key) 26 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 27 | 28 | // Encode to PEM format 29 | pem := string(pem.EncodeToMemory(&pem.Block{ 30 | Type: "RSA PUBLIC KEY", 31 | Bytes: pkix, 32 | })) 33 | 34 | fmt.Printf("%s", pem) 35 | } 36 | -------------------------------------------------------------------------------- /examples/pem_to_ssh/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ianmcmahon/encoding_ssh" 5 | 6 | "os" 7 | "fmt" 8 | "io/ioutil" 9 | "crypto/x509" 10 | "encoding/pem" 11 | ) 12 | 13 | func main() { 14 | 15 | // read in private key from file (private key is PEM encoded PKCS) 16 | bytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa") 17 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 18 | 19 | // decode PEM encoding to ANS.1 PKCS1 DER 20 | block, _ := pem.Decode(bytes) 21 | if block == nil { fmt.Printf("No Block found in keyfile\n"); os.Exit(1) } 22 | if block.Type != "RSA PRIVATE KEY" { fmt.Printf("Unsupported key type"); os.Exit(1) } 23 | 24 | // parse DER format to a native type 25 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 26 | 27 | // encode the public key portion of the native key into ssh-rsa format 28 | // second parameter is the optional "comment" at the end of the string (usually 'user@host') 29 | ssh_rsa, err := ssh.EncodePublicKey(key.PublicKey, "") 30 | 31 | fmt.Printf("%s\n", ssh_rsa) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Ian McMahon 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "bytes" 6 | "strings" 7 | "encoding/binary" 8 | "encoding/base64" 9 | "crypto/rsa" 10 | "math/big" 11 | ) 12 | 13 | // ssh one-line format (for lack of a better term) consists of three text fields: { key_type, data, comment } 14 | // data is base64 encoded binary which consists of tuples of length (4 bytes) and data of the length described previously. 15 | // For RSA keys, there should be three tuples which should be: { key_type, public_exponent, modulus } 16 | 17 | func EncodePublicKey(key interface{}, comment string) (string, error) { 18 | if rsaKey, ok := key.(rsa.PublicKey); ok { 19 | key_type := "ssh-rsa" 20 | 21 | modulus_bytes := rsaKey.N.Bytes() 22 | 23 | buf := new(bytes.Buffer) 24 | 25 | var data = []interface{} { 26 | uint32(len(key_type)), 27 | []byte(key_type), 28 | uint32(binary.Size(uint32(rsaKey.E))), 29 | uint32(rsaKey.E), 30 | uint32(binary.Size(modulus_bytes)), 31 | modulus_bytes, 32 | } 33 | 34 | for _, v := range data { 35 | err := binary.Write(buf, binary.BigEndian, v) 36 | if err != nil { return "", err } 37 | } 38 | 39 | return fmt.Sprintf("%s %s %s", key_type, base64.StdEncoding.EncodeToString(buf.Bytes()), comment), nil 40 | } 41 | 42 | return "", fmt.Errorf("Unknown key type: %T\n", key) 43 | } 44 | 45 | func readLength(data []byte) ([]byte, uint32, error) { 46 | l_buf := data[0:4] 47 | 48 | buf := bytes.NewBuffer(l_buf) 49 | 50 | var length uint32 51 | 52 | err := binary.Read(buf, binary.BigEndian, &length) 53 | if err != nil { return nil, 0, err } 54 | 55 | return data[4:], length, nil 56 | } 57 | 58 | func readBigInt(data []byte, length uint32) ([]byte, *big.Int, error) { 59 | var bigint = new(big.Int) 60 | bigint.SetBytes(data[0:length]) 61 | return data[length:], bigint, nil 62 | } 63 | 64 | func getRsaValues(data []byte) (format string, e *big.Int, n *big.Int, err error) { 65 | data, length, err := readLength(data) 66 | if err != nil { return } 67 | 68 | format = string(data[0:length]); data = data[length:] 69 | 70 | data, length, err = readLength(data) 71 | if err != nil { return } 72 | 73 | data, e, err = readBigInt(data, length) 74 | if err != nil { return } 75 | 76 | data, length, err = readLength(data) 77 | if err != nil { return } 78 | 79 | data, n, err = readBigInt(data, length) 80 | if err != nil { return } 81 | 82 | return 83 | } 84 | 85 | func DecodePublicKey(str string) (interface{}, error) { 86 | // comes in as a three part string 87 | // split into component parts 88 | 89 | tokens := strings.Split(str, " ") 90 | 91 | if len(tokens) < 2 { return nil, fmt.Errorf("Invalid key format; must contain at least two fields (keytype data [comment])") } 92 | 93 | key_type := tokens[0] 94 | data, err := base64.StdEncoding.DecodeString(tokens[1]) 95 | if err != nil { return nil, err } 96 | 97 | format, e, n, err := getRsaValues(data) 98 | 99 | if format != key_type { return nil, fmt.Errorf("Key type said %s, but encoded format said %s. These should match!", key_type, format) } 100 | 101 | pubKey := &rsa.PublicKey{ 102 | N: n, 103 | E: int(e.Int64()), 104 | } 105 | 106 | return pubKey, nil 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh public key encoding 2 | 3 | go's crypto libraries are fairly comprehensive, but they don't cover the specific case of the "ssh one-line public key encoding". 4 | 5 | This library provides methods to convert between `crypto/rsa.PublicKey` and the string ssh-rsa one-line encoding found in typical ssh `id_rsa.pub` files. There is scaffolding in place to be able to extend it to other ciphers in the future, but that work is not yet done. 6 | 7 | From the native structures such as `rsa.PublicKey`, it's fairly simple to use the existing crypto and encoding libraries to marshal into other formats such as PEM. 8 | 9 | ## Usage 10 | 11 | ```go 12 | 13 | import "github.com/ianmcmahon/encoding_ssh" 14 | 15 | pub_key, err := ssh.DecodePublicKey(string(bytes)) 16 | 17 | ssh_rsa_string, err := ssh.EncodePublicKey(user_key.PublicKey(), "user@host") 18 | 19 | ``` 20 | 21 | Here's a short program which reads $HOME/.ssh/id_rsa.pub and outputs the public key in PKCS8 format. This is equivalent to: 22 | 23 | ssh-keygen -f $HOME/.ssh/id_rsa.pub -e -m pkcs8 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "github.com/ianmcmahon/encoding_ssh" 30 | 31 | "os" 32 | "fmt" 33 | "io/ioutil" 34 | "crypto/x509" 35 | "encoding/pem" 36 | ) 37 | 38 | func main() { 39 | 40 | // read in public key from file 41 | bytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa.pub") 42 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 43 | 44 | // decode string ssh-rsa format to native type 45 | pub_key, err := ssh.DecodePublicKey(string(bytes)) 46 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 47 | // pub_key is of type *rsa.PublicKey 48 | 49 | // Marshal to ASN.1 DER encoding 50 | pkix, err := x509.MarshalPKIXPublicKey(pub_key) 51 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 52 | 53 | // Encode to PEM format 54 | pem := string(pem.EncodeToMemory(&pem.Block{ 55 | Type: "RSA PUBLIC KEY", 56 | Bytes: pkix, 57 | })) 58 | 59 | fmt.Printf("%s", pem) 60 | } 61 | ``` 62 | 63 | 64 | Here is another short program which reads $HOME/.ssh/id_rsa and outputs the public key in ssh-rsa one-line format. This is equivalent to: 65 | 66 | ssh-keygen -y -f ~/.ssh/id_rsa 67 | 68 | ```go 69 | package main 70 | 71 | import ( 72 | "github.com/ianmcmahon/encoding_ssh" 73 | 74 | "os" 75 | "fmt" 76 | "io/ioutil" 77 | "crypto/x509" 78 | "encoding/pem" 79 | ) 80 | 81 | func main() { 82 | 83 | // read in private key from file (private key is PEM encoded PKCS) 84 | bytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa") 85 | if err != nil { fmt.Printf("%v\n", err); os.Exit(1) } 86 | 87 | // decode PEM encoding to ANS.1 PKCS1 DER 88 | block, _ := pem.Decode(bytes) 89 | if block == nil { fmt.Printf("No Block found in keyfile\n"); os.Exit(1) } 90 | if block.Type != "RSA PRIVATE KEY" { fmt.Printf("Unsupported key type"); os.Exit(1) } 91 | 92 | // parse DER format to a native type 93 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 94 | 95 | // encode the public key portion of the native key into ssh-rsa format 96 | // second parameter is the optional "comment" at the end of the string (usually 'user@host') 97 | ssh_rsa, err := ssh.EncodePublicKey(key.PublicKey, "") 98 | 99 | fmt.Printf("%s\n", ssh_rsa) 100 | } 101 | ``` --------------------------------------------------------------------------------