/
kssh.go
346 lines (311 loc) · 10.2 KB
/
kssh.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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"time"
"github.com/keybase/bot-sshca/src/keybaseca/sshutils"
"github.com/google/uuid"
"github.com/keybase/bot-sshca/src/kssh"
"github.com/keybase/bot-sshca/src/shared"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
func main() {
kssh.InitLogging()
botName, remainingArgs, action, err := handleArgs(os.Args[1:])
if err != nil {
fmt.Printf("Failed to parse arguments: %v\n", err)
os.Exit(1)
}
keyPath, err := getSignedKeyLocation(botName)
if err != nil {
fmt.Printf("Failed to retrieve location to store SSH keys: %v\n", err)
os.Exit(1)
}
if isValidCert(keyPath) {
log.WithField("keyPath", keyPath).Debug("Reusing unexpired certificate")
doAction(action, keyPath, remainingArgs)
os.Exit(0)
}
err = provisionNewKey(botName, keyPath)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
doAction(action, keyPath, remainingArgs)
}
func doAction(action Action, keyPath string, remainingArgs []string) {
if action == SSH {
runSSHWithKey(keyPath, remainingArgs)
} else if action == Provision {
provision(keyPath)
}
}
func provision(keyPath string) {
err := kssh.AddKeyToSSHAgent(keyPath)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
user, err := kssh.GetDefaultSSHUser()
if err != nil {
fmt.Printf("Failed to retrieve default SSH user: %v\n", err)
os.Exit(1)
}
err = kssh.CreateDefaultUserConfigFile(keyPath)
if err != nil {
fmt.Printf("Failed to create the ssh config file for the default user: %v\n", err)
os.Exit(1)
}
fmt.Printf("Provisioned new SSH key at %s\n", keyPath)
if user != "" {
fmt.Println("See docs/troubleshooting.md for information on configuring scp, rsync, etc to " +
"use the configured kssh default user")
}
}
// getSignedKeyLocation returns the path of where the signed SSH key should be stored. botName is the name of the bot
// specified via --bot if specified. It is necessary to include the bot in the filename in order to properly
// handle how the switch bot flow interacts with the isValidCert function
func getSignedKeyLocation(botName string) (string, error) {
signedKeyLocation := shared.ExpandPathWithTilde("~/.ssh/keybase-signed-key--")
if botName != "" {
return signedKeyLocation + botName, nil
}
defaultBot, _, err := kssh.GetDefaultBotAndTeam()
if err != nil {
return "", err
}
return signedKeyLocation + defaultBot, nil
}
var cliArguments = []kssh.CLIArgument{
{Name: "--set-default-bot", HasArgument: true},
{Name: "--clear-default-bot", HasArgument: false},
{Name: "--bot", HasArgument: true},
{Name: "--provision", HasArgument: false},
{Name: "--set-default-user", HasArgument: true},
{Name: "--clear-default-user", HasArgument: false},
{Name: "--help", HasArgument: false},
{Name: "-v", HasArgument: false, Preserve: true},
{Name: "--set-keybase-binary", HasArgument: true},
}
var VersionNumber = "master"
func generateHelpPage() string {
return fmt.Sprintf(`NAME:
kssh - A replacement ssh binary using Keybase SSH CA to provision SSH keys
USAGE:
kssh [kssh options] [ssh arguments...]
VERSION:
%s
GLOBAL OPTIONS:
--help Show help
-v Enable kssh and ssh debug logs
--provision Provision a new SSH key and add it to the ssh-agent. Useful if you need to run another
program that uses SSH auth (eg scp, rsync, etc)
--set-default-bot Set the default bot to be used for kssh. Not necessary if you are only in one team that
is using Keybase SSH CA
--clear-default-bot Clear the default bot
--bot Specify a specific bot to be used for kssh. Not necessary if you are only in one team that
is using Keybase SSH CA
--set-default-user Set the default SSH user to be used for kssh. Useful if you use ssh configs that do not set
a default SSH user
--clear-default-user Clear the default SSH user
--set-keybase-binary Run kssh with a specific keybase binary rather than resolving via $PATH `, VersionNumber)
}
type Action int
const (
Provision Action = iota
SSH
)
// Returns botName, remaining arguments, action, error
// If the argument requires exiting after processing, it will call os.Exit
func handleArgs(args []string) (string, []string, Action, error) {
remaining, found, err := kssh.ParseArgs(args, cliArguments)
if err != nil {
return "", nil, 0, fmt.Errorf("Failed to parse provided arguments: %v", err)
}
botName := ""
action := SSH
for _, arg := range found {
if arg.Argument.Name == "--bot" {
botName = arg.Value
}
if arg.Argument.Name == "--set-default-user" {
err := kssh.SetDefaultSSHUser(arg.Value)
if err != nil {
fmt.Printf("Failed to set the default ssh user: %v\n", err)
os.Exit(1)
}
fmt.Println("Set default ssh user, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--clear-default-user" {
err := kssh.SetDefaultSSHUser("")
if err != nil {
fmt.Printf("Failed to clear the default ssh user: %v\n", err)
os.Exit(1)
}
fmt.Println("Cleared default ssh user, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--set-default-bot" {
// We exit immediately after setting the default bot
err := kssh.SetDefaultBot(arg.Value)
if err != nil {
fmt.Printf("Failed to set the default bot: %v\n", err)
os.Exit(1)
}
fmt.Println("Set default bot, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--clear-default-bot" {
err := kssh.ClearDefaultBot()
if err != nil {
fmt.Printf("Failed to clear the default bot: %v\n", err)
os.Exit(1)
}
fmt.Println("Cleared default bot, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--set-keybase-binary" {
err := kssh.SetKeybaseBinaryPath(arg.Value)
if err != nil {
fmt.Printf("Failed to set the keybase binary: %v\n", err)
os.Exit(1)
}
fmt.Println("Set keybase binary, exiting...")
os.Exit(0)
}
if arg.Argument.Name == "--provision" {
action = Provision
}
if arg.Argument.Name == "--help" {
fmt.Println(generateHelpPage())
os.Exit(0)
}
if arg.Argument.Name == "-v" {
log.SetLevel(log.DebugLevel)
}
}
return botName, remaining, action, nil
}
// Returns whether or not the cert at the given path is a valid unexpired certificate
func isValidCert(keyPath string) bool {
_, err1 := os.Stat(keyPath)
_, err2 := os.Stat(shared.KeyPathToPubKey(keyPath))
_, err3 := os.Stat(shared.KeyPathToCert(keyPath))
if os.IsNotExist(err1) || os.IsNotExist(err2) || os.IsNotExist(err3) {
return false // Cert does not exist
}
certBytes, err := ioutil.ReadFile(shared.KeyPathToCert(keyPath))
if err != nil {
// Failed to read the file for some reason, just provision a new cert
return false
}
k, _, _, _, err := ssh.ParseAuthorizedKey(certBytes)
if err != nil {
// Failed to parse it so just provision a new cert
return false
}
// This is legal, see: https://github.com/golang/go/issues/22046
cert := k.(*ssh.Certificate)
validBefore := time.Unix(int64(cert.ValidBefore), 0)
validAfter := time.Unix(int64(cert.ValidAfter), 0)
return time.Now().After(validAfter) && time.Now().Before(validBefore)
}
// Provision a new signed SSH key :with the given config
func provisionNewKey(botName string, keyPath string) error {
log.Debug("Generating a new SSH key...")
requester, err := kssh.NewRequester()
if err != nil {
return err
}
// Make ~/.ssh/ in case it doesn't exist
err = kssh.MakeDotSSH()
if err != nil {
return err
}
// Generate the key itself and read it
err = sshutils.GenerateNewSSHKey(keyPath, true, false)
if err != nil {
return fmt.Errorf("Failed to generate a new SSH key: %v", err)
}
pubKey, err := ioutil.ReadFile(shared.KeyPathToPubKey(keyPath))
if err != nil {
return fmt.Errorf("Failed to read the SSH key from the filesystem: %v", err)
}
// Provision the key
randomUUID, err := uuid.NewRandom()
if err != nil {
return fmt.Errorf("Failed to generate a new UUID for the SignatureRequest: %v", err)
}
log.Debug("Requesting signature from the CA....")
resp, err := requester.GetSignedKey(botName, shared.SignatureRequest{
UUID: randomUUID.String(),
SSHPublicKey: string(pubKey),
})
if err != nil {
return fmt.Errorf("Failed to get a signed key from the CA: %v", err)
}
log.Debug("Received signature from the CA!")
// Write it to ~/.ssh
err = ioutil.WriteFile(shared.KeyPathToCert(keyPath), []byte(resp.SignedKey), 0600)
if err != nil {
return fmt.Errorf("Failed to write new SSH key to disk: %v", err)
}
return nil
}
// Run SSH with the given key. Calls os.Exit and does not return.
func runSSHWithKey(keyPath string, remainingArgs []string) {
// Determine whether a default SSH user has been specified and configure it if so
useConfig := false
user, err := kssh.GetDefaultSSHUser()
if err != nil {
fmt.Printf("Failed to retrieve default SSH user: %v\n", err)
os.Exit(1)
}
if user != "" {
useConfig = true
err = kssh.CreateDefaultUserConfigFile(keyPath)
if err != nil {
fmt.Printf("Failed to set default user: %v\n", err)
os.Exit(1)
}
}
// Add the key to the ssh-agent in case we are doing multiple connections (eg via the `-J` flag)
err = kssh.AddKeyToSSHAgent(keyPath)
if err != nil {
fmt.Printf("Failed to add SSH key to the SSH agent: %v\n", err)
os.Exit(1)
}
argumentList := []string{"-i", keyPath, "-o", "IdentitiesOnly=yes"}
checkAndWarnOnUnspecifiedBehavior(useConfig, remainingArgs)
if useConfig {
argumentList = append(argumentList, "-F", kssh.AlternateSSHConfigFile)
log.WithField("user", user).Debug("Using default ssh user")
}
argumentList = append(argumentList, remainingArgs...)
cmd := exec.Command("ssh", argumentList...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err != nil {
fmt.Printf("SSH exited with err: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func checkAndWarnOnUnspecifiedBehavior(useConfig bool, arguments []string) {
if useConfig {
for _, arg := range arguments {
if arg == "-F" {
log.Warn("Warning: You passed a -F flag, but kssh also uses this argument in " +
"order to implement support for a default SSH username, which you're also using. " +
"Either do not use the -F flag or run `kssh --clear-default-user` to reset the " +
"default SSH user and delegate this to the running CA bot.")
}
}
}
}