commit
2ca63a9ff5
@ -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(®ion, "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[:]
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue