├── TODO ├── readpass ├── doc.go ├── example │ └── example.go ├── password.go ├── noecho.h └── noecho.c ├── testdata ├── test2_ecdsa.pub ├── test_ecdsa.pub ├── test_ecdsa ├── test2_ecdsa ├── test2_rsa.pub ├── test_rsa.pub ├── test2_dsa.pub ├── test_dsa.pub ├── test2_dsa ├── test_dsa ├── test_rsa └── test2_rsa ├── password_unix.go ├── LICENSE ├── README ├── doc.go ├── sshkey_test.go ├── password.go └── sshkey.go /TODO: -------------------------------------------------------------------------------- 1 | * Determine the best way to get DefaultPasswordPrompt to not echo 2 | * Add support for PKCS #8-protected keys. 3 | -------------------------------------------------------------------------------- /readpass/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package readpass provides password reading functionality. It is 3 | a Unix-based package, and will probably not work on other platforms. 4 | */ 5 | package readpass 6 | -------------------------------------------------------------------------------- /testdata/test2_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMEtsgi1zwwbHgbLUb9qiEJ0/74BwZ8HygBGUwFy3VBpsMmVE2VQTRWvZDO2w+SOn+TXXvBvZcClkS4kaXqYK6A= kyle@ono-sendai 2 | -------------------------------------------------------------------------------- /testdata/test_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGWKlhSvcZEKiBYkC8CL6srHva35P0+rpg3NemfZJFrUAXR4CqMQLQD82Ko/GImI8uJ+BN8yAfIUPAtwG/Pd2ik= kyle@ono-sendai 2 | -------------------------------------------------------------------------------- /testdata/test_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIE4YI+auQIx0lQiY9z+dyJJpEGYwUjfhJNl3P86yXLOGoAoGCCqGSM49 3 | AwEHoUQDQgAEZYqWFK9xkQqIFiQLwIvqyse9rfk/T6umDc16Z9kkWtQBdHgKoxAt 4 | APzYqj8YiYjy4n4E3zIB8hQ8C3Ab893aKQ== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /testdata/test2_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,E8F2F868136F76D03500846DEA344E2E 4 | 5 | 97B69kX1SkwRjTBpOF1Na9Dyk4e0L3fZ9bYWs0F6G4oKBaBM06S3+7sh8xmA5YUR 6 | nvlZYyXa/S0ZJ26ic8Hd2eEwRPphg0HESZYC5rZ3BT04xZNOCyovyDar75XHZr3L 7 | iNyeVXvaGqN+o8BCuThX7y5EqBnVqy9PYcKpjHk0Mtw= 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /testdata/test2_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVeRl4Sxs4dWrpxTns7opwfj6i7/fwav7clHkezH4jcewnxalAXAP7xK+xfrmYNNh0RVI/jO+TaGiqM8X1KIOpWgUJ6M1qZwduV4cX0dECzRBBwr/3wUHP3TLVwo1/4Zz0vlrh2viXKaX7r4SLjCaeWFsC1GIkPq8QGHTSlh4OMpDW9T5NWkpmig61+JHi9E1rp8G451WLVl+FJ7bDP4GuqBuPnSw5f/vVVg/nwCR04JLw7gaz6KzXtVdRhqYGr3ryMthwwLe45G1oIOrTtpKYO2X1jigUSQQ51PewAlCwC3Q3eWAMZ9QYgIwRQ2hwb1vhIajctSly8kvUsq9AbiQt kyle@ono-sendai 2 | -------------------------------------------------------------------------------- /testdata/test_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxtQ0U5dLlONK4V/Me0ciAVpNgOgCaXAzWIdk7ngxXewX1uZDxMRStZdwZeHBrRS8mg+D51GJTSPLOFTeqS9WB4Ohv7dwYMpbX7ArUrbllBCiXIh9063QwQctEgUDLUaA8atkh0jxGjXncUFhROzawWg9HGlSs6kAlrehBPex6OSwxwdQD/qC7rlXCRa/trpcApyQgXHeR1tIDipoDfGTvjveko0hDDQNvbcnm0NZn/ofH9l+02F9E7VbhrYcPzppPDthdkYSs0TnWQRvsgj4YJFMfyxK/HeDXyPhp/grSOICkeTx7+VQMVh39bNVoy1ZXdmH3VO5rdDdvSLAxhusn kyle@ono-sendai 2 | -------------------------------------------------------------------------------- /password_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package sshkey 4 | 5 | import "fmt" 6 | import "github.com/gokyle/sshkey/readpass" 7 | 8 | func unixReadPassword(prompt string) (password string, err error) { 9 | binPass, err := readpass.ReadPass(prompt) 10 | fmt.Printf("\n") 11 | if err == nil { 12 | password = string(binPass) 13 | } 14 | return 15 | } 16 | 17 | func init() { 18 | PasswordPrompt = unixReadPassword 19 | } 20 | -------------------------------------------------------------------------------- /readpass/example/example.go: -------------------------------------------------------------------------------- 1 | // This is a quick demo showing how to use ReadPass. It reads a 2 | // password securely from the console and then displays the 3 | // length of the password. 4 | package main 5 | 6 | import "fmt" 7 | import "github.com/gokyle/sshkey/readpass" 8 | 9 | func main() { 10 | password, err := readpass.ReadPass("enter password: ") 11 | fmt.Printf("\n") 12 | if err != nil { 13 | fmt.Println(err.Error()) 14 | } else { 15 | fmt.Printf("%d character password read\n", len(password)) 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /testdata/test2_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAI/awUQns29eaa1DhxPostpz4oPKgyHFcGNubwan+apbRSEJ+AIJl6jgXm8KYjFHK1bNiiY96WWzNrPgDId7vFQGivTN3HlUQmgpZ4wu1HUHn1vhQws5pQeTsxZEX30WMV47VE2FrCaA5pRaFT/2mTRYTrDM81fDir+XUAm0BgOnAAAAFQDtBF1BGAQ8Ix9Og/XBdE53BkMfXwAAAIA5T8wYDsjSzzU+JT1/tYLeqjmpqK0/D0PAowCOzVLrRrbSGuXbo0Cc2r7/yb443iM+2jpi4PjddZW7S9so2VGJVjSM6BFcadQF8j0mrinLN1L57uSJHOZcCZyAyA1iZvYgRo1EFzARQHeCbPEjIKqGw45iOdCZWuMBNrBGn9IOzgAAAIA1Bd+SVLQrcLQL67dFG4RexHuxyeK3U4PsJ4HJ+YAXC5wm8PEmBtlytRvovKnflc4Mntoo4CCwnuSF97BEEfUk7NdxToeOCCvHUB3qJuLNQAyvz7GhCgKx5vLptcmgV+XohKaprsiUum3ltflX7as8HpxgbrYeGEAGBYMEw+DLtg== yosida95@yosida95 2 | -------------------------------------------------------------------------------- /testdata/test_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAKbRx6ZXw5ZQfdVkCmdQmvBI9WCCoKvnujbg/NP16Kwh+CUIdS/Z0vr7JO4B+eXHl+Km23uL7HSVIEur9ztxTL+O+FKqu111WeP/5MyBNgoslJlhTOKKCJT838HQIQOg40mv4Y8wD0JtujSOZtnvj4ve2YlRx8OFTFXnP9g7vf+3AAAAFQDMKCiCOLCp5VxGPEHbr0epxu2NQQAAAIBPTI2EylP1aMZ8Kcnc0kgjtgXSnnwu/6yjGR3GKq4vEdgSUeS8lL+DJgs7AIh24zU5xyS8wo7ZmtA6yslPR6aicwk5RSCDFbyVVvpcy799tCf7GNc5g3CECFFZmy6aKYA+CuZ4+9yoLAyTpzYOAl7KbMbwLS2FmFe8ePPv+kGY1gAAAIB31Wq98j2A8IiFU4COrNU1v/bbUCwryl03Vb1sQIe/etId6iZSGm/v+WDieg3zk0xAMQbDwOC9iTtOGzb5jAxsml80gbYdgABxRFfYtzMzRm5Bv6uEkCbSEuOBU2uyljnuc+hiHC/SPlyZ0nofayfuCqbFz86Snbx88k6nuMvanA== yosida95@yosida95 2 | -------------------------------------------------------------------------------- /readpass/password.go: -------------------------------------------------------------------------------- 1 | package readpass 2 | 3 | // #cgo openbsd CFLAGS: -fpic -nopie 4 | // #cgo openbsd LDFLAGS: -fpic 5 | // #include 6 | // #include "noecho.h" 7 | import "C" 8 | import "fmt" 9 | import "unsafe" 10 | 11 | // ReadPass shows the prompt, and reads the password while blanking 12 | // the console. Most users will want to print a newline immediately 13 | // after reading the password. 14 | func ReadPass(prompt string) ([]byte, error) { 15 | fmt.Printf("%s", prompt) 16 | pw := C.readpass() 17 | if pw == nil { 18 | return nil, fmt.Errorf("readpass: failed to read password") 19 | } 20 | password := C.GoString(pw) 21 | C.free(unsafe.Pointer(pw)) 22 | return []byte(password), nil 23 | } 24 | -------------------------------------------------------------------------------- /testdata/test2_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBuwIBAAKBgQCP2sFEJ7NvXmmtQ4cT6LLac+KDyoMhxXBjbm8Gp/mqW0UhCfgC 3 | CZeo4F5vCmIxRytWzYomPellszaz4AyHe7xUBor0zdx5VEJoKWeMLtR1B59b4UML 4 | OaUHk7MWRF99FjFeO1RNhawmgOaUWhU/9pk0WE6wzPNXw4q/l1AJtAYDpwIVAO0E 5 | XUEYBDwjH06D9cF0TncGQx9fAoGAOU/MGA7I0s81PiU9f7WC3qo5qaitPw9DwKMA 6 | js1S60a20hrl26NAnNq+/8m+ON4jPto6YuD43XWVu0vbKNlRiVY0jOgRXGnUBfI9 7 | Jq4pyzdS+e7kiRzmXAmcgMgNYmb2IEaNRBcwEUB3gmzxIyCqhsOOYjnQmVrjATaw 8 | Rp/SDs4CgYA1Bd+SVLQrcLQL67dFG4RexHuxyeK3U4PsJ4HJ+YAXC5wm8PEmBtly 9 | tRvovKnflc4Mntoo4CCwnuSF97BEEfUk7NdxToeOCCvHUB3qJuLNQAyvz7GhCgKx 10 | 5vLptcmgV+XohKaprsiUum3ltflX7as8HpxgbrYeGEAGBYMEw+DLtgIVAJBzPAjG 11 | 80gPrOu9UnLImDBJzPae 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /testdata/test_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBugIBAAKBgQCm0cemV8OWUH3VZApnUJrwSPVggqCr57o24PzT9eisIfglCHUv 3 | 2dL6+yTuAfnlx5fiptt7i+x0lSBLq/c7cUy/jvhSqrtddVnj/+TMgTYKLJSZYUzi 4 | igiU/N/B0CEDoONJr+GPMA9Cbbo0jmbZ74+L3tmJUcfDhUxV5z/YO73/twIVAMwo 5 | KII4sKnlXEY8QduvR6nG7Y1BAoGAT0yNhMpT9WjGfCnJ3NJII7YF0p58Lv+soxkd 6 | xiquLxHYElHkvJS/gyYLOwCIduM1OcckvMKO2ZrQOsrJT0emonMJOUUggxW8lVb6 7 | XMu/fbQn+xjXOYNwhAhRWZsumimAPgrmePvcqCwMk6c2DgJeymzG8C0thZhXvHjz 8 | 7/pBmNYCgYB31Wq98j2A8IiFU4COrNU1v/bbUCwryl03Vb1sQIe/etId6iZSGm/v 9 | +WDieg3zk0xAMQbDwOC9iTtOGzb5jAxsml80gbYdgABxRFfYtzMzRm5Bv6uEkCbS 10 | EuOBU2uyljnuc+hiHC/SPlyZ0nofayfuCqbFz86Snbx88k6nuMvanAIUYbKssiTX 11 | BCCmLz3MSTfhlU27hZg= 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Kyle Isom 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | sshkey: a small package for loading OpenSSH ECDSA and RSA keys. 2 | 3 | The key may be loaded via file, HTTP(S), or as byte slices. 4 | 5 | Example: 6 | 7 | pub, keytype, err := sshkey.LoadPublicKeyFile("/home/user/.ssh/id_rsa.pub", false) 8 | switch keytype { 9 | case KEY_RSA: doSomethingRSA(pub.(*rsa.PublicKey)) 10 | case KEY_ECDSA: doSomethingEC(pub.(*ecdsa.PublicKey)) 11 | default: // unknown key type 12 | } 13 | 14 | priv, keytype, err := sshkey.LoadPrivateKeyFile("/home/user/.ssh/id_rsa") 15 | switch keytype { 16 | case KEY_RSA: doSomethingSecretRSA(pub.(*rsa.PrivateKey)) 17 | case KEY_ECDSA: doSomethingSecretEC(pub.(*ecdsa.PrivateKey)) 18 | default: // unknown key type 19 | } 20 | 21 | License: 22 | sshkey is released under an ISC license. 23 | -------------------------------------------------------------------------------- /readpass/noecho.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 by Kyle Isom . 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS 9 | * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE 11 | * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 | * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 13 | * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 14 | * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 15 | * SOFTWARE. 16 | */ 17 | 18 | #ifndef __READ_PASS_H 19 | #define __READ_PASS_H 20 | 21 | 22 | char *readpass(void); 23 | 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package sshkey provides Go handling of OpenSSH keys. It handles RSA 3 | (protocol 2 only) and ECDSA keys, and aims to provide interoperability 4 | between OpenSSH and Go programs. 5 | 6 | The package can import public and private keys using the LoadPublicKey 7 | and LoadPrivateKey functions; the LoadPublicKeyFile and LoadPrivateKeyfile 8 | functions are wrappers around these functions to load the key from 9 | a file. For example: 10 | 11 | // true tells LoadPublicKey to load the file locally; if false, it 12 | // will try to load the key over HTTP. 13 | pub, err := LoadPublicKeyFile("/home/user/.ssh/id_ecdsa.pub", true) 14 | if err != nil { 15 | fmt.Println(err.Error()) 16 | return 17 | } 18 | 19 | In this example, the ECDSA key is in pub.Key. In order to be used in functions 20 | that require a *ecdsa.PublicKey type, it must be typecast: 21 | 22 | ecpub := pub.Key.(*ecdsa.PublicKey) 23 | 24 | The SSHPublicKey can be marshalled to OpenSSH format by using 25 | MarshalPublicKey. 26 | 27 | The package also provides support for generating new keys. The 28 | GenerateSSHKey function can be used to generate a new key in the 29 | appropriate Go package format (e.g. *ecdsa.PrivateKey). This key 30 | can be marshalled into a PEM-encoded OpenSSH key using the 31 | MarshalPrivate function. 32 | */ 33 | package sshkey 34 | -------------------------------------------------------------------------------- /testdata/test_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAsbUNFOXS5TjSuFfzHtHIgFaTYDoAmlwM1iHZO54MV3sF9bmQ 3 | 8TEUrWXcGXhwa0UvJoPg+dRiU0jyzhU3qkvVgeDob+3cGDKW1+wK1K25ZQQolyIf 4 | dOt0MEHLRIFAy1GgPGrZIdI8Ro153FBYUTs2sFoPRxpUrOpAJa3oQT3sejksMcHU 5 | A/6gu65VwkWv7a6XAKckIFx3kdbSA4qaA3xk7473pKNIQw0Db23J5tDWZ/6Hx/Zf 6 | tNhfRO1W4a2HD86aTw7YXZGErNE51kEb7II+GCRTH8sSvx3g18j4af4K0jiApHk8 7 | e/lUDFYd/WzVaMtWV3Zh91Tua3Q3b0iwMYbrJwIDAQABAoIBAQCnpQecSVv1hu+z 8 | nLTsxDeEayhJRS7AcEpj0DbhOS6ncJWkxNTp1MWO92L2XFqht/jNOuiIh0XjEBSP 9 | OhDzhOr8xbJBtYFHCqKnPDTTgrg9hfjM6xIF+QH5bHhYPh9kL7McjCge8fiPDnSB 10 | 1QIcT+I96Bg1ma98GNk/MprCwapPegFrBEtp8GxoHYkjB5lCTZULryIBJEFYXOJX 11 | FyEdUrGY/YdRjfXKSfdGEqeArhBnv1msgxVMVU0BEt9PWeYjvZzEhDFFAj6GsweR 12 | eQMMj+AOsmP6rJbbDf4Jy+3btACTsn3sv9qInlmGH+dpZ1+UJKC6tW30yAAd7M6r 13 | QPogiWQxAoGBAN3UYk24RNZY3WdgfcGBMINEIzD5QqejguZui7HOVCSoTAi2flXr 14 | +vEIeFg5hTolo0o5SPxXMX4koaZZWHSAKk34WplHiuDb/X1h/7pquaEYywhVBRux 15 | JzGqWmtni/EGpogt35WrHdhxuECCWTzMu0O2RIFvSKCVw+N4oBis7WEDAoGBAM0U 16 | v03sioT6Z9pQYx8vPY9lWH2l7Y4PFFVeFaeraCM2WKuj4WxwS+TOHomjM8W1OjeN 17 | 0BqqqmB5ekD3Q4BuQKnom8JuTGMRjj0C0u5S9RKygTSLSVVWs/2LWaqzFib6848c 18 | lVTtp/S5cSoiqX3yISlqDqdn92o078CF3EDSr6oNAoGBALbX76tGHp9bIiuqkh1k 19 | ohsgl60vV6ycDzJmngiSHDMW8Y0g9yNVXkM8iw2VY+YYze5JJ4qvSCFEimV2MX3d 20 | goFAG1VKbgOH8PpfQ41hmN5bdEbK5Wn87GR4mQz8jZG+vJd00F0qbXBR0+CAUJPX 21 | 4OcgHnldw8xxhcRCUhvktckHAoGAPmgM2wKbGGI2zileMHQosIUvi3S9uFgAYQXz 22 | YwV2+BgmnXyOMXhur97FTi76pzRB1fvyktrSY+8zc6eKw/9I8CtXkrAL0K3b8db1 23 | Jw7ZguoNBVdJZo2u0f3guGAs4onFKgyRqLl1PbRcqmzH1QqkarzaXrj1gj0/o/9Y 24 | CeXTDNUCgYEAz1pPXMZT/JaOIFICDDH06azgYDMNDS39aQpgMFEiu8KARw1/mZyJ 25 | EvUFJ4CfL9iRxpH3tF0GvsBcQvK8XuNUj5zyurmSIDB1HYQMQndhweeQPReYZu1D 26 | uh+n0efsHxVjhOjbuB4GOLOx7sVNy7Lf49/FOu+XK3TQbAfdkCz3DeU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /readpass/noecho.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 by Kyle Isom . 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS 9 | * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE 11 | * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 12 | * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 13 | * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 14 | * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 15 | * SOFTWARE. 16 | */ 17 | 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "noecho.h" 25 | 26 | 27 | /* 28 | * readpass switches the console to a non-echoing mode, reads a 29 | * line of standard input, and then switches the console back to 30 | * echoing mode. 31 | */ 32 | char * 33 | readpass() 34 | { 35 | struct termios term, restore; 36 | char *password = NULL; 37 | size_t pw_size = 0; 38 | ssize_t pw_len; 39 | 40 | if (tcgetattr(STDIN_FILENO, &term) == -1) 41 | return NULL; 42 | 43 | restore = term; 44 | term.c_lflag &= ~ECHO; 45 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term) == -1) 46 | return NULL; 47 | 48 | pw_len = getline(&password, &pw_size, stdin); 49 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &restore) == -1) 50 | return NULL; 51 | if (password != NULL) 52 | password[pw_len - 1] = (char)0; 53 | return password; 54 | } 55 | -------------------------------------------------------------------------------- /testdata/test2_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,54D26968CBC432AC906A8516FECDA456 4 | 5 | qPjE0qeMLk8lTZNVHQkbY5WwjeCKtL1qKGpxk714h5rAch/8PCBmAgDVfIkjQqE2 6 | o0TjKkddnfN/3TRytkMXrHbeJPB7U7z0EQcq14aD3qVzMfpdYQ2EmFe6xuJmQk9i 7 | IjyCQ9FYnKN2q4y5Zg0DKrdR55RQ5blR4OrMyrvKEFSU8M3tvXIyKyiNZfKqm4hp 8 | Tm6BJAV+hSmGy1tu6V9hTzmMnxDFr0LrkGYqqwSO74EKaxUEFOIj1R1uoNJ5s/q3 9 | rayHU6OLfdm4tzpwdVGtfQPU9x6UNwgNkSNu4syT8L112Y09wjx1WDzxMClQi/Ic 10 | 0fhWYQ801D37HHuSOB4vDhD9dFC6HOvyUBKuI/m54an2gm7jue4aPrT9mu6iJRgk 11 | 8kvSobFnU/meUo58vMgz2HDKpJ2bq4TGZfkFrvIAB685HF57QVj9b7wvelTNL6ky 12 | kyi42I6E8ZTDpF7jjhZKY9vM2xR6UnbeuNluFs/7AqU5QtmcL+ckBqTQizOQzgll 13 | l7++9mbZGDawc5m3sRGmDjq+5J+xxjxMmCMi68n27JzASmduLlu1lCkvgMmtN/uQ 14 | NImQmv4bfiHKQWfLXFqjM7ZBtnURKJLwj2J+TC4o14SwhGd8iSlZl49ov9jGJq8X 15 | pN6N3XTcpIKrbCt9FdEA/L9LEv8Pka13j4f3l8W3juA/d5vbNbzzhYUMLo7mW+0U 16 | jkEf+yR0HkieRhDkLHxqTSop7OUJ8wL8yWHip4e6tOWyLgCLpnEzLhPUe4QZ0UBr 17 | LVmjkuL+M9yvS2PvHAWTEO1C6RG6SqTuFYqNiqujkqd/1xNqmyJ36V9dRjlGzm88 18 | dekh8UPgsXx/LEXlVYRC54FhxpTIUOxk7+jlCqTo17nNDMt7MiCLMaMVLt4GkX9F 19 | 7JtI+pP/NqmDOIy4qya1PHkbewq/qWWnq/sQEyIs5ctw3mJIwhBDEMVm8SZykz9l 20 | 3Gi/QzB/IeWwxLLvhyf2PdlyWXQmHar8T4dJSOcc8Vd02nvFEfBXpDqjSIRvqdxw 21 | rZpGUimA8nduevuzwzBaC5n69AlNva6xUhoDxP2zVfGHzT+3/KYk7N7MF894hhiP 22 | xBjfKb8ChDlZmaPyX6g3XoPbMRY6qv1v2GDWXJXVTQwTJziiBG6A02jioVOco5Y/ 23 | v87/S1vT4aYRjg+8NIsyA1z7rO6EJMe/PFxPVUxLn0sogUGiJCnN6S/+KvNv/tXz 24 | cM5sdwIsaN5ZlKpsO9BUyYjCZanwoYEIsX1QRbH5Kp2WM63GlvHhAKHHg+NQUPQ0 25 | NQT08cVvCt2XMVsx8chWS4F2i28I43lbEb7s3ReK/CLtxZ7caOm9LYT0hoYsASR2 26 | tCiFiNFOIt7R61R9PmMrgzW+XekTwhjWEjyWADLpqvY1SE2OiLqU2V4+hdi/Drvj 27 | aCUzpURDokKUnvjRpPjelRiWxF9BnAPOvdiRHYV/8CQ773hJiQ39gDm9lOKuJRd2 28 | t8CQeE2A6IC2zNbLXDT3021JpkC7RakuK5Ah87cT1+wY22Vd6t/5jJdjgYCogTuF 29 | kogXQhg+gmA3nL0q70fHdP8FNzbX6ITjmyPqqb5SZXJA/cRRhE/jlUADu4m2L8j2 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /sshkey_test.go: -------------------------------------------------------------------------------- 1 | package sshkey 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func StubPasswordPrompt(prompt string) (password string, err error) { 9 | return "foo bar", nil 10 | } 11 | 12 | type pubTest struct { 13 | FileName string 14 | Fingerprint string 15 | Type Type 16 | Size int 17 | } 18 | 19 | var pubTests = []pubTest{ 20 | pubTest{ 21 | FileName: "testdata/test_rsa.pub", 22 | Fingerprint: "d3:86:5e:77:d4:b7:52:fb:61:39:38:43:31:bb:a2:76", 23 | Type: KEY_RSA, 24 | Size: 2048, 25 | }, 26 | pubTest{ 27 | FileName: "testdata/test2_rsa.pub", 28 | Fingerprint: "a8:02:c1:3f:d8:08:16:84:5f:82:4f:08:6c:2e:98:5b", 29 | Type: KEY_RSA, 30 | Size: 2048, 31 | }, 32 | pubTest{ 33 | FileName: "testdata/test_ecdsa.pub", 34 | Fingerprint: "04:12:2a:3f:e6:ef:da:05:ad:86:72:44:e2:bb:45:d9", 35 | Type: KEY_ECDSA, 36 | Size: 256, 37 | }, 38 | pubTest{ 39 | FileName: "testdata/test2_ecdsa.pub", 40 | Fingerprint: "1f:7f:1d:f1:72:b5:f9:21:dd:38:bf:d0:68:80:85:78", 41 | Type: KEY_ECDSA, 42 | Size: 256, 43 | }, 44 | pubTest{ 45 | FileName: "testdata/test_dsa.pub", 46 | Fingerprint: "a0:21:8c:1c:f4:4f:45:b4:1c:2a:a9:47:4b:7f:ba:f0", 47 | Type: KEY_DSA, 48 | Size: 1024, 49 | }, 50 | pubTest{ 51 | FileName: "testdata/test2_dsa.pub", 52 | Fingerprint: "04:65:87:9f:ae:6d:d6:56:6d:b3:a9:e6:4f:d7:e9:25", 53 | Type: KEY_DSA, 54 | Size: 1024, 55 | }, 56 | } 57 | 58 | var fingerprintList map[string]string 59 | var privList = []string{ 60 | "testdata/test_rsa", 61 | "testdata/test2_rsa", 62 | "testdata/test_ecdsa", 63 | "testdata/test2_ecdsa", 64 | "testdata/test_dsa", 65 | "testdata/test2_dsa", 66 | } 67 | 68 | func init() { 69 | PasswordPrompt = StubPasswordPrompt 70 | } 71 | 72 | func TestLoadPublicKeys(t *testing.T) { 73 | for _, tc := range pubTests { 74 | pub, err := LoadPublicKeyFile(tc.FileName, true) 75 | if err != nil { 76 | fmt.Println(tc.FileName) 77 | fmt.Println(err.Error()) 78 | t.FailNow() 79 | } 80 | fpr, err := FingerprintPretty(pub, 0) 81 | if err != nil { 82 | fmt.Println(tc.FileName) 83 | fmt.Println(err.Error()) 84 | t.FailNow() 85 | } 86 | if fpr != tc.Fingerprint { 87 | fmt.Printf("sshkey: bad fingerprint %s\n\texpected: %s\n", 88 | fpr, tc.Fingerprint) 89 | t.FailNow() 90 | } else if pub.Size() != tc.Size { 91 | fmt.Println(tc.FileName) 92 | fmt.Printf("sshkey: bad key size %d bits: expected %d bits\n", 93 | pub.Size(), tc.Size) 94 | t.FailNow() 95 | } 96 | } 97 | } 98 | 99 | func TestLoadPrivateKeys(t *testing.T) { 100 | for _, keyFile := range privList { 101 | _, _, err := LoadPrivateKeyFile(keyFile) 102 | if err != nil { 103 | fmt.Println(keyFile) 104 | fmt.Println(err.Error()) 105 | t.FailNow() 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | package sshkey 2 | 3 | // This file contains utillity functions for decrypting password protecting keys 4 | // and password protecting keys. 5 | 6 | import ( 7 | "bufio" 8 | "crypto/aes" 9 | "crypto/cipher" 10 | "crypto/md5" 11 | "crypto/rand" 12 | "encoding/hex" 13 | "encoding/pem" 14 | "fmt" 15 | "io" 16 | "os" 17 | "strings" 18 | ) 19 | 20 | // The PasswordPrompt function is the function that is called to prompt the user for 21 | // a password. 22 | var PasswordPrompt func(prompt string) (password string, err error) = DefaultPasswordPrompt 23 | 24 | var ( 25 | ErrInvalidDEK = fmt.Errorf("sshkey: invalid DEK info") 26 | ErrUnableToDecrypt = fmt.Errorf("sshkey: unable to decrypt key") 27 | ) 28 | 29 | func decrypt(raw []byte, dekInfo string) (key []byte, err error) { 30 | dekInfoMap := strings.Split(dekInfo, ",") 31 | if len(dekInfoMap) != 2 { 32 | return nil, ErrInvalidDEK 33 | } 34 | algo := dekInfoMap[0] 35 | iv, err := hex.DecodeString(dekInfoMap[1]) 36 | if err != nil { 37 | return 38 | } 39 | 40 | password, err := PasswordPrompt("SSH key password: ") 41 | if err != nil { 42 | return 43 | } 44 | aeskey, err := opensshKDF(iv, []byte(password)) 45 | if err != nil { 46 | return 47 | } 48 | 49 | switch algo { 50 | case "AES-128-CBC": 51 | key, err = aesCBCdecrypt(aeskey, iv, raw) 52 | default: 53 | err = ErrUnableToDecrypt 54 | } 55 | return 56 | } 57 | 58 | func opensshKDF(iv []byte, password []byte) (key []byte, err error) { 59 | hash := md5.New() 60 | hash.Write(password) 61 | hash.Write(iv[:8]) 62 | key = hash.Sum(nil) 63 | return 64 | } 65 | 66 | // DefaultPasswordPrompt is a simple (but echoing) password entry function 67 | // that takes a prompt and reads the password. 68 | func DefaultPasswordPrompt(prompt string) (password string, err error) { 69 | fmt.Printf(prompt) 70 | rd := bufio.NewReader(os.Stdin) 71 | line, err := rd.ReadString('\n') 72 | if err != nil { 73 | return 74 | } 75 | password = strings.TrimSpace(line) 76 | return 77 | } 78 | 79 | func aesCBCdecrypt(aeskey, iv, ct []byte) (key []byte, err error) { 80 | c, err := aes.NewCipher(aeskey) 81 | if err != nil { 82 | return 83 | } 84 | 85 | cbc := cipher.NewCBCDecrypter(c, iv) 86 | key = make([]byte, len(ct)) 87 | cbc.CryptBlocks(key, ct) 88 | key = sshUnpad(key) 89 | return 90 | } 91 | 92 | // PKCS #5 padding scheme 93 | func sshUnpad(padded []byte) (unpadded []byte) { 94 | paddedLen := len(padded) 95 | var padnum int = int(padded[paddedLen-1]) 96 | stop := len(padded) - padnum 97 | return padded[:stop] 98 | } 99 | 100 | func sshPad(unpadded []byte) (padded []byte) { 101 | padLen := ((len(unpadded) + 15) / 16) * 16 102 | 103 | padded = make([]byte, padLen) 104 | padding := make([]byte, padLen-len(unpadded)) 105 | for i := 0; i < len(padding); i++ { 106 | padding[i] = byte(len(padding)) 107 | } 108 | 109 | copy(padded, unpadded) 110 | copy(padded[len(unpadded):], padding) 111 | return 112 | } 113 | 114 | func generateIV() (iv []byte, err error) { 115 | iv = make([]byte, aes.BlockSize) 116 | _, err = io.ReadFull(rand.Reader, iv) 117 | return 118 | } 119 | 120 | func aesCBCencrypt(aeskey, key, iv []byte) (ct []byte, err error) { 121 | c, err := aes.NewCipher(aeskey) 122 | if err != nil { 123 | return 124 | } 125 | 126 | cbc := cipher.NewCBCEncrypter(c, iv) 127 | ct = sshPad(key) 128 | cbc.CryptBlocks(ct, ct) 129 | return 130 | } 131 | 132 | func encryptKey(key []byte, password string) (cryptkey, iv []byte, err error) { 133 | iv, err = generateIV() 134 | if err != nil { 135 | return 136 | } 137 | 138 | aeskey, err := opensshKDF(iv, []byte(password)) 139 | if err != nil { 140 | return 141 | } 142 | 143 | cryptkey, err = aesCBCencrypt(aeskey, key, iv) 144 | return 145 | } 146 | 147 | func encrypt(key []byte, keytype Type, password string) (out []byte, err error) { 148 | cryptkey, iv, err := encryptKey(key, password) 149 | if err != nil { 150 | return 151 | } 152 | 153 | var block pem.Block 154 | switch keytype { 155 | case KEY_RSA: 156 | block.Type = "RSA PRIVATE KEY" 157 | case KEY_ECDSA: 158 | block.Type = "EC PRIVATE KEY" 159 | case KEY_DSA: 160 | block.Type = "DSA PRIVATE KEY" 161 | default: 162 | err = ErrInvalidPrivateKey 163 | return 164 | } 165 | block.Bytes = cryptkey 166 | block.Headers = make(map[string]string) 167 | block.Headers["Proc-Type"] = "4,ENCRYPTED" 168 | block.Headers["DEK-Info"] = fmt.Sprintf("AES-128-CBC,%X", iv) 169 | out = pem.EncodeToMemory(&block) 170 | return 171 | } 172 | -------------------------------------------------------------------------------- /sshkey.go: -------------------------------------------------------------------------------- 1 | package sshkey 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/dsa" 7 | "crypto/ecdsa" 8 | "crypto/elliptic" 9 | "crypto/md5" 10 | "crypto/rand" 11 | "crypto/rsa" 12 | "crypto/sha1" 13 | "crypto/sha256" 14 | "crypto/x509" 15 | "encoding/asn1" 16 | "encoding/base64" 17 | "encoding/binary" 18 | "encoding/pem" 19 | "fmt" 20 | "hash" 21 | "io" 22 | "io/ioutil" 23 | "math/big" 24 | "net/http" 25 | "regexp" 26 | "strings" 27 | ) 28 | 29 | var ( 30 | ErrInvalidDigest = fmt.Errorf("sshkey: invalid digest algorithm") 31 | ErrInvalidKeySize = fmt.Errorf("sshkey: invalid private key size") 32 | ErrInvalidPrivateKey = fmt.Errorf("sshkey: invalid private key") 33 | ErrInvalidPublicKey = fmt.Errorf("sshkey: invalid public key") 34 | ErrUnsupportedPublicKey = fmt.Errorf("sshkey: unsupported public key type") 35 | ErrUnsupportedPrivateKey = fmt.Errorf("sshkey: unsupported private key type") 36 | ) 37 | 38 | // PRNG contains the random data source to be used in key generation. It 39 | // defaults to crypto/rand.Reader. 40 | var PRNG io.Reader = rand.Reader 41 | 42 | // Representation of type of SSH Key 43 | type Type int 44 | 45 | // Representation of an SSH public key in the library. 46 | type SSHPublicKey struct { 47 | Type Type 48 | Key interface{} 49 | Comment string 50 | } 51 | 52 | // Given a private key and comment, NewPublic will return a new SSHPublicKey. 53 | func NewPublic(priv interface{}, comment string) *SSHPublicKey { 54 | pub := new(SSHPublicKey) 55 | switch priv.(type) { 56 | case *rsa.PrivateKey: 57 | rsapub := &priv.(*rsa.PrivateKey).PublicKey 58 | pub.Type = KEY_RSA 59 | pub.Key = rsapub 60 | pub.Comment = comment 61 | case *ecdsa.PrivateKey: 62 | ecpub := &priv.(*ecdsa.PrivateKey).PublicKey 63 | pub.Type = KEY_ECDSA 64 | pub.Key = ecpub 65 | pub.Comment = comment 66 | case *dsa.PrivateKey: 67 | dsapub := &priv.(*dsa.PrivateKey).PublicKey 68 | pub.Type = KEY_DSA 69 | pub.Key = dsapub 70 | pub.Comment = comment 71 | default: 72 | return nil 73 | } 74 | 75 | return pub 76 | } 77 | 78 | // These constants are used as the key type in the SSHPublicKey. 79 | const ( 80 | KEY_UNSUPPORTED Type = iota - 1 81 | KEY_ECDSA 82 | KEY_RSA 83 | KEY_DSA 84 | ) 85 | 86 | var pubkeyRegexp = regexp.MustCompile("(?m)^[a-z0-9-]+ (\\S+).*$") 87 | var commentRegexp = regexp.MustCompile("(?m)^[a-z0-9-]+ (\\S+) (\\S*)$") 88 | 89 | // fetchKey retrieves the raw data for a key, either via file or an HTTP get. 90 | func fetchKey(name string, local bool) (kb []byte, err error) { 91 | if local { 92 | kb, err = ioutil.ReadFile(name) 93 | } else { 94 | var resp *http.Response 95 | resp, err = http.Get(name) 96 | if err != nil { 97 | return 98 | } 99 | defer resp.Body.Close() 100 | 101 | kb, err = ioutil.ReadAll(resp.Body) 102 | } 103 | return 104 | } 105 | 106 | // LoadPublicKey loads an OpenSSH public key from a file or via HTTP. If 107 | // local is false, the key will be fetched over HTTP. 108 | func LoadPublicKeyFile(name string, local bool) (key *SSHPublicKey, err error) { 109 | kb64, err := fetchKey(name, local) 110 | return UnmarshalPublic(kb64) 111 | } 112 | 113 | // UnmarshalPublic decodes a byte slice containing an OpenSSH public key 114 | // into an public key. It supports RSA and ECDSA keys. 115 | func UnmarshalPublic(raw []byte) (key *SSHPublicKey, err error) { 116 | kb64 := pubkeyRegexp.ReplaceAll(raw, []byte("$1")) 117 | kb := make([]byte, base64.StdEncoding.DecodedLen(len(raw))) 118 | i, err := base64.StdEncoding.Decode(kb, kb64) 119 | if err != nil { 120 | return 121 | } 122 | kb = kb[:i] 123 | 124 | key = new(SSHPublicKey) 125 | if commentRegexp.Match(raw) { 126 | key.Comment = string(commentRegexp.ReplaceAll(raw, []byte("$3"))) 127 | key.Comment = strings.TrimSpace(key.Comment) 128 | } 129 | 130 | switch { 131 | case bytes.HasPrefix(raw, []byte("ssh-rsa")): 132 | key.Type = KEY_RSA 133 | key.Key, err = parseRSAPublicKey(kb) 134 | case bytes.HasPrefix(raw, []byte("ecdsa")): 135 | key.Type = KEY_ECDSA 136 | key.Key, err = parseECDSAPublicKey(kb) 137 | case bytes.HasPrefix(raw, []byte("ssh-dss")): 138 | key.Type = KEY_DSA 139 | key.Key, err = parseDSAPublicKey(kb) 140 | default: 141 | key.Type = KEY_UNSUPPORTED 142 | err = ErrUnsupportedPublicKey 143 | } 144 | return 145 | } 146 | 147 | // Load an OpenSSH private key from a file. 148 | func LoadPrivateKeyFile(name string) (key interface{}, keytype Type, err error) { 149 | kb, err := fetchKey(name, true) 150 | return UnmarshalPrivate(kb) 151 | } 152 | 153 | // Load an OpenSSH private key from a byte slice. 154 | func UnmarshalPrivate(raw []byte) (key interface{}, keytype Type, err error) { 155 | block, _ := pem.Decode(raw) 156 | if block == nil { 157 | err = ErrInvalidPrivateKey 158 | return 159 | } else { 160 | raw := block.Bytes 161 | if block.Headers != nil && len(block.Headers) != 0 { 162 | if dekInfo, ok := block.Headers["DEK-Info"]; ok { 163 | raw, err = decrypt(raw, dekInfo) 164 | if err != nil { 165 | return 166 | } 167 | } 168 | } 169 | switch block.Type { 170 | case "RSA PRIVATE KEY": 171 | keytype = KEY_RSA 172 | key, err = x509.ParsePKCS1PrivateKey(raw) 173 | if err == nil && key.(*rsa.PrivateKey).PublicKey.N.BitLen() < 2047 { 174 | fmt.Printf("[-] warning: SSH key is a weak key (consider ") 175 | fmt.Printf("upgrading to a 2048+ bit key.") 176 | } else if err != nil { 177 | err = ErrInvalidPrivateKey 178 | } 179 | case "EC PRIVATE KEY": 180 | keytype = KEY_ECDSA 181 | key, err = x509.ParseECPrivateKey(raw) 182 | if err != nil { 183 | err = ErrInvalidPrivateKey 184 | } 185 | case "DSA PRIVATE KEY": 186 | keytype = KEY_DSA 187 | k := struct { 188 | Version int 189 | P *big.Int 190 | Q *big.Int 191 | G *big.Int 192 | Priv *big.Int 193 | Pub *big.Int 194 | }{} 195 | 196 | var rest []byte 197 | rest, err = asn1.Unmarshal(raw, &k) 198 | 199 | if err != nil { 200 | err = ErrInvalidPrivateKey 201 | return 202 | } else if len(rest) > 0 { 203 | err = ErrInvalidPrivateKey 204 | return 205 | } 206 | 207 | key = &dsa.PrivateKey{ 208 | PublicKey: dsa.PublicKey{ 209 | Parameters: dsa.Parameters{ 210 | P: k.P, 211 | Q: k.Q, 212 | G: k.G, 213 | }, 214 | Y: k.Priv, 215 | }, 216 | X: k.Pub, 217 | } 218 | 219 | if key.(*dsa.PrivateKey).PublicKey.P.BitLen() < 1023 { 220 | fmt.Printf("[-] warning: SSH key is a weak key (consider ") 221 | fmt.Printf("upgrading to a 1023+ bit key.") 222 | } 223 | default: 224 | err = ErrUnsupportedPrivateKey 225 | return 226 | } 227 | } 228 | return 229 | } 230 | 231 | func parseRSAPublicKey(raw []byte) (key *rsa.PublicKey, err error) { 232 | buf := bytes.NewBuffer(raw) 233 | var algorithm, exponent, modulus []byte 234 | var length int32 235 | 236 | err = binary.Read(buf, binary.BigEndian, &length) 237 | if err != nil { 238 | return 239 | } 240 | 241 | algorithm = make([]byte, length) 242 | _, err = io.ReadFull(buf, algorithm) 243 | if err != nil { 244 | return 245 | } 246 | if string(algorithm) != "ssh-rsa" { 247 | err = ErrInvalidPublicKey 248 | return 249 | } 250 | 251 | err = binary.Read(buf, binary.BigEndian, &length) 252 | if err != nil { 253 | return 254 | } 255 | exponent = make([]byte, length) 256 | _, err = io.ReadFull(buf, exponent) 257 | if err != nil { 258 | return 259 | } 260 | 261 | err = binary.Read(buf, binary.BigEndian, &length) 262 | if err != nil { 263 | return 264 | } 265 | modulus = make([]byte, length) 266 | _, err = io.ReadFull(buf, modulus) 267 | if err != nil { 268 | return 269 | } 270 | 271 | key = new(rsa.PublicKey) 272 | key.N = new(big.Int).SetBytes(modulus) 273 | key.E = int(new(big.Int).SetBytes(exponent).Int64()) 274 | if key.N.BitLen() < 2047 { 275 | fmt.Printf("[-] warning: SSH key is a weak key (consider ") 276 | fmt.Println("upgrading to a 2048+ bit key).") 277 | } 278 | return 279 | } 280 | 281 | func parseECDSAPublicKey(raw []byte) (key *ecdsa.PublicKey, err error) { 282 | buf := bytes.NewBuffer(raw) 283 | var algorithm, curveName, public []byte 284 | var length int32 285 | 286 | err = binary.Read(buf, binary.BigEndian, &length) 287 | if err != nil { 288 | return 289 | } 290 | 291 | algorithm = make([]byte, length) 292 | _, err = io.ReadFull(buf, algorithm) 293 | if err != nil { 294 | return 295 | } 296 | 297 | err = binary.Read(buf, binary.BigEndian, &length) 298 | if err != nil { 299 | return 300 | } 301 | curveName = make([]byte, length) 302 | _, err = io.ReadFull(buf, curveName) 303 | if err != nil { 304 | return 305 | } 306 | 307 | err = binary.Read(buf, binary.BigEndian, &length) 308 | if err != nil { 309 | return 310 | } 311 | public = make([]byte, length) 312 | _, err = io.ReadFull(buf, public) 313 | if err != nil { 314 | return 315 | } 316 | 317 | key = new(ecdsa.PublicKey) 318 | var curve elliptic.Curve 319 | switch string(curveName) { 320 | case "nistp256": 321 | curve = elliptic.P256() 322 | case "nistp384": 323 | curve = elliptic.P384() 324 | case "nistp521": 325 | curve = elliptic.P521() 326 | default: 327 | err = ErrUnsupportedPublicKey 328 | return 329 | } 330 | 331 | key.X, key.Y = elliptic.Unmarshal(curve, public) 332 | if key.X == nil { 333 | err = ErrInvalidPublicKey 334 | return 335 | } 336 | key.Curve = curve 337 | return 338 | } 339 | 340 | func parseDSAPublicKey(raw []byte) (key *dsa.PublicKey, err error) { 341 | buf := bytes.NewBuffer(raw) 342 | var algorithm []byte 343 | var length int32 344 | 345 | err = binary.Read(buf, binary.BigEndian, &length) 346 | if err != nil { 347 | return 348 | } 349 | 350 | algorithm = make([]byte, length) 351 | _, err = io.ReadFull(buf, algorithm) 352 | if err != nil { 353 | return 354 | } 355 | 356 | if string(algorithm) != "ssh-dss" { 357 | err = ErrInvalidPublicKey 358 | return 359 | } 360 | 361 | parseInt := func(in io.Reader) (*big.Int, error) { 362 | var length int32 363 | if err := binary.Read(in, binary.BigEndian, &length); err != nil { 364 | return nil, err 365 | } 366 | val := make([]byte, length) 367 | if _, err := io.ReadFull(in, val); err != nil { 368 | return nil, err 369 | } 370 | 371 | return new(big.Int).SetBytes(val), nil 372 | } 373 | 374 | key = new(dsa.PublicKey) 375 | 376 | key.P, err = parseInt(buf) 377 | if err != nil { 378 | return 379 | } 380 | 381 | key.Q, err = parseInt(buf) 382 | if err != nil { 383 | return 384 | } 385 | 386 | key.G, err = parseInt(buf) 387 | if err != nil { 388 | return 389 | } 390 | 391 | key.Y, err = parseInt(buf) 392 | if err != nil { 393 | return 394 | } 395 | 396 | return 397 | 398 | } 399 | 400 | func uint32ToBlob(n uint32) []byte { 401 | buf := new(bytes.Buffer) 402 | err := binary.Write(buf, binary.BigEndian, n) 403 | if err != nil { 404 | return nil 405 | } 406 | return buf.Bytes() 407 | } 408 | 409 | func curveName(curve elliptic.Curve) []byte { 410 | switch curve { 411 | case elliptic.P256(): 412 | return []byte("nistp256") 413 | case elliptic.P384(): 414 | return []byte("nistp384") 415 | case elliptic.P521(): 416 | return []byte("nistp521") 417 | default: 418 | return nil 419 | } 420 | } 421 | 422 | func publicToBlob(pub *SSHPublicKey) ([]byte, error) { 423 | buf := new(bytes.Buffer) 424 | 425 | switch pub.Key.(type) { 426 | case *rsa.PublicKey: 427 | rsapub := pub.Key.(*rsa.PublicKey) 428 | tag1 := uint32ToBlob(7) // 7 characters for 'ssh-rsa' 429 | if tag1 == nil { 430 | return nil, ErrInvalidPublicKey 431 | } 432 | buf.Write(tag1) 433 | buf.Write([]byte("ssh-rsa")) 434 | 435 | E := big.NewInt(int64(rsapub.E)).Bytes() 436 | tag2 := uint32ToBlob(uint32(len(E))) 437 | if tag2 == nil { 438 | return nil, ErrInvalidPublicKey 439 | } 440 | buf.Write(tag2) 441 | buf.Write(E) 442 | 443 | N := rsapub.N.Bytes() 444 | tag3 := uint32ToBlob(uint32(len(N) + 1)) 445 | if tag3 == nil { 446 | return nil, ErrInvalidPublicKey 447 | } 448 | buf.Write(tag3) 449 | buf.Write([]byte{0}) 450 | buf.Write(N) 451 | case *ecdsa.PublicKey: 452 | ecpub := pub.Key.(*ecdsa.PublicKey) 453 | cname := curveName(ecpub.Curve) 454 | if cname == nil { 455 | return nil, ErrInvalidPublicKey 456 | } 457 | algo := []byte(fmt.Sprintf("ecdsa-sha2-%s", string(cname))) 458 | tag1 := uint32ToBlob(uint32(len(algo))) 459 | if tag1 == nil { 460 | return nil, ErrInvalidPublicKey 461 | } 462 | buf.Write(tag1) 463 | buf.Write(algo) 464 | 465 | tag2 := uint32ToBlob(uint32(len(cname))) 466 | if tag2 == nil { 467 | return nil, ErrInvalidPublicKey 468 | } 469 | buf.Write(tag2) 470 | buf.Write(cname) 471 | 472 | pubkey := elliptic.Marshal(ecpub.Curve, ecpub.X, ecpub.Y) 473 | if pubkey == nil { 474 | return nil, ErrInvalidPublicKey 475 | } 476 | tag3 := uint32ToBlob(uint32(len(pubkey))) 477 | if tag3 == nil { 478 | return nil, ErrInvalidPublicKey 479 | } 480 | buf.Write(tag3) 481 | buf.Write(pubkey) 482 | case *dsa.PublicKey: 483 | dsapub := pub.Key.(*dsa.PublicKey) 484 | tag1 := uint32ToBlob(7) // 7 characters for 'ssh-rsa' 485 | if tag1 == nil { 486 | return nil, ErrInvalidPublicKey 487 | } 488 | buf.Write(tag1) 489 | buf.Write([]byte("ssh-dss")) 490 | 491 | P := dsapub.P.Bytes() 492 | tag2 := uint32ToBlob(uint32(len(P) + 1)) 493 | if tag2 == nil { 494 | return nil, ErrInvalidPublicKey 495 | } 496 | buf.Write(tag2) 497 | buf.Write([]byte{0}) 498 | buf.Write(P) 499 | 500 | Q := dsapub.Q.Bytes() 501 | tag3 := uint32ToBlob(uint32(len(Q) + 1)) 502 | if tag3 == nil { 503 | return nil, ErrInvalidPublicKey 504 | } 505 | buf.Write(tag3) 506 | buf.Write([]byte{0}) 507 | buf.Write(Q) 508 | 509 | G := dsapub.G.Bytes() 510 | tag4 := uint32ToBlob(uint32(len(G))) 511 | if tag4 == nil { 512 | return nil, ErrInvalidPublicKey 513 | } 514 | buf.Write(tag4) 515 | buf.Write(G) 516 | 517 | Y := dsapub.Y.Bytes() 518 | tag5 := uint32ToBlob(uint32(len(Y))) 519 | if tag5 == nil { 520 | return nil, ErrInvalidPublicKey 521 | } 522 | buf.Write(tag5) 523 | buf.Write(Y) 524 | 525 | default: 526 | return nil, ErrInvalidPublicKey 527 | } 528 | 529 | return buf.Bytes(), nil 530 | } 531 | 532 | // Given a private key and a (possibly empty) password, returns a byte 533 | // slice containing a PEM-encoded private key in the appropriate 534 | // OpenSSH format. 535 | func MarshalPrivate(priv interface{}, password string) (out []byte, err error) { 536 | var ( 537 | keytype Type 538 | der []byte 539 | btype string 540 | ) 541 | 542 | switch priv.(type) { 543 | case *rsa.PrivateKey: 544 | keytype = KEY_RSA 545 | der = x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey)) 546 | if der == nil { 547 | err = ErrInvalidPrivateKey 548 | return 549 | } 550 | btype = "RSA PRIVATE KEY" 551 | case *ecdsa.PrivateKey: 552 | keytype = KEY_ECDSA 553 | der, err = marshalECDSAKey(priv.(*ecdsa.PrivateKey)) 554 | btype = "EC PRIVATE KEY" 555 | case *dsa.PrivateKey: 556 | keytype = KEY_DSA 557 | 558 | dsakey := priv.(*dsa.PrivateKey) 559 | k := struct { 560 | Version int 561 | P *big.Int 562 | Q *big.Int 563 | G *big.Int 564 | Priv *big.Int 565 | Pub *big.Int 566 | }{ 567 | Version: 1, 568 | P: dsakey.PublicKey.P, 569 | Q: dsakey.PublicKey.Q, 570 | G: dsakey.PublicKey.G, 571 | Priv: dsakey.PublicKey.Y, 572 | Pub: dsakey.X, 573 | } 574 | der, err = asn1.Marshal(k) 575 | if err != nil { 576 | return 577 | } 578 | btype = "DSA PRIVATE KEY" 579 | default: 580 | err = ErrInvalidPrivateKey 581 | return 582 | } 583 | 584 | if password != "" { 585 | out, err = encrypt(der, keytype, password) 586 | return 587 | } 588 | var block pem.Block 589 | block.Type = btype 590 | block.Bytes = der 591 | out = pem.EncodeToMemory(&block) 592 | return 593 | } 594 | 595 | type ecPrivateKey struct { 596 | Version int 597 | PrivateKey []byte 598 | NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` 599 | PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` 600 | } 601 | 602 | var ( 603 | oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33} 604 | oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7} 605 | oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34} 606 | oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35} 607 | ) 608 | 609 | func marshalECDSAKey(priv *ecdsa.PrivateKey) (out []byte, err error) { 610 | var eckey ecPrivateKey 611 | 612 | eckey.Version = 1 613 | eckey.PrivateKey = priv.D.Bytes() 614 | switch priv.PublicKey.Curve { 615 | case elliptic.P256(): 616 | eckey.NamedCurveOID = oidNamedCurveP256 617 | case elliptic.P384(): 618 | eckey.NamedCurveOID = oidNamedCurveP384 619 | case elliptic.P521(): 620 | eckey.NamedCurveOID = oidNamedCurveP521 621 | default: 622 | err = ErrInvalidPrivateKey 623 | } 624 | 625 | pkey := elliptic.Marshal(priv.PublicKey.Curve, priv.PublicKey.X, 626 | priv.PublicKey.Y) 627 | if pkey == nil { 628 | err = ErrInvalidPrivateKey 629 | return 630 | } 631 | 632 | eckey.PublicKey = asn1.BitString{ 633 | BitLength: len(pkey) * 8, 634 | Bytes: pkey, 635 | } 636 | out, err = asn1.Marshal(eckey) 637 | return 638 | } 639 | 640 | // MarshalPublic returns a byte slice containing an OpenSSH public key built 641 | // from the SSHPublicKey. 642 | func MarshalPublic(pub *SSHPublicKey) (out []byte) { 643 | blob, err := publicToBlob(pub) 644 | if err != nil { 645 | return nil 646 | } 647 | encodedBlob := base64.StdEncoding.EncodeToString(blob) 648 | 649 | var algo string 650 | 651 | switch pub.Type { 652 | case KEY_RSA: 653 | algo = "ssh-rsa" 654 | case KEY_ECDSA: 655 | algo = fmt.Sprintf("ecdsa-sha2-%s", 656 | curveName(pub.Key.(*ecdsa.PublicKey).Curve)) 657 | default: 658 | return nil 659 | } 660 | 661 | out = []byte(fmt.Sprintf("%s %s %s", algo, encodedBlob, pub.Comment)) 662 | return 663 | } 664 | 665 | // Return the bitsize of the underlying public key. 666 | func (key *SSHPublicKey) Size() int { 667 | switch key.Type { 668 | case KEY_RSA: 669 | return key.Key.(*rsa.PublicKey).N.BitLen() 670 | case KEY_ECDSA: 671 | return key.Key.(*ecdsa.PublicKey).Curve.Params().BitSize 672 | case KEY_DSA: 673 | return key.Key.(*dsa.PublicKey).P.BitLen() 674 | default: 675 | return 0 676 | } 677 | } 678 | 679 | // Generates a compatible OpenSSH private key. The key is in the 680 | // raw Go key format. To convert this to a PEM encoded key, see 681 | // MarshalPrivate. 682 | func GenerateKey(keytype Type, size int) (key interface{}, err error) { 683 | switch keytype { 684 | case KEY_RSA: 685 | if size < 2048 { 686 | return nil, ErrInvalidKeySize 687 | } 688 | var rsakey *rsa.PrivateKey 689 | rsakey, err = rsa.GenerateKey(PRNG, size) 690 | if err != nil { 691 | return 692 | } 693 | key = rsakey 694 | case KEY_ECDSA: 695 | var eckey *ecdsa.PrivateKey 696 | switch size { 697 | case 256: 698 | eckey, err = ecdsa.GenerateKey(elliptic.P256(), PRNG) 699 | case 384: 700 | eckey, err = ecdsa.GenerateKey(elliptic.P384(), PRNG) 701 | case 521: 702 | eckey, err = ecdsa.GenerateKey(elliptic.P521(), PRNG) 703 | default: 704 | return nil, ErrInvalidKeySize 705 | } 706 | key = eckey 707 | case KEY_DSA: 708 | var sizes dsa.ParameterSizes 709 | switch size { 710 | case 1024: 711 | sizes = dsa.L1024N160 712 | case 2048: 713 | sizes = dsa.L2048N256 714 | case 3072: 715 | sizes = dsa.L3072N256 716 | default: 717 | err = ErrInvalidKeySize 718 | return 719 | } 720 | 721 | params := dsa.Parameters{} 722 | err = dsa.GenerateParameters(¶ms, rand.Reader, sizes) 723 | if err != nil { 724 | return 725 | } 726 | 727 | dsakey := &dsa.PrivateKey{ 728 | PublicKey: dsa.PublicKey{ 729 | Parameters: params, 730 | }, 731 | } 732 | err = dsa.GenerateKey(dsakey, rand.Reader) 733 | if err != nil { 734 | return 735 | } 736 | key = dsakey 737 | } 738 | 739 | return 740 | } 741 | 742 | // Return the fingerprint of the key in a raw format. 743 | func Fingerprint(pub *SSHPublicKey, hashalgo crypto.Hash) (fpr []byte, err error) { 744 | var h hash.Hash 745 | 746 | // The default algorithm for OpenSSH appears to be MD5. 747 | if hashalgo == 0 { 748 | hashalgo = crypto.MD5 749 | } 750 | 751 | switch hashalgo { 752 | case crypto.MD5: 753 | h = md5.New() 754 | case crypto.SHA1: 755 | h = sha1.New() 756 | case crypto.SHA256: 757 | h = sha256.New() 758 | default: 759 | return nil, ErrInvalidDigest 760 | } 761 | 762 | blob, err := publicToBlob(pub) 763 | if err != nil { 764 | return nil, err 765 | } 766 | h.Write(blob) 767 | 768 | return h.Sum(nil), nil 769 | } 770 | 771 | // Return a string containing a printable form of the key's fingerprint. 772 | func FingerprintPretty(pub *SSHPublicKey, hashalgo crypto.Hash) (fpr string, err error) { 773 | fprBytes, err := Fingerprint(pub, hashalgo) 774 | if err != nil { 775 | return 776 | } 777 | 778 | for _, v := range fprBytes { 779 | fpr += fmt.Sprintf("%02x:", v) 780 | } 781 | fpr = fpr[:len(fpr)-1] 782 | return 783 | } 784 | --------------------------------------------------------------------------------