Skip to content

Commit 7593b89

Browse files
committed
Add tool for generating KMS-encrypted CA keys
Previously you had to run ssh-keygen temporarily storing the output in a file before using this utility to encrypt the key. Now you can simply have this tool generate the key and send the private directly to KMS for encryption. This should be both simpler and more secure.
1 parent d2a532c commit 7593b89

4 files changed

Lines changed: 143 additions & 19 deletions

File tree

README.rst

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ building and distributing software). The subcommands are:
223223
- request
224224
- sign
225225
- get
226+
- encrypt-key
226227

227228
As you might have guessed by now this means that a server needs to be
228229
running and serving the ssh-cert-authority service. Users that require
@@ -400,18 +401,25 @@ Launch Instance
400401
Now launch an instance and use the EC2 instance profile. A t2 class instance is
401402
likely sufficient. Copy over the latest ssh-cert-authority binary (you
402403
can also use the container) and generate a new key for the CA using
403-
ssh-keygen and then use ssh-cert-authority to encrypt it::
404+
ssh-cert-authority. The nice thing here is that the key is never written
405+
anywhere unencrypted. It is generated within ssh-cert-authority,
406+
encrypted via KMS and then written to disk in encrypted form. ::
404407

405408
environment_name=production
406-
ssh-keygen -q -t rsa -b 4096 -C "ssh-cert-authority ${environment_name}" -f ca-key-${environment_name}
407-
cat ca-key-${environment_name} | ./ssh-cert-authority-linux-amd64 encrypt-key --key-id \
408-
arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
409-
--output ca-key-${environment_name}.kms && rm ca-key-${environment_name}
410-
411-
At this point you're ready to fire up the authority. The rest of this
412-
document applies, simply add a PrivateKeyFile option to signer certd's
413-
config for the environment you're working on and reference the path to
414-
the encrypted file we just created, `ca-key-${environment_name}.kms`
409+
ssh-cert-authority encrypt-key --generate-rsa \
410+
--key-id arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
411+
--output ca-key-${environment}.kms
412+
413+
The output of this is two files: ca-key-production.kms and
414+
ca-key-production.kms.pub. The kms file should be referenced in the ssh
415+
cert authority config file, as documented elsewhere in this file, and
416+
the .pub file will be used within authorized_keys on servers you wish to
417+
SSH to.
418+
419+
--generate-rsa will generate a 4096 bit RSA key. --generate-ecdsa will
420+
generate a key from nist's p384 curve. ECDSA support is nonexistent on
421+
OS X hosts unless your users build openssh from scratch (or homebrew).
422+
This is considered painful.
415423

416424
Requesting Certificates
417425
=======================

encrypt.go

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ package main
22

33
import (
44
"bufio"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/rsa"
9+
"crypto/x509"
10+
"encoding/pem"
511
"fmt"
612
"github.com/aws/aws-sdk-go/aws"
713
"github.com/aws/aws-sdk-go/aws/ec2metadata"
814
"github.com/aws/aws-sdk-go/aws/session"
915
"github.com/aws/aws-sdk-go/service/kms"
1016
"github.com/codegangsta/cli"
17+
"golang.org/x/crypto/ssh"
1118
"io/ioutil"
1219
"os"
1320
)
@@ -24,30 +31,113 @@ func encryptFlags() []cli.Flag {
2431
Value: "ca-key.kms",
2532
Usage: "The filename for key output",
2633
},
34+
cli.BoolFlag{
35+
Name: "generate-ecdsa",
36+
Usage: "When set generate an ECDSA key from Curve P384",
37+
},
38+
cli.BoolFlag{
39+
Name: "generate-rsa",
40+
Usage: "When set generate a 4096 bit RSA key",
41+
},
42+
}
43+
}
44+
45+
func generateRsa() ([]byte, error) {
46+
key, err := rsa.GenerateKey(rand.Reader, 4096)
47+
if err != nil {
48+
return nil, err
49+
}
50+
derBlock := x509.MarshalPKCS1PrivateKey(key)
51+
pemBlock := &pem.Block{
52+
Type: "RSA PRIVATE KEY",
53+
Bytes: derBlock,
54+
}
55+
return pem.EncodeToMemory(pemBlock), nil
56+
}
57+
58+
func generateEcdsa() ([]byte, error) {
59+
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
60+
if err != nil {
61+
return nil, err
62+
}
63+
derBlock, err := x509.MarshalECPrivateKey(key)
64+
if err != nil {
65+
return nil, err
66+
}
67+
pemBlock := &pem.Block{
68+
Type: "EC PRIVATE KEY",
69+
Bytes: derBlock,
2770
}
71+
return pem.EncodeToMemory(pemBlock), nil
2872
}
2973

30-
func encryptKey(c *cli.Context) {
74+
func cmdEncryptKey(c *cli.Context) {
3175
region, err := ec2metadata.New(session.New(), aws.NewConfig()).Region()
3276
if err != nil {
3377
fmt.Printf("Unable to determine our region: %s", err)
3478
os.Exit(1)
3579
}
80+
keyId := c.String("key-id")
81+
82+
var ciphertext []byte
83+
if c.Bool("generate-ecdsa") || c.Bool("generate-rsa") {
84+
var key []byte
85+
if c.Bool("generate-ecdsa") {
86+
key, err = generateEcdsa()
87+
} else {
88+
key, err = generateRsa()
89+
}
90+
if err != nil {
91+
fmt.Printf("Unable to generate key: %s", err)
92+
os.Exit(1)
93+
}
94+
ciphertext, err = encryptKey(key, region, keyId)
95+
if err != nil {
96+
fmt.Printf("Unable to generate ecdsa key: %s", err)
97+
os.Exit(1)
98+
}
99+
signer, err := ssh.ParsePrivateKey(key)
100+
if err != nil {
101+
fmt.Printf("Unable to parse generated private key: %s", err)
102+
os.Exit(1)
103+
}
104+
err = ioutil.WriteFile(c.String("output")+".pub", ssh.MarshalAuthorizedKey(signer.PublicKey()), 0644)
105+
if err != nil {
106+
fmt.Printf("Unable to write new public key: %s", err)
107+
os.Exit(1)
108+
}
109+
} else {
110+
ciphertext, err = encryptKeyFromStdin(keyId, region)
111+
if err != nil {
112+
fmt.Printf("Failed to encrypt key: %s", err)
113+
os.Exit(1)
114+
}
115+
}
116+
err = ioutil.WriteFile(c.String("output"), ciphertext, 0644)
117+
if err != nil {
118+
fmt.Printf("Unable to write new encrypted private key: %s", err)
119+
os.Exit(1)
120+
}
121+
}
122+
123+
func encryptKeyFromStdin(keyId, region string) ([]byte, error) {
36124
keyContents, err := ioutil.ReadAll(bufio.NewReader(os.Stdin))
37125
if err != nil {
38126
fmt.Printf("Unable to read private key: %s", err)
39127
os.Exit(1)
40128
}
129+
return encryptKey(keyContents, region, keyId)
130+
}
131+
132+
func encryptKey(plaintextKey []byte, region, kmsKeyId string) ([]byte, error) {
41133
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
42134
params := &kms.EncryptInput{
43-
Plaintext: keyContents,
44-
KeyId: aws.String(c.String("key-id")),
135+
Plaintext: plaintextKey,
136+
KeyId: aws.String(kmsKeyId),
45137
}
46138
resp, err := svc.Encrypt(params)
47139
if err != nil {
48-
fmt.Printf("Unable to Encrypt CA key: %v\n", err)
49-
os.Exit(1)
140+
return nil, fmt.Errorf("Unable to Encrypt CA key: %v\n", err)
50141
}
51-
keyContents = resp.CiphertextBlob
52-
ioutil.WriteFile(c.String("output"), resp.CiphertextBlob, 0444)
142+
return []byte(resp.CiphertextBlob), nil
53143
}

encrypt_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestRsaGeneration(t *testing.T) {
9+
rsaKey, err := generateRsa()
10+
if err != nil {
11+
t.Errorf("failed to generate rsa key: %s", err)
12+
}
13+
if !strings.Contains(string(rsaKey), "RSA PRIVATE KEY") {
14+
t.Errorf("Didn't generate an RSA key?: %s", rsaKey)
15+
}
16+
}
17+
18+
func TestEcdsaGeneration(t *testing.T) {
19+
ecdsaKey, err := generateEcdsa()
20+
if err != nil {
21+
t.Errorf("failed to generate ecdsa key: %s", err)
22+
}
23+
if !strings.Contains(string(ecdsaKey), "EC PRIVATE KEY") {
24+
t.Errorf("Didn't generate an ECDSA key?: %s", ecdsaKey)
25+
}
26+
}

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ func main() {
5050
{
5151
Name: "encrypt-key",
5252
Flags: encryptFlags(),
53-
Usage: "Encrypt an ssh private key from stdin",
54-
Action: encryptKey,
53+
Usage: "Optionally generate and then encrypt an ssh private key",
54+
Action: cmdEncryptKey,
5555
},
5656
}
5757
app.Run(os.Args)

0 commit comments

Comments
 (0)