Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit fc8e95d

Browse files
committed
Add keybaseca sign command to sign keys manually when keybase is down
Also: - Adds tests for `keybaseca sign` that verify that it generates a correct signed key - Updates docs to explain `keybaseca sign` (and keep the docs on doing it manually via ssh-keygen in case anything ever breaks with `keybaseca sign`) - Refactored key signing to live in a single function that is called by `keybaseca sign` and the signature request handler. - Updates the version of urfave/cli in order to pull in a recent change that added support for making CLI flags required. - Adds `strings.TrimSpace(...)` around the output of commands when they error and we include the command's output in an error string. This removes the new line from error messages and makes them more readable.
1 parent ccdb5d9 commit fc8e95d

13 files changed

Lines changed: 168 additions & 44 deletions

File tree

docs/troubleshooting.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ and on the client run `kssh -p 2222 user@server` and inspect the debug logs.
3939
## Keybase is down
4040

4141
If Keybase is down, the bot will not work since it relies on Keybase chat for communication. In this scenario, you can
42-
manually sign SSH keys with the CA key. Place the CA private key in `~/cakey` and the CA public key in `~/cakey.pub`.
43-
Then run the command:
42+
manually sign SSH keys with the CA key. This can be done via `keybaseca sign --public-key /path/to/key.pub`. Alternatively,
43+
this can be done manually without relying on any of the tooling in this repository. To do so, place the CA private key
44+
in `~/cakey` and the CA public key in `~/cakey.pub`. Then run the command:
4445

4546
```bash
4647
ssh-keygen \
@@ -55,7 +56,7 @@ ssh-keygen \
5556
# Specify the password on the CA key (if exported via `keybaseca backup` there is no password)
5657
-N "" \
5758
# The location of the public key you wish to sign
58-
~/.ssh/id_rsa.pub
59+
/path/to/key.pub
5960
```
6061

61-
You can then use the signed SSH key to SSH via `ssh -i ~/.ssh/id_rsa user@server`.
62+
You can then use the signed SSH key to SSH via `ssh -i /path/to/key.pub user@server`.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/google/uuid v1.1.1
77
github.com/keybase/go-keybase-chat-bot v0.0.0-20190812134859-bc54fd9cf83b
88
github.com/stretchr/testify v1.3.0
9-
github.com/urfave/cli v1.20.0
9+
github.com/urfave/cli v1.21.0
1010
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
1111
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e // indirect
1212
)

go.sum

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
12
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
23
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
45
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5-
github.com/keybase/go-keybase-chat-bot v0.0.0-20190730220753-8e3930228e5a h1:gZKAzUClAbcGN8rEIUkqG9vXu5g9w+qLD1gA68qOX2Y=
6-
github.com/keybase/go-keybase-chat-bot v0.0.0-20190730220753-8e3930228e5a/go.mod h1:vNc28YFzigVJod0j5EbuTtRIe7swx8vodh2yA4jZ2s8=
76
github.com/keybase/go-keybase-chat-bot v0.0.0-20190812134859-bc54fd9cf83b h1:7Te2f9LQ/rd6XSzpntz6BaCBgglZ0uiCdv3/GdhX9VA=
87
github.com/keybase/go-keybase-chat-bot v0.0.0-20190812134859-bc54fd9cf83b/go.mod h1:vNc28YFzigVJod0j5EbuTtRIe7swx8vodh2yA4jZ2s8=
98
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
109
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1110
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1211
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
1312
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
14-
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
15-
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
13+
github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE=
14+
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
1615
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
1716
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
1817
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -23,3 +22,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
2322
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
2423
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
2524
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
25+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
26+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
27+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
28+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

src/cmd/keybaseca/keybaseca.go

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"sync"
1313
"syscall"
1414

15+
"github.com/google/uuid"
16+
1517
"github.com/keybase/bot-ssh-ca/src/keybaseca/bot"
1618
"github.com/keybase/bot-ssh-ca/src/keybaseca/config"
1719
klog "github.com/keybase/bot-ssh-ca/src/keybaseca/log"
@@ -60,6 +62,22 @@ func main() {
6062
Usage: "Start the CA service in the foreground",
6163
Action: serviceAction,
6264
},
65+
{
66+
Name: "sign",
67+
Usage: "Sign a given public key with all permissions without a dependency on Keybase",
68+
Flags: []cli.Flag{
69+
cli.StringFlag{
70+
Name: "public-key",
71+
Usage: "The path to the public key you wish to sign. Eg `~/.ssh/id_rsa.pub`",
72+
Required: true,
73+
},
74+
cli.BoolFlag{
75+
Name: "overwrite",
76+
Usage: "Overwrite the existing certificate on the filesystem",
77+
},
78+
},
79+
Action: signAction,
80+
},
6381
}
6482
app.Action = mainAction
6583
err := app.Run(os.Args)
@@ -126,6 +144,46 @@ func serviceAction(c *cli.Context) error {
126144
return nil
127145
}
128146

147+
// The action for the `keybaseca sign` subcommand
148+
func signAction(c *cli.Context) error {
149+
// Skip validation of the config since that relies on Keybase's servers
150+
conf := config.EnvConfig{}
151+
principals := strings.Join(conf.GetTeams(), ",")
152+
expiration := conf.GetKeyExpiration()
153+
randomUUID, err := uuid.NewRandom()
154+
if err != nil {
155+
return fmt.Errorf("Failed to generate unique key ID: %v", err)
156+
}
157+
158+
// Read the public key from the specified file
159+
filename := c.String("public-key")
160+
pubKey, err := ioutil.ReadFile(filename)
161+
if err != nil {
162+
return fmt.Errorf("Failed to read file at %s to get the public key: %v", filename, err)
163+
}
164+
165+
// Sign the public key
166+
signature, err := sshutils.SignKey(conf.GetCAKeyLocation(), randomUUID.String()+":keybaseca-sign", principals, expiration, string(pubKey))
167+
if err != nil {
168+
return fmt.Errorf("Failed to sign key: %v", err)
169+
}
170+
171+
// Either store it in a file or print it to stdout
172+
certPath := shared.KeyPathToCert(shared.PubKeyPathToKeyPath(filename))
173+
_, err = os.Stat(certPath)
174+
if os.IsNotExist(err) || c.Bool("overwrite") {
175+
err = ioutil.WriteFile(certPath, []byte(signature), 0600)
176+
if err != nil {
177+
return fmt.Errorf("Failed to write certificate to file: %v", err)
178+
}
179+
fmt.Printf("Provisioned new certificate in %s\n", certPath)
180+
} else {
181+
fmt.Printf("Provisioned new certificate. Place this in %s in order to use it with ssh.\n", certPath)
182+
fmt.Printf("\n```\n%s```\n", signature)
183+
}
184+
return nil
185+
}
186+
129187
// The action for the `keybaseca` command. Only used for hidden and unlisted flags.
130188
func mainAction(c *cli.Context) error {
131189
if c.Bool("wipe-all-configs") {
@@ -157,8 +215,7 @@ func mainAction(c *cli.Context) error {
157215
}(team)
158216
}
159217
semaphore.Wait()
160-
}
161-
if c.Bool("wipe-logs") {
218+
} else if c.Bool("wipe-logs") {
162219
conf, err := loadServerConfig()
163220
if err != nil {
164221
return err
@@ -176,6 +233,8 @@ func mainAction(c *cli.Context) error {
176233
}
177234
}
178235
fmt.Println("Wiped existing log file at " + logLocation)
236+
} else {
237+
cli.ShowAppHelpAndExit(c, 1)
179238
}
180239
return nil
181240
}

src/keybaseca/log/log.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func Log(conf config.Config, str string) {
1616
strWithTs := fmt.Sprintf("[%s] %s", time.Now().String(), str)
1717

1818
if conf.GetLogLocation() == "" {
19-
fmt.Print(strWithTs)
19+
fmt.Print(strWithTs + "\n")
2020
} else {
2121
err := appendToFile(conf.GetLogLocation(), strWithTs)
2222
if err != nil {

src/keybaseca/sshutils/generate_unix.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package sshutils
55
import (
66
"fmt"
77
"os/exec"
8+
"strings"
89
)
910

1011
// Generate a new SSH key. Places the private key at filename and the public key at filename.pub. If `overwrite`,
@@ -15,7 +16,7 @@ func generateNewSSHKey(filename string) error {
1516
cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-f", filename, "-m", "PEM", "-N", "")
1617
bytes, err := cmd.CombinedOutput()
1718
if err != nil {
18-
return fmt.Errorf("ssh-keygen failed: %s (%v)", string(bytes), err)
19+
return fmt.Errorf("ssh-keygen failed: %s (%v)", strings.TrimSpace(string(bytes)), err)
1920
}
2021
return nil
2122
}

src/keybaseca/sshutils/sshutils.go

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,37 +79,57 @@ func ProcessSignatureRequest(conf config.Config, sr shared.SignatureRequest) (re
7979
return
8080
}
8181

82+
// The key ID uniquely identifies the certificate by encoding the UUID of the request, a new UUID, and the username
83+
// Use both their uuid and our uuid to ensure it is unique
84+
keyID := sr.UUID + ":" + randomUUID.String() + ":" + sr.Username
85+
86+
log.Log(conf, fmt.Sprintf("Processing SignatureRequest from user=%s on device='%s' keyID:%s, principals:%s, expiration:%s, pubkey:%s",
87+
sr.Username, sr.DeviceName, keyID, principals, conf.GetKeyExpiration(), sr.SSHPublicKey))
88+
signature, err := SignKey(conf.GetCAKeyLocation(), keyID, principals, conf.GetKeyExpiration(), sr.SSHPublicKey)
89+
90+
return shared.SignatureResponse{SignedKey: signature, UUID: sr.UUID}, nil
91+
}
92+
93+
// Sign an SSH public key with the given data. Do so without any operations that rely on Keybase in order to ensure
94+
// that running `keybaseca sign` works even if Keybase is down.
95+
func SignKey(caKeyLocation, keyID, principals, expiration, publicKey string) (signature string, err error) {
96+
// Just a little bit of validation to give a nice error message
97+
if strings.Contains(publicKey, "PRIVATE KEY") {
98+
return "", fmt.Errorf("SignKey expects a public key (not a private key)")
99+
}
100+
101+
// Write the public key to a temporary file
82102
tempFilename, err := getTempFilename("keybase-ca-signed-key")
83103
if err != nil {
84104
return
85105
}
86-
err = ioutil.WriteFile(shared.KeyPathToPubKey(tempFilename), []byte(sr.SSHPublicKey), 0600)
106+
err = ioutil.WriteFile(shared.KeyPathToPubKey(tempFilename), []byte(publicKey), 0600)
87107
if err != nil {
88108
return
89109
}
90110

91-
// The key ID uniquely identifies the certificate by encoding the UUID of the request, a new UUID, and the username
92-
keyID := sr.UUID + ":" + randomUUID.String() + ":" + sr.Username
93-
94-
log.Log(conf, fmt.Sprintf("Processing SignatureRequest from user=%s on device='%s' keyID:%s, principals:%s, expiration:%s, pubkey:%s",
95-
sr.Username, sr.DeviceName, keyID, principals, conf.GetKeyExpiration(), sr.SSHPublicKey))
111+
// Note that we use ssh-keygen rather than Go's builtin SSH library since Go's SSH library does not support ed25519
112+
// SSH keys.
96113
cmd := exec.Command("ssh-keygen",
97-
"-s", conf.GetCAKeyLocation(), // The CA key
98-
"-I", keyID, // The ID of the signed key. Use their uuid and our uuid to ensure it is unique
114+
"-s", caKeyLocation, // The CA key
115+
"-I", keyID, // A unique key ID
99116
"-n", principals, // The allowed principals
100-
"-V", conf.GetKeyExpiration(), // The configured key expiration
117+
"-V", expiration, // The expiration period for the key
101118
"-N", "", // No password on the key
102-
shared.KeyPathToPubKey(tempFilename), // The location of where to put the key
119+
shared.KeyPathToPubKey(tempFilename), // The location of the public key
103120
)
104-
err = cmd.Run()
121+
bytes, err := cmd.CombinedOutput()
105122
if err != nil {
106-
return
123+
return "", fmt.Errorf("ssh-keygen error: %s (%v)", strings.TrimSpace(string(bytes)), err)
107124
}
108125

109-
data, err := ioutil.ReadFile(shared.KeyPathToCert(tempFilename))
126+
// Read the certificate from the file
127+
signatureBytes, err := ioutil.ReadFile(shared.KeyPathToCert(tempFilename))
110128
if err != nil {
111129
return
112130
}
131+
132+
// Delete the certificate and the pub key from the filesystem
113133
err = os.Remove(shared.KeyPathToPubKey(tempFilename))
114134
if err != nil {
115135
return
@@ -118,7 +138,8 @@ func ProcessSignatureRequest(conf config.Config, sr shared.SignatureRequest) (re
118138
if err != nil {
119139
return
120140
}
121-
return shared.SignatureResponse{SignedKey: string(data), UUID: sr.UUID}, nil
141+
142+
return string(signatureBytes), nil
122143
}
123144

124145
// Get the principals that should be placed in the signed certificate.

src/kssh/ssh.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package kssh
33
import (
44
"fmt"
55
"os/exec"
6+
"strings"
67
)
78

89
func AddKeyToSSHAgent(keyPath string) error {
910
cmd := exec.Command("ssh-add", keyPath)
1011
bytes, err := cmd.CombinedOutput()
1112
if err != nil {
12-
return fmt.Errorf("failed to add SSH key to the ssh-agent (is it running?): %s (%v)", string(bytes), err)
13+
return fmt.Errorf("failed to add SSH key to the ssh-agent (is it running?): %s (%v)", strings.TrimSpace(string(bytes)), err)
1314
}
1415
return nil
1516
}

src/shared/kbfs.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func KBFSFileExists(kbfsFilename string) (bool, error) {
4343
if strings.Contains(string(bytes), "ERROR file does not exist") {
4444
return false, nil
4545
}
46-
return false, fmt.Errorf("failed to stat %s: %s (%v)", kbfsFilename, string(bytes), err)
46+
return false, fmt.Errorf("failed to stat %s: %s (%v)", kbfsFilename, strings.TrimSpace(string(bytes)), err)
4747
}
4848

4949
func KBFSRead(kbfsFilename string) ([]byte, error) {
@@ -54,7 +54,7 @@ func KBFSRead(kbfsFilename string) ([]byte, error) {
5454
cmd := exec.Command("keybase", "fs", "read", kbfsFilename)
5555
bytes, err := cmd.CombinedOutput()
5656
if err != nil {
57-
return nil, fmt.Errorf("failed to read %s: %s (%v)", kbfsFilename, string(bytes), err)
57+
return nil, fmt.Errorf("failed to read %s: %s (%v)", kbfsFilename, strings.TrimSpace(string(bytes)), err)
5858
}
5959
return bytes, nil
6060
}
@@ -63,7 +63,7 @@ func KBFSDelete(filename string) error {
6363
cmd := exec.Command("keybase", "fs", "rm", filename)
6464
bytes, err := cmd.CombinedOutput()
6565
if err != nil {
66-
return fmt.Errorf("failed to delete the file at %s: %s (%v)", filename, string(bytes), err)
66+
return fmt.Errorf("failed to delete the file at %s: %s (%v)", filename, strings.TrimSpace(string(bytes)), err)
6767
}
6868
return nil
6969
}
@@ -87,7 +87,7 @@ func KBFSWrite(filename string, contents string, appendToFile bool) error {
8787
cmd.Stdin = strings.NewReader(string(contents))
8888
bytes, err := cmd.CombinedOutput()
8989
if err != nil {
90-
return fmt.Errorf("failed to write to file at %s: %s (%v)", filename, string(bytes), err)
90+
return fmt.Errorf("failed to write to file at %s: %s (%v)", filename, strings.TrimSpace(string(bytes)), err)
9191
}
9292
return nil
9393
}
@@ -96,7 +96,7 @@ func KBFSList(path string) ([]string, error) {
9696
cmd := exec.Command("keybase", "fs", "ls", "-1", "--nocolor", path)
9797
output, err := cmd.CombinedOutput()
9898
if err != nil {
99-
return nil, fmt.Errorf("failed to list files in /keybase/team/: %s (%v)", string(output), err)
99+
return nil, fmt.Errorf("failed to list files in /keybase/team/: %s (%v)", strings.TrimSpace(string(output)), err)
100100
}
101101
var ret []string
102102
for _, s := range strings.Split(string(output), "\n") {

src/shared/utils.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ func KeyPathToCert(keyPath string) string {
1414
return keyPath + "-cert.pub"
1515
}
1616

17+
func PubKeyPathToKeyPath(pubKeyPath string) string {
18+
return strings.Replace(pubKeyPath, ".pub", "", 1)
19+
}
20+
1721
// Expand out a path that starts with a tilde to be an absolute path
1822
func ExpandPathWithTilde(path string) string {
1923
usr, _ := user.Current()

0 commit comments

Comments
 (0)