|
1 | 1 | package sshutils |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "crypto/ecdsa" |
| 5 | + "crypto/elliptic" |
4 | 6 | "crypto/rand" |
| 7 | + "crypto/x509" |
| 8 | + "encoding/pem" |
5 | 9 | "fmt" |
6 | 10 | "io/ioutil" |
| 11 | + "os" |
| 12 | + "os/exec" |
| 13 | + "strings" |
7 | 14 |
|
8 | | - "github.com/ScaleFT/sshkeys" |
9 | | - "github.com/keybase/bot-sshca/src/shared" |
10 | | - "golang.org/x/crypto/ed25519" |
11 | 15 | "golang.org/x/crypto/ssh" |
| 16 | + |
| 17 | + "github.com/keybase/bot-sshca/src/shared" |
12 | 18 | ) |
13 | 19 |
|
14 | | -// Generate a new SSH key. Places the private key at filename and the public key at filename.pub. |
15 | | -// We use ed25519 keys since they may be more secure (and are smaller). The go crypto ssh library |
16 | | -// does not support marshalling ed25519 keys so we use ScaleFT/sshkeys to marshal them to the |
17 | | -// correct on disk format for SSH |
| 20 | +// Generate a new SSH key and store the private key at filename and the public key at filename.pub |
| 21 | +// If the ssh-keygen binary exists, generates an ed25519 ssh key using ssh-keygen. Otherwise, |
| 22 | +// generates an ecdsa key using go's crypto library. Note that we use ecdsa rather than ed25519 |
| 23 | +// in this case since go's crypto library does not support marshalling ed25519 keys into the format |
| 24 | +// expected by openssh. github.com/ScaleFT/sshkeys claims to support this but does not reliably |
| 25 | +// work with all versions of ssh. |
18 | 26 | func generateNewSSHKey(filename string) error { |
19 | | - // Generate the key |
20 | | - pub, private, err := ed25519.GenerateKey(rand.Reader) |
| 27 | + if sshKeygenBinaryExists() { |
| 28 | + return generateNewSSHKeyEd25519(filename) |
| 29 | + } |
| 30 | + |
| 31 | + return generateNewSSHKeyEcdsa(filename) |
| 32 | +} |
| 33 | + |
| 34 | +// Returns true iff the ssh-keygen binary exists and is in the user's path |
| 35 | +func sshKeygenBinaryExists() bool { |
| 36 | + _, err := exec.LookPath("ssh-keygen") |
| 37 | + return err == nil |
| 38 | +} |
| 39 | + |
| 40 | +// Generate an ed25519 ssh key via ssh-keygen. Stores the private key at filename and the public key at filename.pub |
| 41 | +func generateNewSSHKeyEd25519(filename string) error { |
| 42 | + cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", filename, "-m", "PEM", "-N", "") |
| 43 | + bytes, err := cmd.CombinedOutput() |
21 | 44 | if err != nil { |
22 | | - return fmt.Errorf("failed to generate ed25519 key: %v", err) |
| 45 | + return fmt.Errorf("ssh-keygen failed: %s (%v)", strings.TrimSpace(string(bytes)), err) |
23 | 46 | } |
| 47 | + return nil |
| 48 | +} |
24 | 49 |
|
25 | | - // Write the private key |
26 | | - bytes, err := sshkeys.Marshal(private, &sshkeys.MarshalOptions{Format: sshkeys.FormatOpenSSHv1}) |
| 50 | +// Generate an ecdsa ssh key in pure go code. Stores the private key at filename and the public key at filename.pub |
| 51 | +// Note that if you are editing this code, be careful to ensure you test it manually since the integration tests |
| 52 | +// run in an environment with ssh-keygen and thus do not call this function. This function is manually used on windows. |
| 53 | +func generateNewSSHKeyEcdsa(filename string) error { |
| 54 | + // ssh-keygen -t ecdsa uses P256 by default |
| 55 | + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
27 | 56 | if err != nil { |
28 | | - return fmt.Errorf("failed to marshal ed25519 key: %v", err) |
| 57 | + return err |
29 | 58 | } |
30 | | - err = ioutil.WriteFile(filename, bytes, 0600) |
| 59 | + |
| 60 | + // 0600 are the correct permissions for an ssh private key |
| 61 | + privateKeyFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) |
31 | 62 | if err != nil { |
32 | | - return fmt.Errorf("failed to write ssh private key to %s: %v", filename, err) |
| 63 | + return err |
33 | 64 | } |
| 65 | + defer privateKeyFile.Close() |
34 | 66 |
|
35 | | - // Write the public key |
36 | | - publicKey, err := ssh.NewPublicKey(pub) |
| 67 | + bytes, err := x509.MarshalECPrivateKey(privateKey) |
37 | 68 | if err != nil { |
38 | | - return fmt.Errorf("failed to create public key from ed25519 key: %v", err) |
| 69 | + return err |
39 | 70 | } |
40 | | - bytes = ssh.MarshalAuthorizedKey(publicKey) |
41 | | - err = ioutil.WriteFile(shared.KeyPathToPubKey(filename), bytes, 0600) |
| 71 | + |
| 72 | + privateKeyPEM := &pem.Block{Type: "EC PRIVATE KEY", Bytes: bytes} |
| 73 | + err = pem.Encode(privateKeyFile, privateKeyPEM) |
42 | 74 | if err != nil { |
43 | | - return fmt.Errorf("failed to write ssh public key to %s: %v", shared.KeyPathToPubKey(filename), err) |
| 75 | + return err |
44 | 76 | } |
45 | 77 |
|
46 | | - return nil |
| 78 | + pub, err := ssh.NewPublicKey(&privateKey.PublicKey) |
| 79 | + if err != nil { |
| 80 | + return err |
| 81 | + } |
| 82 | + return ioutil.WriteFile(shared.KeyPathToPubKey(filename), ssh.MarshalAuthorizedKey(pub), 0600) |
47 | 83 | } |
0 commit comments