-
Notifications
You must be signed in to change notification settings - Fork 12
/
keygen.go
287 lines (246 loc) · 7.12 KB
/
keygen.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// Package keygen handles the creation of new SSH key pairs.
package keygen
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"github.com/mikesmitty/edkey"
"github.com/mitchellh/go-homedir"
"golang.org/x/crypto/ssh"
)
const rsaDefaultBits = 4096
// ErrMissingSSHKeys indicates we're missing some keys that we expected to
// have after generating. This should be an extreme edge case.
var ErrMissingSSHKeys = errors.New("missing one or more keys; did something happen to them after they were generated?")
// FilesystemErr is used to signal there was a problem creating keys at the
// filesystem-level. For example, when we're unable to create a directory to
// store new SSH keys in.
type FilesystemErr struct {
Err error
}
// Error returns a human-readable string for the erorr. It implements the error
// interface.
func (e FilesystemErr) Error() string {
return e.Err.Error()
}
// Unwrap returne the underlying error.
func (e FilesystemErr) Unwrap() error {
return e.Err
}
// SSHKeysAlreadyExistErr indicates that files already exist at the location at
// which we're attempting to create SSH keys.
type SSHKeysAlreadyExistErr struct {
Path string
}
// SSHKeyPair holds a pair of SSH keys and associated methods.
type SSHKeyPair struct {
PrivateKeyPEM []byte
PublicKey []byte
KeyDir string
Filename string // private key filename; public key will have .pub appended
}
func (s SSHKeyPair) privateKeyPath() string {
return filepath.Join(s.KeyDir, s.Filename)
}
func (s SSHKeyPair) publicKeyPath() string {
return filepath.Join(s.KeyDir, s.Filename+".pub")
}
// NewSSHKeyPair generates an SSHKeyPair, which contains a pair of SSH keys.
// The keys are written to disk.
func NewSSHKeyPair(path string, name string, passphrase []byte, keyType string) (*SSHKeyPair, error) {
var err error
s := &SSHKeyPair{
KeyDir: path,
Filename: fmt.Sprintf("%s_%s", name, keyType),
}
if s.KeyPairExist() {
pubData, err := ioutil.ReadFile(s.publicKeyPath())
if err != nil {
return nil, err
}
s.PublicKey = pubData
privData, err := ioutil.ReadFile(s.privateKeyPath())
if err != nil {
return nil, err
}
s.PrivateKeyPEM = privData
return s, nil
}
switch keyType {
case "ed25519":
err = s.generateEd25519Keys()
case "rsa":
err = s.generateRSAKeys(rsaDefaultBits, passphrase)
default:
return nil, fmt.Errorf("unsupported key type %s", keyType)
}
if err != nil {
return nil, err
}
return s, nil
}
// generateEd25519Keys creates a pair of EdD25519 keys for SSH auth.
func (s *SSHKeyPair) generateEd25519Keys() error {
// Generate keys
pubKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
// Encode PEM
pemBlock := pem.EncodeToMemory(&pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: edkey.MarshalED25519PrivateKey(privateKey),
})
// Prepare public key
publicKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
return err
}
// serialize for public key file on disk
serializedPublicKey := ssh.MarshalAuthorizedKey(publicKey)
s.PrivateKeyPEM = pemBlock
s.PublicKey = pubKeyWithMemo(serializedPublicKey)
return nil
}
// generateRSAKeys creates a pair for RSA keys for SSH auth.
func (s *SSHKeyPair) generateRSAKeys(bitSize int, passphrase []byte) error {
// Generate private key
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
return err
}
// Validate private key
err = privateKey.Validate()
if err != nil {
return err
}
// Get ASN.1 DER format
x509Encoded := x509.MarshalPKCS1PrivateKey(privateKey)
block := &pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509Encoded,
}
// encrypt private key with passphrase
if len(passphrase) > 0 {
block, err = x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, passphrase, x509.PEMCipherAES256)
if err != nil {
return err
}
}
// Private key in PEM format
pemBlock := pem.EncodeToMemory(block)
// Generate public key
publicRSAKey, err := ssh.NewPublicKey(privateKey.Public())
if err != nil {
return err
}
// serialize for public key file on disk
serializedPubKey := ssh.MarshalAuthorizedKey(publicRSAKey)
s.PrivateKeyPEM = pemBlock
s.PublicKey = pubKeyWithMemo(serializedPubKey)
return nil
}
// prepFilesystem makes sure the state of the filesystem is as it needs to be
// in order to write our keys to disk. It will create and/or set permissions on
// the SSH directory we're going to write our keys to (for example, ~/.ssh) as
// well as make sure that no files exist at the location in which we're going
// to write out keys.
func (s *SSHKeyPair) prepFilesystem() error {
var err error
s.KeyDir, err = homedir.Expand(s.KeyDir)
if err != nil {
return err
}
info, err := os.Stat(s.KeyDir)
if os.IsNotExist(err) {
// Directory doesn't exist: create it
return os.MkdirAll(s.KeyDir, 0700)
}
if err != nil {
// There was another error statting the directory; something is awry
return FilesystemErr{Err: err}
}
if !info.IsDir() {
// It exists but it's not a directory
return FilesystemErr{Err: fmt.Errorf("%s is not a directory", s.KeyDir)}
}
if info.Mode().Perm() != 0700 {
// Permissions are wrong: fix 'em
if err := os.Chmod(s.KeyDir, 0700); err != nil {
return FilesystemErr{Err: err}
}
}
// Make sure the files we're going to write to don't already exist
if fileExists(s.privateKeyPath()) {
return SSHKeysAlreadyExistErr{Path: s.privateKeyPath()}
}
if fileExists(s.publicKeyPath()) {
return SSHKeysAlreadyExistErr{Path: s.publicKeyPath()}
}
// The directory looks good as-is
return nil
}
// WriteKeys writes the SSH key pair to disk.
func (s *SSHKeyPair) WriteKeys() error {
if len(s.PrivateKeyPEM) == 0 || len(s.PublicKey) == 0 {
return ErrMissingSSHKeys
}
if err := s.prepFilesystem(); err != nil {
return err
}
if err := writeKeyToFile(s.PrivateKeyPEM, s.privateKeyPath()); err != nil {
return err
}
if err := writeKeyToFile(s.PublicKey, s.publicKeyPath()); err != nil {
return err
}
return nil
}
func (s *SSHKeyPair) KeyPairExist() bool {
return fileExists(s.privateKeyPath()) && fileExists(s.publicKeyPath())
}
func writeKeyToFile(keyBytes []byte, path string) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return ioutil.WriteFile(path, keyBytes, 0600)
}
return FilesystemErr{Err: fmt.Errorf("file %s already exists", path)}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return false
}
if err != nil {
return false
}
return true
}
// attaches a user@host suffix to a serialized public key. returns the original
// pubkey if we can't get the username or host.
func pubKeyWithMemo(pubKey []byte) []byte {
u, err := user.Current()
if err != nil {
return pubKey
}
hostname, err := os.Hostname()
if err != nil {
return pubKey
}
return append(bytes.TrimRight(pubKey, "\n"), []byte(fmt.Sprintf(" %s@%s\n", u.Username, hostname))...)
}
// Error returns the a human-readable error message for SSHKeysAlreadyExistErr.
// It satisfies the error interface.
func (e SSHKeysAlreadyExistErr) Error() string {
return fmt.Sprintf("ssh key %s already exists", e.Path)
}