Skip to content

Commit

Permalink
Add tool for generating KMS-encrypted CA keys
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bobveznat committed Apr 6, 2016
1 parent d2a532c commit 7593b89
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 19 deletions.
28 changes: 18 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ building and distributing software). The subcommands are:
- request
- sign
- get
- encrypt-key

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

environment_name=production
ssh-keygen -q -t rsa -b 4096 -C "ssh-cert-authority ${environment_name}" -f ca-key-${environment_name}
cat ca-key-${environment_name} | ./ssh-cert-authority-linux-amd64 encrypt-key --key-id \
arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
--output ca-key-${environment_name}.kms && rm ca-key-${environment_name}

At this point you're ready to fire up the authority. The rest of this
document applies, simply add a PrivateKeyFile option to signer certd's
config for the environment you're working on and reference the path to
the encrypted file we just created, `ca-key-${environment_name}.kms`
ssh-cert-authority encrypt-key --generate-rsa \
--key-id arn:aws:kms:us-west-2:881577346222:key/d1401480-8220-4bb7-a1de-d03dfda44a13 \
--output ca-key-${environment}.kms

The output of this is two files: ca-key-production.kms and
ca-key-production.kms.pub. The kms file should be referenced in the ssh
cert authority config file, as documented elsewhere in this file, and
the .pub file will be used within authorized_keys on servers you wish to
SSH to.

--generate-rsa will generate a 4096 bit RSA key. --generate-ecdsa will
generate a key from nist's p384 curve. ECDSA support is nonexistent on
OS X hosts unless your users build openssh from scratch (or homebrew).
This is considered painful.

Requesting Certificates
=======================
Expand Down
104 changes: 97 additions & 7 deletions encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package main

import (
"bufio"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/codegangsta/cli"
"golang.org/x/crypto/ssh"
"io/ioutil"
"os"
)
Expand All @@ -24,30 +31,113 @@ func encryptFlags() []cli.Flag {
Value: "ca-key.kms",
Usage: "The filename for key output",
},
cli.BoolFlag{
Name: "generate-ecdsa",
Usage: "When set generate an ECDSA key from Curve P384",
},
cli.BoolFlag{
Name: "generate-rsa",
Usage: "When set generate a 4096 bit RSA key",
},
}
}

func generateRsa() ([]byte, error) {
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, err
}
derBlock := x509.MarshalPKCS1PrivateKey(key)
pemBlock := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: derBlock,
}
return pem.EncodeToMemory(pemBlock), nil
}

func generateEcdsa() ([]byte, error) {
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, err
}
derBlock, err := x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
pemBlock := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: derBlock,
}
return pem.EncodeToMemory(pemBlock), nil
}

func encryptKey(c *cli.Context) {
func cmdEncryptKey(c *cli.Context) {
region, err := ec2metadata.New(session.New(), aws.NewConfig()).Region()
if err != nil {
fmt.Printf("Unable to determine our region: %s", err)
os.Exit(1)
}
keyId := c.String("key-id")

var ciphertext []byte
if c.Bool("generate-ecdsa") || c.Bool("generate-rsa") {
var key []byte
if c.Bool("generate-ecdsa") {
key, err = generateEcdsa()
} else {
key, err = generateRsa()
}
if err != nil {
fmt.Printf("Unable to generate key: %s", err)
os.Exit(1)
}
ciphertext, err = encryptKey(key, region, keyId)
if err != nil {
fmt.Printf("Unable to generate ecdsa key: %s", err)
os.Exit(1)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
fmt.Printf("Unable to parse generated private key: %s", err)
os.Exit(1)
}
err = ioutil.WriteFile(c.String("output")+".pub", ssh.MarshalAuthorizedKey(signer.PublicKey()), 0644)
if err != nil {
fmt.Printf("Unable to write new public key: %s", err)
os.Exit(1)
}
} else {
ciphertext, err = encryptKeyFromStdin(keyId, region)
if err != nil {
fmt.Printf("Failed to encrypt key: %s", err)
os.Exit(1)
}
}
err = ioutil.WriteFile(c.String("output"), ciphertext, 0644)
if err != nil {
fmt.Printf("Unable to write new encrypted private key: %s", err)
os.Exit(1)
}
}

func encryptKeyFromStdin(keyId, region string) ([]byte, error) {
keyContents, err := ioutil.ReadAll(bufio.NewReader(os.Stdin))
if err != nil {
fmt.Printf("Unable to read private key: %s", err)
os.Exit(1)
}
return encryptKey(keyContents, region, keyId)
}

func encryptKey(plaintextKey []byte, region, kmsKeyId string) ([]byte, error) {
svc := kms.New(session.New(), aws.NewConfig().WithRegion(region))
params := &kms.EncryptInput{
Plaintext: keyContents,
KeyId: aws.String(c.String("key-id")),
Plaintext: plaintextKey,
KeyId: aws.String(kmsKeyId),
}
resp, err := svc.Encrypt(params)
if err != nil {
fmt.Printf("Unable to Encrypt CA key: %v\n", err)
os.Exit(1)
return nil, fmt.Errorf("Unable to Encrypt CA key: %v\n", err)
}
keyContents = resp.CiphertextBlob
ioutil.WriteFile(c.String("output"), resp.CiphertextBlob, 0444)
return []byte(resp.CiphertextBlob), nil
}
26 changes: 26 additions & 0 deletions encrypt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"strings"
"testing"
)

func TestRsaGeneration(t *testing.T) {
rsaKey, err := generateRsa()
if err != nil {
t.Errorf("failed to generate rsa key: %s", err)
}
if !strings.Contains(string(rsaKey), "RSA PRIVATE KEY") {
t.Errorf("Didn't generate an RSA key?: %s", rsaKey)
}
}

func TestEcdsaGeneration(t *testing.T) {
ecdsaKey, err := generateEcdsa()
if err != nil {
t.Errorf("failed to generate ecdsa key: %s", err)
}
if !strings.Contains(string(ecdsaKey), "EC PRIVATE KEY") {
t.Errorf("Didn't generate an ECDSA key?: %s", ecdsaKey)
}
}
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ func main() {
{
Name: "encrypt-key",
Flags: encryptFlags(),
Usage: "Encrypt an ssh private key from stdin",
Action: encryptKey,
Usage: "Optionally generate and then encrypt an ssh private key",
Action: cmdEncryptKey,
},
}
app.Run(os.Args)
Expand Down

0 comments on commit 7593b89

Please sign in to comment.