Merge pull request #267 from smallstep/awskms

AWS KMS support
This commit is contained in:
Mariano Cano 2020-05-26 16:00:34 -07:00 committed by GitHub
commit 2ca63a9ff5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1628 additions and 12 deletions

View file

@ -1,6 +1,6 @@
language: go
go:
- 1.13.x
- 1.14.x
addons:
apt:
packages:

View file

@ -2,6 +2,8 @@ PKG?=github.com/smallstep/certificates/cmd/step-ca
BINNAME?=step-ca
CLOUDKMS_BINNAME?=step-cloudkms-init
CLOUDKMS_PKG?=github.com/smallstep/certificates/cmd/step-cloudkms-init
AWSKMS_BINNAME?=step-awskms-init
AWSKMS_PKG?=github.com/smallstep/certificates/cmd/step-awskms-init
YUBIKEY_BINNAME?=step-yubikey-init
YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init
@ -66,7 +68,7 @@ GOFLAGS := CGO_ENABLED=0
download:
$Q go mod download
build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME)
build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME)
@echo "Build Complete!"
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
@ -77,6 +79,10 @@ $(PREFIX)bin/$(CLOUDKMS_BINNAME): download $(call rwildcard,*.go)
$Q mkdir -p $(@D)
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG)
$(PREFIX)bin/$(AWSKMS_BINNAME): download $(call rwildcard,*.go)
$Q mkdir -p $(@D)
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(AWSKMS_BINNAME) $(LDFLAGS) $(AWSKMS_PKG)
$(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go)
$Q mkdir -p $(@D)
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_PKG)

View file

@ -0,0 +1,236 @@
package main
import (
"context"
"crypto"
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"math/big"
"os"
"time"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/awskms"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/ui"
"github.com/smallstep/cli/utils"
"golang.org/x/crypto/ssh"
)
func main() {
var credentialsFile, region string
var ssh bool
flag.StringVar(&credentialsFile, "credentials-file", "", "Path to the `file` containing the AWS KMS credentials.")
flag.StringVar(&region, "region", "", "AWS KMS region name.")
flag.BoolVar(&ssh, "ssh", false, "Create SSH keys.")
flag.Usage = usage
flag.Parse()
c, err := awskms.New(context.Background(), apiv1.Options{
Type: string(apiv1.AmazonKMS),
Region: region,
CredentialsFile: credentialsFile,
})
if err != nil {
fatal(err)
}
if err := createX509(c); err != nil {
fatal(err)
}
if ssh {
ui.Println()
if err := createSSH(c); err != nil {
fatal(err)
}
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
func usage() {
fmt.Fprintln(os.Stderr, "Usage: step-awskms-init")
fmt.Fprintln(os.Stderr, `
The step-awskms-init command initializes a public key infrastructure (PKI)
to be used by step-ca.
This tool is experimental and in the future it will be integrated in step cli.
OPTIONS`)
fmt.Fprintln(os.Stderr)
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, `
COPYRIGHT
(c) 2018-2020 Smallstep Labs, Inc.`)
os.Exit(1)
}
func createX509(c *awskms.KMS) error {
ui.Println("Creating X.509 PKI ...")
// Root Certificate
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
})
if err != nil {
return err
}
signer, err := c.CreateSigner(&resp.CreateSignerRequest)
if err != nil {
return err
}
now := time.Now()
root := &x509.Certificate{
IsCA: true,
NotBefore: now,
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
MaxPathLen: 1,
MaxPathLenZero: false,
Issuer: pkix.Name{CommonName: "Smallstep Root"},
Subject: pkix.Name{CommonName: "Smallstep Root"},
SerialNumber: mustSerialNumber(),
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
AuthorityKeyId: mustSubjectKeyID(resp.PublicKey),
}
b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer)
if err != nil {
return err
}
if err = utils.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: b,
}), 0600); err != nil {
return err
}
ui.PrintSelected("Root Key", resp.Name)
ui.PrintSelected("Root Certificate", "root_ca.crt")
root, err = pemutil.ReadCertificate("root_ca.crt")
if err != nil {
return err
}
// Intermediate Certificate
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
Name: "intermediate",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
})
if err != nil {
return err
}
intermediate := &x509.Certificate{
IsCA: true,
NotBefore: now,
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
MaxPathLen: 0,
MaxPathLenZero: true,
Issuer: root.Subject,
Subject: pkix.Name{CommonName: "Smallstep Intermediate"},
SerialNumber: mustSerialNumber(),
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
}
b, err = x509.CreateCertificate(rand.Reader, intermediate, root, resp.PublicKey, signer)
if err != nil {
return err
}
if err = utils.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: b,
}), 0600); err != nil {
return err
}
ui.PrintSelected("Intermediate Key", resp.Name)
ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt")
return nil
}
func createSSH(c *awskms.KMS) error {
ui.Println("Creating SSH Keys ...")
// User Key
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
Name: "ssh-user-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
})
if err != nil {
return err
}
key, err := ssh.NewPublicKey(resp.PublicKey)
if err != nil {
return err
}
if err = utils.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
return err
}
ui.PrintSelected("SSH User Public Key", "ssh_user_ca_key.pub")
ui.PrintSelected("SSH User Private Key", resp.Name)
// Host Key
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
Name: "ssh-host-key",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
})
if err != nil {
return err
}
key, err = ssh.NewPublicKey(resp.PublicKey)
if err != nil {
return err
}
if err = utils.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
return err
}
ui.PrintSelected("SSH Host Public Key", "ssh_host_ca_key.pub")
ui.PrintSelected("SSH Host Private Key", resp.Name)
return nil
}
func mustSerialNumber() *big.Int {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
sn, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
panic(err)
}
return sn
}
func mustSubjectKeyID(key crypto.PublicKey) []byte {
b, err := x509.MarshalPKIXPublicKey(key)
if err != nil {
panic(err)
}
hash := sha1.Sum(b)
return hash[:]
}

View file

@ -3,8 +3,10 @@
This document describes how to use a key management service or KMS to store the
private keys and sign certificates.
Support for multiple KMS are planned, but currently the only supported one is
Google's Cloud KMS.
Support for multiple KMS are planned, but currently the only Google's Cloud KMS,
and Amazon's AWS KMS are supported. A still experimental version for YubiKeys is
also available if you compile
[step-certificates](https://github.com/smallstep/certificates) yourself.
## Google's Cloud KMS
@ -66,6 +68,79 @@ Creating SSH Keys ...
See `step-cloudkms-init --help` for more options.
## AWS KMS
[AWS KMS](https://docs.aws.amazon.com/kms/index.html) is the Amazon's managed
encryption and key management service. It creates and store the cryptographic
keys, and use their infrastructure for signing operations. Amazon KMS operations
are always backed by hardware security modules (HSMs).
To configure AWS KMS in your CA you need add the `"kms"` property to you
`ca.json`, and replace the property`"key"` with the AWS KMS key name of your
intermediate key:
```json
{
...
"key": "awskms:key-id=f879f239-feb6-4596-9ed2-b1606277c7fe",
...
"kms": {
"type": "awskms",
"region": "us-east-1"
}
}
```
By default it uses the credentials in `~/.aws/credentials`, but this can be
overridden using the `credentialsFile` option, `region` and `profile` can also
be configured as options. These can also be configured using environment
variables as described by their [session
docs](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/).
To configure SSH certificate signing we do something similar, and replace the
ssh keys with the ones in the KMS:
```json
{
...
"ssh": {
"hostKey": "awskms:key-id=d48e502a-09bc-4bf7-9af8-ae1bccedc931",
"userKey": "awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12"
},
}
```
The keys can also be just the Amazon's Key ID or the ARN, but using the format
based on the [RFC7512](https://tools.ietf.org/html/rfc7512) will allow more
flexibility for future releases of `step`.
Currently [step](https://github.com/smallstep/cli) does not provide an automatic
way to initialize the public key infrastructure (PKI) using AWS KMS, but an
experimental tool named `step-awskms-init` is available for this use case. At
some point this tool will be integrated into `step` and it will be deleted.
To use `step-awskms-init` make sure to have to have your [environment
configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)
running `aws configure` and then just run:
```sh
$ bin/step-awskms-init --ssh --region us-east-1
Creating PKI ...
✔ Root Key: awskms:key-id=f53fb767-4029-40ff-b650-0dd35fb661df
✔ Root Certificate: root_ca.crt
✔ Intermediate Key: awskms:key-id=f879f239-feb6-4596-9ed2-b1606277c7fe
✔ Intermediate Certificate: intermediate_ca.crt
Creating SSH Keys ...
✔ SSH User Public Key: ssh_user_ca_key.pub
✔ SSH User Private Key: awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12
✔ SSH Host Public Key: ssh_host_ca_key.pub
✔ SSH Host Private Key: awskms:key-id=cf28e942-1e10-4a08-b84c-5359af1b5f12
```
The `--region` parameter is only required if your aws configuration does not
define a region. See `step-awskms-init --help` for more options.
## YubiKey
And incomplete and experimental support for [YubiKeys](https://www.yubico.com)

5
go.mod
View file

@ -5,13 +5,14 @@ go 1.13
require (
cloud.google.com/go v0.51.0
github.com/Masterminds/sprig/v3 v3.0.0
github.com/aws/aws-sdk-go v1.30.29
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-piv/piv-go v1.5.0
github.com/googleapis/gax-go/v2 v2.0.5
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/newrelic/go-agent v2.15.0+incompatible
github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
github.com/sirupsen/logrus v1.4.2
github.com/smallstep/assert v0.0.0-20200103212524-b99dc1097b15
@ -19,7 +20,7 @@ require (
github.com/smallstep/nosql v0.3.0
github.com/urfave/cli v1.22.2
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
google.golang.org/api v0.15.0
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb
google.golang.org/grpc v1.26.0

14
go.sum
View file

@ -43,7 +43,10 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.19.18 h1:Hb3+b9HCqrOrbAtFstUWg7H5TQ+/EcklJtE8VShVs8o=
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0=
github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -128,6 +131,8 @@ github.com/go-piv/piv-go v1.5.0 h1:UtHPfrJsZKY+Z3UIjmJLh6DY+KtmNOl/9b/zt4N81pM=
github.com/go-piv/piv-go v1.5.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8HKy7Gk=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
@ -268,7 +273,10 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@ -386,6 +394,8 @@ github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
@ -500,6 +510,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
@ -618,6 +630,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

View file

@ -57,7 +57,7 @@ type Options struct {
// The type of the KMS to use.
Type string `json:"type"`
// Path to the credentials file used in CloudKMS.
// Path to the credentials file used in CloudKMS and AmazonKMS.
CredentialsFile string `json:"credentialsFile"`
// Path to the module used with PKCS11 KMS.
@ -65,6 +65,12 @@ type Options struct {
// Pin used to access the PKCS11 module.
Pin string `json:"pin"`
// Region to use in AmazonKMS.
Region string `json:"region"`
// Profile to use in AmazonKMS.
Profile string `json:"profile"`
}
// Validate checks the fields in Options.
@ -74,10 +80,8 @@ func (o *Options) Validate() error {
}
switch Type(strings.ToLower(o.Type)) {
case DefaultKMS, SoftKMS, CloudKMS:
case DefaultKMS, SoftKMS, CloudKMS, AmazonKMS:
case YubiKey:
case AmazonKMS:
return ErrNotImplemented{"support for AmazonKMS is not yet implemented"}
case PKCS11:
return ErrNotImplemented{"support for PKCS11 is not yet implemented"}
default:

View file

@ -13,7 +13,7 @@ func TestOptions_Validate(t *testing.T) {
{"nil", nil, false},
{"softkms", &Options{Type: "softkms"}, false},
{"cloudkms", &Options{Type: "cloudkms"}, false},
{"awskms", &Options{Type: "awskms"}, true},
{"awskms", &Options{Type: "awskms"}, false},
{"pkcs11", &Options{Type: "pkcs11"}, true},
{"unsupported", &Options{Type: "unsupported"}, true},
}

247
kms/awskms/awskms.go Normal file
View file

@ -0,0 +1,247 @@
package awskms
import (
"context"
"crypto"
"net/url"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/pkg/errors"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/uri"
"github.com/smallstep/cli/crypto/pemutil"
)
// KMS implements a KMS using AWS Key Management Service.
type KMS struct {
session *session.Session
service KeyManagementClient
}
// KeyManagementClient defines the methods on KeyManagementClient that this
// package will use. This interface will be used for unit testing.
type KeyManagementClient interface {
GetPublicKeyWithContext(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error)
CreateKeyWithContext(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error)
CreateAliasWithContext(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error)
SignWithContext(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error)
}
// customerMasterKeySpecMapping is a mapping between the step signature algorithm,
// and bits for RSA keys, with awskms CustomerMasterKeySpec.
var customerMasterKeySpecMapping = map[apiv1.SignatureAlgorithm]interface{}{
apiv1.UnspecifiedSignAlgorithm: kms.CustomerMasterKeySpecEccNistP256,
apiv1.SHA256WithRSA: map[int]string{
0: kms.CustomerMasterKeySpecRsa3072,
2048: kms.CustomerMasterKeySpecRsa2048,
3072: kms.CustomerMasterKeySpecRsa3072,
4096: kms.CustomerMasterKeySpecRsa4096,
},
apiv1.SHA512WithRSA: map[int]string{
0: kms.CustomerMasterKeySpecRsa4096,
4096: kms.CustomerMasterKeySpecRsa4096,
},
apiv1.SHA256WithRSAPSS: map[int]string{
0: kms.CustomerMasterKeySpecRsa3072,
2048: kms.CustomerMasterKeySpecRsa2048,
3072: kms.CustomerMasterKeySpecRsa3072,
4096: kms.CustomerMasterKeySpecRsa4096,
},
apiv1.SHA512WithRSAPSS: map[int]string{
0: kms.CustomerMasterKeySpecRsa4096,
4096: kms.CustomerMasterKeySpecRsa4096,
},
apiv1.ECDSAWithSHA256: kms.CustomerMasterKeySpecEccNistP256,
apiv1.ECDSAWithSHA384: kms.CustomerMasterKeySpecEccNistP384,
apiv1.ECDSAWithSHA512: kms.CustomerMasterKeySpecEccNistP521,
}
// New creates a new AWSKMS. By default, sessions will be created using the
// credentials in `~/.aws/credentials`, but this can be overridden using the
// CredentialsFile option, the Region and Profile can also be configured as
// options.
//
// AWS sessions can also be configured with environment variables, see docs at
// https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for all the options.
func New(ctx context.Context, opts apiv1.Options) (*KMS, error) {
o := session.Options{}
if opts.Region != "" {
o.Config.Region = &opts.Region
}
if opts.Profile != "" {
o.Profile = opts.Profile
}
if opts.CredentialsFile != "" {
o.SharedConfigFiles = []string{opts.CredentialsFile}
}
sess, err := session.NewSessionWithOptions(o)
if err != nil {
return nil, errors.Wrap(err, "error creating AWS session")
}
return &KMS{
session: sess,
service: kms.New(sess),
}, nil
}
func init() {
apiv1.Register(apiv1.AmazonKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
return New(ctx, opts)
})
}
// GetPublicKey returns a public key from KMS.
func (k *KMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
if req.Name == "" {
return nil, errors.New("getPublicKey 'name' cannot be empty")
}
keyID, err := parseKeyID(req.Name)
if err != nil {
return nil, err
}
ctx, cancel := defaultContext()
defer cancel()
resp, err := k.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{
KeyId: &keyID,
})
if err != nil {
return nil, errors.Wrap(err, "awskms GetPublicKeyWithContext failed")
}
return pemutil.ParseDER(resp.PublicKey)
}
// CreateKey generates a new key in KMS and returns the public key version
// of it.
func (k *KMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
if req.Name == "" {
return nil, errors.New("createKeyRequest 'name' cannot be empty")
}
keySpec, err := getCustomerMasterKeySpecMapping(req.SignatureAlgorithm, req.Bits)
if err != nil {
return nil, err
}
tag := new(kms.Tag)
tag.SetTagKey("name")
tag.SetTagValue(req.Name)
input := &kms.CreateKeyInput{
Description: &req.Name,
CustomerMasterKeySpec: &keySpec,
Tags: []*kms.Tag{tag},
}
input.SetKeyUsage(kms.KeyUsageTypeSignVerify)
ctx, cancel := defaultContext()
defer cancel()
resp, err := k.service.CreateKeyWithContext(ctx, input)
if err != nil {
return nil, errors.Wrap(err, "awskms CreateKeyWithContext failed")
}
if err := k.createKeyAlias(*resp.KeyMetadata.KeyId, req.Name); err != nil {
return nil, err
}
// Create uri for key
name := uri.New("awskms", url.Values{
"key-id": []string{*resp.KeyMetadata.KeyId},
}).String()
publicKey, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{
Name: name,
})
if err != nil {
return nil, err
}
// Names uses Amazon Resource Name
// https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
return &apiv1.CreateKeyResponse{
Name: name,
PublicKey: publicKey,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: name,
},
}, nil
}
func (k *KMS) createKeyAlias(keyID, alias string) error {
alias = "alias/" + alias + "-" + keyID[:8]
ctx, cancel := defaultContext()
defer cancel()
_, err := k.service.CreateAliasWithContext(ctx, &kms.CreateAliasInput{
AliasName: &alias,
TargetKeyId: &keyID,
})
if err != nil {
return errors.Wrap(err, "awskms CreateAliasWithContext failed")
}
return nil
}
// CreateSigner creates a new crypto.Signer with a previously configured key.
func (k *KMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
if req.SigningKey == "" {
return nil, errors.New("createSigner 'signingKey' cannot be empty")
}
return NewSigner(k.service, req.SigningKey)
}
// Close closes the connection of the KMS client.
func (k *KMS) Close() error {
return nil
}
func defaultContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 15*time.Second)
}
// parseKeyID extracts the key-id from an uri.
func parseKeyID(name string) (string, error) {
name = strings.ToLower(name)
if strings.HasPrefix(name, "awskms:") || strings.HasPrefix(name, "aws:") {
u, err := uri.Parse(name)
if err != nil {
return "", err
}
if k := u.Get("key-id"); k != "" {
return k, nil
}
return "", errors.Errorf("failed to get key-id from %s", name)
}
return name, nil
}
func getCustomerMasterKeySpecMapping(alg apiv1.SignatureAlgorithm, bits int) (string, error) {
v, ok := customerMasterKeySpecMapping[alg]
if !ok {
return "", errors.Errorf("awskms does not support signature algorithm '%s'", alg)
}
switch v := v.(type) {
case string:
return v, nil
case map[int]string:
s, ok := v[bits]
if !ok {
return "", errors.Errorf("awskms does not support signature algorithm '%s' with '%d' bits", alg, bits)
}
return s, nil
default:
return "", errors.Errorf("unexpected error: this should not happen")
}
}

358
kms/awskms/awskms_test.go Normal file
View file

@ -0,0 +1,358 @@
package awskms
import (
"context"
"crypto"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/cli/crypto/pemutil"
)
func TestNew(t *testing.T) {
ctx := context.Background()
sess, err := session.NewSessionWithOptions(session.Options{})
if err != nil {
t.Fatal(err)
}
expected := &KMS{
session: sess,
service: kms.New(sess),
}
// This will force an error in the session creation.
// It does not fail with missing credentials.
forceError := func(t *testing.T) {
key := "AWS_CA_BUNDLE"
value := os.Getenv(key)
os.Setenv(key, filepath.Join(os.TempDir(), "missing-ca.crt"))
t.Cleanup(func() {
if value == "" {
os.Unsetenv(key)
} else {
os.Setenv(key, value)
}
})
}
type args struct {
ctx context.Context
opts apiv1.Options
}
tests := []struct {
name string
args args
want *KMS
wantErr bool
}{
{"ok", args{ctx, apiv1.Options{}}, expected, false},
{"ok with options", args{ctx, apiv1.Options{
Region: "us-east-1",
Profile: "smallstep",
CredentialsFile: "~/aws/credentials",
}}, expected, false},
{"fail", args{ctx, apiv1.Options{}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Force an error in the session loading
if tt.wantErr {
forceError(t)
}
got, err := New(tt.args.ctx, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %#v, want %#v", got, tt.want)
}
} else {
if got.session == nil || got.service == nil {
t.Errorf("New() = %#v, want %#v", got, tt.want)
}
}
})
}
}
func TestKMS_GetPublicKey(t *testing.T) {
okClient := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type fields struct {
session *session.Session
service KeyManagementClient
}
type args struct {
req *apiv1.GetPublicKeyRequest
}
tests := []struct {
name string
fields fields
args args
want crypto.PublicKey
wantErr bool
}{
{"ok", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{
Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
}}, key, false},
{"fail empty", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{}}, nil, true},
{"fail name", fields{nil, okClient}, args{&apiv1.GetPublicKeyRequest{
Name: "awskms:key-id=",
}}, nil, true},
{"fail getPublicKey", fields{nil, &MockClient{
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return nil, fmt.Errorf("an error")
},
}}, args{&apiv1.GetPublicKeyRequest{
Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
}}, nil, true},
{"fail not der", fields{nil, &MockClient{
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return &kms.GetPublicKeyOutput{
KeyId: input.KeyId,
PublicKey: []byte(publicKey),
}, nil
},
}}, args{&apiv1.GetPublicKeyRequest{
Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KMS{
session: tt.fields.session,
service: tt.fields.service,
}
got, err := k.GetPublicKey(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KMS.GetPublicKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestKMS_CreateKey(t *testing.T) {
okClient := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type fields struct {
session *session.Session
service KeyManagementClient
}
type args struct {
req *apiv1.CreateKeyRequest
}
tests := []struct {
name string
fields fields
args args
want *apiv1.CreateKeyResponse
wantErr bool
}{
{"ok", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, &apiv1.CreateKeyResponse{
Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
PublicKey: key,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
},
}, false},
{"ok rsa", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.SHA256WithRSA,
Bits: 2048,
}}, &apiv1.CreateKeyResponse{
Name: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
PublicKey: key,
CreateSignerRequest: apiv1.CreateSignerRequest{
SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
},
}, false},
{"fail empty", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{}}, nil, true},
{"fail unsupported alg", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.PureEd25519,
}}, nil, true},
{"fail unsupported bits", fields{nil, okClient}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.SHA256WithRSA,
Bits: 1234,
}}, nil, true},
{"fail createKey", fields{nil, &MockClient{
createKeyWithContext: func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) {
return nil, fmt.Errorf("an error")
},
createAliasWithContext: okClient.createAliasWithContext,
getPublicKeyWithContext: okClient.getPublicKeyWithContext,
}}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, nil, true},
{"fail createAlias", fields{nil, &MockClient{
createKeyWithContext: okClient.createKeyWithContext,
createAliasWithContext: func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) {
return nil, fmt.Errorf("an error")
},
getPublicKeyWithContext: okClient.getPublicKeyWithContext,
}}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, nil, true},
{"fail getPublicKey", fields{nil, &MockClient{
createKeyWithContext: okClient.createKeyWithContext,
createAliasWithContext: okClient.createAliasWithContext,
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return nil, fmt.Errorf("an error")
},
}}, args{&apiv1.CreateKeyRequest{
Name: "root",
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KMS{
session: tt.fields.session,
service: tt.fields.service,
}
got, err := k.CreateKey(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KMS.CreateKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestKMS_CreateSigner(t *testing.T) {
client := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type fields struct {
session *session.Session
service KeyManagementClient
}
type args struct {
req *apiv1.CreateSignerRequest
}
tests := []struct {
name string
fields fields
args args
want crypto.Signer
wantErr bool
}{
{"ok", fields{nil, client}, args{&apiv1.CreateSignerRequest{
SigningKey: "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936",
}}, &Signer{
service: client,
keyID: "be468355-ca7a-40d9-a28b-8ae1c4c7f936",
publicKey: key,
}, false},
{"fail empty", fields{nil, client}, args{&apiv1.CreateSignerRequest{}}, nil, true},
{"fail preload", fields{nil, client}, args{&apiv1.CreateSignerRequest{}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KMS{
session: tt.fields.session,
service: tt.fields.service,
}
got, err := k.CreateSigner(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf("KMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KMS.CreateSigner() = %v, want %v", got, tt.want)
}
})
}
}
func TestKMS_Close(t *testing.T) {
type fields struct {
session *session.Session
service KeyManagementClient
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{"ok", fields{nil, getOKClient()}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KMS{
session: tt.fields.session,
service: tt.fields.service,
}
if err := k.Close(); (err != nil) != tt.wantErr {
t.Errorf("KMS.Close() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_parseKeyID(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"ok uri", args{"awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", false},
{"ok key id", args{"be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", false},
{"ok arn", args{"arn:aws:kms:us-east-1:123456789:key/be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, "arn:aws:kms:us-east-1:123456789:key/be468355-ca7a-40d9-a28b-8ae1c4c7f936", false},
{"fail parse", args{"awskms:key-id=%ZZ"}, "", true},
{"fail empty key", args{"awskms:key-id="}, "", true},
{"fail missing", args{"awskms:foo=bar"}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseKeyID(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("parseKeyID() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseKeyID() = %v, want %v", got, tt.want)
}
})
}
}

72
kms/awskms/mock_test.go Normal file
View file

@ -0,0 +1,72 @@
package awskms
import (
"encoding/pem"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/kms"
)
type MockClient struct {
getPublicKeyWithContext func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error)
createKeyWithContext func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error)
createAliasWithContext func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error)
signWithContext func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error)
}
func (m *MockClient) GetPublicKeyWithContext(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return m.getPublicKeyWithContext(ctx, input, opts...)
}
func (m *MockClient) CreateKeyWithContext(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) {
return m.createKeyWithContext(ctx, input, opts...)
}
func (m *MockClient) CreateAliasWithContext(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) {
return m.createAliasWithContext(ctx, input, opts...)
}
func (m *MockClient) SignWithContext(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) {
return m.signWithContext(ctx, input, opts...)
}
const (
publicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8XWlIWkOThxNjGbZLYUgRHmsvCrW
KF+HLktPfPTIK3lGd1k4849WQs59XIN+LXZQ6b2eRBEBKAHEyQus8UU7gw==
-----END PUBLIC KEY-----`
keyID = "be468355-ca7a-40d9-a28b-8ae1c4c7f936"
)
var signature = []byte{
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
}
func getOKClient() *MockClient {
return &MockClient{
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
block, _ := pem.Decode([]byte(publicKey))
return &kms.GetPublicKeyOutput{
KeyId: input.KeyId,
PublicKey: block.Bytes,
}, nil
},
createKeyWithContext: func(ctx aws.Context, input *kms.CreateKeyInput, opts ...request.Option) (*kms.CreateKeyOutput, error) {
md := new(kms.KeyMetadata)
md.SetKeyId(keyID)
return &kms.CreateKeyOutput{
KeyMetadata: md,
}, nil
},
createAliasWithContext: func(ctx aws.Context, input *kms.CreateAliasInput, opts ...request.Option) (*kms.CreateAliasOutput, error) {
return &kms.CreateAliasOutput{}, nil
},
signWithContext: func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) {
return &kms.SignOutput{
Signature: signature,
}, nil
},
}
}

122
kms/awskms/signer.go Normal file
View file

@ -0,0 +1,122 @@
package awskms
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"io"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/pemutil"
)
// Signer implements a crypto.Signer using the AWS KMS.
type Signer struct {
service KeyManagementClient
keyID string
publicKey crypto.PublicKey
}
// NewSigner creates a new signer using a key in the AWS KMS.
func NewSigner(svc KeyManagementClient, signingKey string) (*Signer, error) {
keyID, err := parseKeyID(signingKey)
if err != nil {
return nil, err
}
// Make sure that the key exists.
signer := &Signer{
service: svc,
keyID: keyID,
}
if err := signer.preloadKey(keyID); err != nil {
return nil, err
}
return signer, nil
}
func (s *Signer) preloadKey(keyID string) error {
ctx, cancel := defaultContext()
defer cancel()
resp, err := s.service.GetPublicKeyWithContext(ctx, &kms.GetPublicKeyInput{
KeyId: &keyID,
})
if err != nil {
return errors.Wrap(err, "awskms GetPublicKeyWithContext failed")
}
s.publicKey, err = pemutil.ParseDER(resp.PublicKey)
return err
}
// Public returns the public key of this signer or an error.
func (s *Signer) Public() crypto.PublicKey {
return s.publicKey
}
// Sign signs digest with the private key stored in the AWS KMS.
func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
alg, err := getSigningAlgorithm(s.Public(), opts)
if err != nil {
return nil, err
}
req := &kms.SignInput{
KeyId: &s.keyID,
SigningAlgorithm: &alg,
Message: digest,
}
req.SetMessageType("DIGEST")
ctx, cancel := defaultContext()
defer cancel()
resp, err := s.service.SignWithContext(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "awsKMS SignWithContext failed")
}
return resp.Signature, nil
}
func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (string, error) {
switch key.(type) {
case *rsa.PublicKey:
_, isPSS := opts.(*rsa.PSSOptions)
switch h := opts.HashFunc(); h {
case crypto.SHA256:
if isPSS {
return kms.SigningAlgorithmSpecRsassaPssSha256, nil
}
return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha256, nil
case crypto.SHA384:
if isPSS {
return kms.SigningAlgorithmSpecRsassaPssSha384, nil
}
return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha384, nil
case crypto.SHA512:
if isPSS {
return kms.SigningAlgorithmSpecRsassaPssSha512, nil
}
return kms.SigningAlgorithmSpecRsassaPkcs1V15Sha512, nil
default:
return "", errors.Errorf("unsupported hash function %v", h)
}
case *ecdsa.PublicKey:
switch h := opts.HashFunc(); h {
case crypto.SHA256:
return kms.SigningAlgorithmSpecEcdsaSha256, nil
case crypto.SHA384:
return kms.SigningAlgorithmSpecEcdsaSha384, nil
case crypto.SHA512:
return kms.SigningAlgorithmSpecEcdsaSha512, nil
default:
return "", errors.Errorf("unsupported hash function %v", h)
}
default:
return "", errors.Errorf("unsupported key type %T", key)
}
}

191
kms/awskms/signer_test.go Normal file
View file

@ -0,0 +1,191 @@
package awskms
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"fmt"
"io"
"reflect"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/smallstep/cli/crypto/pemutil"
)
func TestNewSigner(t *testing.T) {
okClient := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type args struct {
svc KeyManagementClient
signingKey string
}
tests := []struct {
name string
args args
want *Signer
wantErr bool
}{
{"ok", args{okClient, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, &Signer{
service: okClient,
keyID: "be468355-ca7a-40d9-a28b-8ae1c4c7f936",
publicKey: key,
}, false},
{"fail parse", args{okClient, "awskms:key-id="}, nil, true},
{"fail preload", args{&MockClient{
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return nil, fmt.Errorf("an error")
},
}, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, nil, true},
{"fail preload not der", args{&MockClient{
getPublicKeyWithContext: func(ctx aws.Context, input *kms.GetPublicKeyInput, opts ...request.Option) (*kms.GetPublicKeyOutput, error) {
return &kms.GetPublicKeyOutput{
KeyId: input.KeyId,
PublicKey: []byte(publicKey),
}, nil
},
}, "awskms:key-id=be468355-ca7a-40d9-a28b-8ae1c4c7f936"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewSigner(tt.args.svc, tt.args.signingKey)
if (err != nil) != tt.wantErr {
t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewSigner() = %v, want %v", got, tt.want)
}
})
}
}
func TestSigner_Public(t *testing.T) {
okClient := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type fields struct {
service KeyManagementClient
keyID string
publicKey crypto.PublicKey
}
tests := []struct {
name string
fields fields
want crypto.PublicKey
}{
{"ok", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, key},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Signer{
service: tt.fields.service,
keyID: tt.fields.keyID,
publicKey: tt.fields.publicKey,
}
if got := s.Public(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Signer.Public() = %v, want %v", got, tt.want)
}
})
}
}
func TestSigner_Sign(t *testing.T) {
okClient := getOKClient()
key, err := pemutil.ParseKey([]byte(publicKey))
if err != nil {
t.Fatal(err)
}
type fields struct {
service KeyManagementClient
keyID string
publicKey crypto.PublicKey
}
type args struct {
rand io.Reader
digest []byte
opts crypto.SignerOpts
}
tests := []struct {
name string
fields fields
args args
want []byte
wantErr bool
}{
{"ok", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.SHA256}, signature, false},
{"fail alg", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.MD5}, nil, true},
{"fail key", fields{okClient, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", []byte("key")}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true},
{"fail sign", fields{&MockClient{
signWithContext: func(ctx aws.Context, input *kms.SignInput, opts ...request.Option) (*kms.SignOutput, error) {
return nil, fmt.Errorf("an error")
},
}, "be468355-ca7a-40d9-a28b-8ae1c4c7f936", key}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Signer{
service: tt.fields.service,
keyID: tt.fields.keyID,
publicKey: tt.fields.publicKey,
}
got, err := s.Sign(tt.args.rand, tt.args.digest, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("Signer.Sign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Signer.Sign() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getSigningAlgorithm(t *testing.T) {
type args struct {
key crypto.PublicKey
opts crypto.SignerOpts
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{"rsa+sha256", args{&rsa.PublicKey{}, crypto.SHA256}, "RSASSA_PKCS1_V1_5_SHA_256", false},
{"rsa+sha384", args{&rsa.PublicKey{}, crypto.SHA384}, "RSASSA_PKCS1_V1_5_SHA_384", false},
{"rsa+sha512", args{&rsa.PublicKey{}, crypto.SHA512}, "RSASSA_PKCS1_V1_5_SHA_512", false},
{"pssrsa+sha256", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA256.HashFunc()}}, "RSASSA_PSS_SHA_256", false},
{"pssrsa+sha384", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA384.HashFunc()}}, "RSASSA_PSS_SHA_384", false},
{"pssrsa+sha512", args{&rsa.PublicKey{}, &rsa.PSSOptions{Hash: crypto.SHA512.HashFunc()}}, "RSASSA_PSS_SHA_512", false},
{"P256", args{&ecdsa.PublicKey{}, crypto.SHA256}, "ECDSA_SHA_256", false},
{"P384", args{&ecdsa.PublicKey{}, crypto.SHA384}, "ECDSA_SHA_384", false},
{"P521", args{&ecdsa.PublicKey{}, crypto.SHA512}, "ECDSA_SHA_512", false},
{"fail type", args{[]byte("key"), crypto.SHA256}, "", true},
{"fail rsa alg", args{&rsa.PublicKey{}, crypto.MD5}, "", true},
{"fail ecdsa alg", args{&ecdsa.PublicKey{}, crypto.MD5}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getSigningAlgorithm(tt.args.key, tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("getSigningAlgorithm() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getSigningAlgorithm() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -8,6 +8,7 @@ import (
"github.com/smallstep/certificates/kms/apiv1"
// Enabled kms interfaces.
_ "github.com/smallstep/certificates/kms/awskms"
_ "github.com/smallstep/certificates/kms/cloudkms"
_ "github.com/smallstep/certificates/kms/softkms"

View file

@ -7,6 +7,7 @@ import (
"testing"
"github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/awskms"
"github.com/smallstep/certificates/kms/cloudkms"
"github.com/smallstep/certificates/kms/softkms"
)
@ -27,8 +28,8 @@ func TestNew(t *testing.T) {
}{
{"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false},
{"default", false, args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, false},
{"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, &awskms.KMS{}, false},
{"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, true}, // fails because not credentials
{"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, nil, true}, // not yet supported
{"pkcs11", false, args{ctx, apiv1.Options{Type: "pkcs11"}}, nil, true}, // not yet supported
{"fail validation", false, args{ctx, apiv1.Options{Type: "foobar"}}, nil, true},
}

86
kms/uri/uri.go Normal file
View file

@ -0,0 +1,86 @@
package uri
import (
"net/url"
"strings"
"github.com/pkg/errors"
)
// URI implements a parser for a URI format based on the the PKCS #11 URI Scheme
// defined in https://tools.ietf.org/html/rfc7512
//
// These URIs will be used to define the key names in a KMS.
type URI struct {
*url.URL
Values url.Values
}
// New creates a new URI from a scheme and key-value pairs.
func New(scheme string, values url.Values) *URI {
return &URI{
URL: &url.URL{
Scheme: scheme,
Opaque: strings.ReplaceAll(values.Encode(), "&", ";"),
},
Values: values,
}
}
// NewFile creates an uri for a file.
func NewFile(path string) *URI {
return &URI{
URL: &url.URL{
Scheme: "file",
Path: path,
},
}
}
// HasScheme returns true if the given uri has the given scheme, false otherwise.
func HasScheme(scheme, rawuri string) bool {
u, err := url.Parse(rawuri)
if err != nil {
return false
}
return strings.EqualFold(u.Scheme, scheme)
}
// Parse returns the URI for the given string or an error.
func Parse(rawuri string) (*URI, error) {
u, err := url.Parse(rawuri)
if err != nil {
return nil, errors.Wrapf(err, "error parsing %s", rawuri)
}
if u.Scheme == "" {
return nil, errors.Errorf("error parsing %s: scheme is missing", rawuri)
}
v, err := url.ParseQuery(u.Opaque)
if err != nil {
return nil, errors.Wrapf(err, "error parsing %s", rawuri)
}
return &URI{
URL: u,
Values: v,
}, nil
}
// ParseWithScheme returns the URI for the given string only if it has the given
// scheme.
func ParseWithScheme(scheme, rawuri string) (*URI, error) {
u, err := Parse(rawuri)
if err != nil {
return nil, err
}
if !strings.EqualFold(u.Scheme, scheme) {
return nil, errors.Errorf("error parsing %s: scheme not expected", rawuri)
}
return u, nil
}
// Get returns the first value in the uri with the give n key, it will return
// empty string if that field is not present.
func (u *URI) Get(key string) string {
return u.Values.Get(key)
}

202
kms/uri/uri_test.go Normal file
View file

@ -0,0 +1,202 @@
package uri
import (
"net/url"
"reflect"
"testing"
)
func TestNew(t *testing.T) {
type args struct {
scheme string
values url.Values
}
tests := []struct {
name string
args args
want *URI
}{
{"ok", args{"yubikey", url.Values{"slot-id": []string{"9a"}}}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"},
Values: url.Values{"slot-id": []string{"9a"}},
}},
{"ok multiple", args{"yubikey", url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "foo=bar;slot-id=9a"},
Values: url.Values{
"slot-id": []string{"9a"},
"foo": []string{"bar"},
},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.scheme, tt.args.values); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewFile(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want *URI
}{
{"ok", args{"/tmp/ca.crt"}, &URI{
URL: &url.URL{Scheme: "file", Path: "/tmp/ca.crt"},
Values: url.Values(nil),
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewFile(tt.args.path); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewFile() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasScheme(t *testing.T) {
type args struct {
scheme string
rawuri string
}
tests := []struct {
name string
args args
want bool
}{
{"ok", args{"yubikey", "yubikey:slot-id=9a"}, true},
{"ok empty", args{"yubikey", "yubikey:"}, true},
{"ok letter case", args{"awsKMS", "AWSkms:key-id=abcdefg?foo=bar"}, true},
{"fail", args{"yubikey", "awskms:key-id=abcdefg"}, false},
{"fail parse", args{"yubikey", "yubi%key:slot-id=9a"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasScheme(tt.args.scheme, tt.args.rawuri); got != tt.want {
t.Errorf("HasScheme() = %v, want %v", got, tt.want)
}
})
}
}
func TestParse(t *testing.T) {
type args struct {
rawuri string
}
tests := []struct {
name string
args args
want *URI
wantErr bool
}{
{"ok", args{"yubikey:slot-id=9a"}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"},
Values: url.Values{"slot-id": []string{"9a"}},
}, false},
{"ok query", args{"yubikey:slot-id=9a;foo=bar?pin=123456&foo=bar"}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a;foo=bar", RawQuery: "pin=123456&foo=bar"},
Values: url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}},
}, false},
{"ok file", args{"file:///tmp/ca.cert"}, &URI{
URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"},
Values: url.Values{},
}, false},
{"ok file simple", args{"file:/tmp/ca.cert"}, &URI{
URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"},
Values: url.Values{},
}, false},
{"ok file host", args{"file://tmp/ca.cert"}, &URI{
URL: &url.URL{Scheme: "file", Host: "tmp", Path: "/ca.cert"},
Values: url.Values{},
}, false},
{"fail parse", args{"yubi%key:slot-id=9a"}, nil, true},
{"fail scheme", args{"yubikey"}, nil, true},
{"fail parse opaque", args{"yubikey:slot-id=%ZZ"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.args.rawuri)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %#v, want %v", got.URL, tt.want)
}
})
}
}
func TestParseWithScheme(t *testing.T) {
type args struct {
scheme string
rawuri string
}
tests := []struct {
name string
args args
want *URI
wantErr bool
}{
{"ok", args{"yubikey", "yubikey:slot-id=9a"}, &URI{
URL: &url.URL{Scheme: "yubikey", Opaque: "slot-id=9a"},
Values: url.Values{"slot-id": []string{"9a"}},
}, false},
{"ok file", args{"file", "file:///tmp/ca.cert"}, &URI{
URL: &url.URL{Scheme: "file", Path: "/tmp/ca.cert"},
Values: url.Values{},
}, false},
{"fail parse", args{"yubikey", "yubikey"}, nil, true},
{"fail scheme", args{"yubikey", "awskms:slot-id=9a"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseWithScheme(tt.args.scheme, tt.args.rawuri)
if (err != nil) != tt.wantErr {
t.Errorf("ParseWithScheme() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseWithScheme() = %v, want %v", got, tt.want)
}
})
}
}
func TestURI_Get(t *testing.T) {
mustParse := func(s string) *URI {
u, err := Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
type args struct {
key string
}
tests := []struct {
name string
uri *URI
args args
want string
}{
{"ok", mustParse("yubikey:slot-id=9a"), args{"slot-id"}, "9a"},
{"ok first", mustParse("yubikey:slot-id=9a;slot-id=9b"), args{"slot-id"}, "9a"},
{"ok multiple", mustParse("yubikey:slot-id=9a;foo=bar"), args{"foo"}, "bar"},
{"fail missing", mustParse("yubikey:slot-id=9a"), args{"foo"}, ""},
{"fail in query", mustParse("yubikey:slot-id=9a?foo=bar"), args{"foo"}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.uri.Get(tt.args.key); got != tt.want {
t.Errorf("URI.Get() = %v, want %v", got, tt.want)
}
})
}
}