commit
cf7ef472f7
@ -0,0 +1,68 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotImplemented
|
||||
type ErrNotImplemented struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e ErrNotImplemented) Error() string {
|
||||
if e.msg != "" {
|
||||
return e.msg
|
||||
}
|
||||
return "not implemented"
|
||||
}
|
||||
|
||||
// Type represents the KMS type used.
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// DefaultKMS is a KMS implementation using software.
|
||||
DefaultKMS Type = ""
|
||||
// SoftKMS is a KMS implementation using software.
|
||||
SoftKMS Type = "softkms"
|
||||
// CloudKMS is a KMS implementation using Google's Cloud KMS.
|
||||
CloudKMS Type = "cloudkms"
|
||||
// AmazonKMS is a KMS implementation using Amazon AWS KMS.
|
||||
AmazonKMS Type = "awskms"
|
||||
// PKCS11 is a KMS implementation using the PKCS11 standard.
|
||||
PKCS11 Type = "pkcs11"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// The type of the KMS to use.
|
||||
Type string `json:"type"`
|
||||
|
||||
// Path to the credentials file used in CloudKMS.
|
||||
CredentialsFile string `json:"credentialsFile"`
|
||||
|
||||
// Path to the module used with PKCS11 KMS.
|
||||
Module string `json:"module"`
|
||||
|
||||
// Pin used to access the PKCS11 module.
|
||||
Pin string `json:"pin"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in Options.
|
||||
func (o *Options) Validate() error {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch Type(strings.ToLower(o.Type)) {
|
||||
case DefaultKMS, SoftKMS, CloudKMS:
|
||||
case AmazonKMS:
|
||||
return ErrNotImplemented{"support for AmazonKMS is not yet implemented"}
|
||||
case PKCS11:
|
||||
return ErrNotImplemented{"support for PKCS11 is not yet implemented"}
|
||||
default:
|
||||
return errors.Errorf("unsupported kms type %s", o.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOptions_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *Options
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil", nil, false},
|
||||
{"softkms", &Options{Type: "softkms"}, false},
|
||||
{"cloudkms", &Options{Type: "cloudkms"}, false},
|
||||
{"awskms", &Options{Type: "awskms"}, true},
|
||||
{"pkcs11", &Options{Type: "pkcs11"}, true},
|
||||
{"unsupported", &Options{Type: "unsupported"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.options.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrNotImplemented_Error(t *testing.T) {
|
||||
type fields struct {
|
||||
msg string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{"default", fields{}, "not implemented"},
|
||||
{"custom", fields{"custom message: not implemented"}, "custom message: not implemented"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := ErrNotImplemented{
|
||||
msg: tt.fields.msg,
|
||||
}
|
||||
if got := e.Error(); got != tt.want {
|
||||
t.Errorf("ErrNotImplemented.Error() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ProtectionLevel specifies on some KMS how cryptographic operations are
|
||||
// performed.
|
||||
type ProtectionLevel int
|
||||
|
||||
const (
|
||||
// Protection level not specified.
|
||||
UnspecifiedProtectionLevel ProtectionLevel = iota
|
||||
// Crypto operations are performed in software.
|
||||
Software
|
||||
// Crypto operations are performed in a Hardware Security Module.
|
||||
HSM
|
||||
)
|
||||
|
||||
// String returns a string representation of p.
|
||||
func (p ProtectionLevel) String() string {
|
||||
switch p {
|
||||
case UnspecifiedProtectionLevel:
|
||||
return "unspecified"
|
||||
case Software:
|
||||
return "software"
|
||||
case HSM:
|
||||
return "hsm"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", p)
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureAlgorithm used for cryptographic signing.
|
||||
type SignatureAlgorithm int
|
||||
|
||||
const (
|
||||
// Not specified.
|
||||
UnspecifiedSignAlgorithm SignatureAlgorithm = iota
|
||||
// RSASSA-PKCS1-v1_5 key and a SHA256 digest.
|
||||
SHA256WithRSA
|
||||
// RSASSA-PKCS1-v1_5 key and a SHA384 digest.
|
||||
SHA384WithRSA
|
||||
// RSASSA-PKCS1-v1_5 key and a SHA512 digest.
|
||||
SHA512WithRSA
|
||||
// RSASSA-PSS key with a SHA256 digest.
|
||||
SHA256WithRSAPSS
|
||||
// RSASSA-PSS key with a SHA384 digest.
|
||||
SHA384WithRSAPSS
|
||||
// RSASSA-PSS key with a SHA512 digest.
|
||||
SHA512WithRSAPSS
|
||||
// ECDSA on the NIST P-256 curve with a SHA256 digest.
|
||||
ECDSAWithSHA256
|
||||
// ECDSA on the NIST P-384 curve with a SHA384 digest.
|
||||
ECDSAWithSHA384
|
||||
// ECDSA on the NIST P-521 curve with a SHA512 digest.
|
||||
ECDSAWithSHA512
|
||||
// EdDSA on Curve25519 with a SHA512 digest.
|
||||
PureEd25519
|
||||
)
|
||||
|
||||
// String returns a string representation of s.
|
||||
func (s SignatureAlgorithm) String() string {
|
||||
switch s {
|
||||
case UnspecifiedSignAlgorithm:
|
||||
return "unspecified"
|
||||
case SHA256WithRSA:
|
||||
return "SHA256-RSA"
|
||||
case SHA384WithRSA:
|
||||
return "SHA384-RSA"
|
||||
case SHA512WithRSA:
|
||||
return "SHA512-RSA"
|
||||
case SHA256WithRSAPSS:
|
||||
return "SHA256-RSAPSS"
|
||||
case SHA384WithRSAPSS:
|
||||
return "SHA384-RSAPSS"
|
||||
case SHA512WithRSAPSS:
|
||||
return "SHA512-RSAPSS"
|
||||
case ECDSAWithSHA256:
|
||||
return "ECDSA-SHA256"
|
||||
case ECDSAWithSHA384:
|
||||
return "ECDSA-SHA384"
|
||||
case ECDSAWithSHA512:
|
||||
return "ECDSA-SHA512"
|
||||
case PureEd25519:
|
||||
return "Ed25519"
|
||||
default:
|
||||
return fmt.Sprintf("unknown(%d)", s)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPublicKeyRequest is the parameter used in the kms.GetPublicKey method.
|
||||
type GetPublicKeyRequest struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// CreateKeyRequest is the parameter used in the kms.CreateKey method.
|
||||
type CreateKeyRequest struct {
|
||||
Name string
|
||||
SignatureAlgorithm SignatureAlgorithm
|
||||
Bits int
|
||||
|
||||
// ProtectionLevel specifies how cryptographic operations are performed.
|
||||
// Used by: cloudkms
|
||||
ProtectionLevel ProtectionLevel
|
||||
}
|
||||
|
||||
// CreateKeyResponse is the response value of the kms.CreateKey method.
|
||||
type CreateKeyResponse struct {
|
||||
Name string
|
||||
PublicKey crypto.PublicKey
|
||||
PrivateKey crypto.PrivateKey
|
||||
CreateSignerRequest CreateSignerRequest
|
||||
}
|
||||
|
||||
// CreateSignerRequest is the parameter used in the kms.CreateSigner method.
|
||||
type CreateSignerRequest struct {
|
||||
Signer crypto.Signer
|
||||
SigningKey string
|
||||
SigningKeyPEM []byte
|
||||
TokenLabel string
|
||||
PublicKey string
|
||||
PublicKeyPEM []byte
|
||||
Password []byte
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package apiv1
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestProtectionLevel_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p ProtectionLevel
|
||||
want string
|
||||
}{
|
||||
{"unspecified", UnspecifiedProtectionLevel, "unspecified"},
|
||||
{"software", Software, "software"},
|
||||
{"hsm", HSM, "hsm"},
|
||||
{"unknown", ProtectionLevel(100), "unknown(100)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.String(); got != tt.want {
|
||||
t.Errorf("ProtectionLevel.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureAlgorithm_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s SignatureAlgorithm
|
||||
want string
|
||||
}{
|
||||
{"UnspecifiedSignAlgorithm", UnspecifiedSignAlgorithm, "unspecified"},
|
||||
{"SHA256WithRSA", SHA256WithRSA, "SHA256-RSA"},
|
||||
{"SHA384WithRSA", SHA384WithRSA, "SHA384-RSA"},
|
||||
{"SHA512WithRSA", SHA512WithRSA, "SHA512-RSA"},
|
||||
{"SHA256WithRSAPSS", SHA256WithRSAPSS, "SHA256-RSAPSS"},
|
||||
{"SHA384WithRSAPSS", SHA384WithRSAPSS, "SHA384-RSAPSS"},
|
||||
{"SHA512WithRSAPSS", SHA512WithRSAPSS, "SHA512-RSAPSS"},
|
||||
{"ECDSAWithSHA256", ECDSAWithSHA256, "ECDSA-SHA256"},
|
||||
{"ECDSAWithSHA384", ECDSAWithSHA384, "ECDSA-SHA384"},
|
||||
{"ECDSAWithSHA512", ECDSAWithSHA512, "ECDSA-SHA512"},
|
||||
{"PureEd25519", PureEd25519, "Ed25519"},
|
||||
{"unknown", SignatureAlgorithm(100), "unknown(100)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.s.String(); got != tt.want {
|
||||
t.Errorf("SignatureAlgorithm.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
package cloudkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
cloudkms "cloud.google.com/go/kms/apiv1"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
"google.golang.org/api/option"
|
||||
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
|
||||
)
|
||||
|
||||
// protectionLevelMapping maps step protection levels with cloud kms ones.
|
||||
var protectionLevelMapping = map[apiv1.ProtectionLevel]kmspb.ProtectionLevel{
|
||||
apiv1.UnspecifiedProtectionLevel: kmspb.ProtectionLevel_PROTECTION_LEVEL_UNSPECIFIED,
|
||||
apiv1.Software: kmspb.ProtectionLevel_SOFTWARE,
|
||||
apiv1.HSM: kmspb.ProtectionLevel_HSM,
|
||||
}
|
||||
|
||||
// signatureAlgorithmMapping is a mapping between the step signature algorithm,
|
||||
// and bits for RSA keys, with cloud kms one.
|
||||
//
|
||||
// Cloud KMS does not support SHA384WithRSA, SHA384WithRSAPSS, SHA384WithRSAPSS,
|
||||
// ECDSAWithSHA512, and PureEd25519.
|
||||
var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{
|
||||
apiv1.UnspecifiedSignAlgorithm: kmspb.CryptoKeyVersion_CRYPTO_KEY_VERSION_ALGORITHM_UNSPECIFIED,
|
||||
apiv1.SHA256WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
|
||||
0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
|
||||
2048: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256,
|
||||
3072: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
|
||||
4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
|
||||
},
|
||||
apiv1.SHA512WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
|
||||
0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
|
||||
4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
|
||||
},
|
||||
apiv1.SHA256WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
|
||||
0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256,
|
||||
2048: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256,
|
||||
3072: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256,
|
||||
4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256,
|
||||
},
|
||||
apiv1.SHA512WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{
|
||||
0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512,
|
||||
4096: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512,
|
||||
},
|
||||
apiv1.ECDSAWithSHA256: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
|
||||
apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384,
|
||||
}
|
||||
|
||||
// KeyManagementClient defines the methods on KeyManagementClient that this
|
||||
// package will use. This interface will be used for unit testing.
|
||||
type KeyManagementClient interface {
|
||||
Close() error
|
||||
GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error)
|
||||
AsymmetricSign(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error)
|
||||
CreateCryptoKey(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error)
|
||||
GetKeyRing(context.Context, *kmspb.GetKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error)
|
||||
CreateKeyRing(context.Context, *kmspb.CreateKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error)
|
||||
CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error)
|
||||
}
|
||||
|
||||
// CloudKMS implements a KMS using Google's Cloud apiv1.
|
||||
type CloudKMS struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
|
||||
// New creates a new CloudKMS configured with a new client.
|
||||
func New(ctx context.Context, opts apiv1.Options) (*CloudKMS, error) {
|
||||
var cloudOpts []option.ClientOption
|
||||
if opts.CredentialsFile != "" {
|
||||
cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile))
|
||||
}
|
||||
|
||||
client, err := cloudkms.NewKeyManagementClient(ctx, cloudOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CloudKMS{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCloudKMS creates a CloudKMS with a given client.
|
||||
func NewCloudKMS(client KeyManagementClient) *CloudKMS {
|
||||
return &CloudKMS{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection of the Cloud KMS client.
|
||||
func (k *CloudKMS) Close() error {
|
||||
if err := k.client.Close(); err != nil {
|
||||
return errors.Wrap(err, "cloudKMS Close failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSigner returns a new cloudkms signer configured with the given signing
|
||||
// key name.
|
||||
func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
|
||||
if req.SigningKey == "" {
|
||||
return nil, errors.New("signing key cannot be empty")
|
||||
}
|
||||
|
||||
return NewSigner(k.client, req.SigningKey), nil
|
||||
}
|
||||
|
||||
// CreateKey creates in Google's Cloud KMS a new asymmetric key for signing.
|
||||
func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
|
||||
if req.Name == "" {
|
||||
return nil, errors.New("createKeyRequest 'name' cannot be empty")
|
||||
}
|
||||
|
||||
protectionLevel, ok := protectionLevelMapping[req.ProtectionLevel]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cloudKMS does not support protection level '%s'", req.ProtectionLevel)
|
||||
}
|
||||
|
||||
var signatureAlgorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm
|
||||
v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s'", req.SignatureAlgorithm)
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm:
|
||||
signatureAlgorithm = v
|
||||
case map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm:
|
||||
if signatureAlgorithm, ok = v[req.Bits]; !ok {
|
||||
return nil, errors.Errorf("cloudKMS does not support signature algorithm '%s' with '%d' bits", req.SignatureAlgorithm, req.Bits)
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unexpected error: this should not happen")
|
||||
}
|
||||
|
||||
var crytoKeyName string
|
||||
|
||||
// Split `projects/PROJECT_ID/locations/global/keyRings/RING_ID/cryptoKeys/KEY_ID`
|
||||
// to `projects/PROJECT_ID/locations/global/keyRings/RING_ID` and `KEY_ID`.
|
||||
keyRing, keyID := Parent(req.Name)
|
||||
if err := k.createKeyRingIfNeeded(keyRing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
// Create private key in CloudKMS.
|
||||
response, err := k.client.CreateCryptoKey(ctx, &kmspb.CreateCryptoKeyRequest{
|
||||
Parent: keyRing,
|
||||
CryptoKeyId: keyID,
|
||||
CryptoKey: &kmspb.CryptoKey{
|
||||
Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN,
|
||||
VersionTemplate: &kmspb.CryptoKeyVersionTemplate{
|
||||
ProtectionLevel: protectionLevel,
|
||||
Algorithm: signatureAlgorithm,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if status.Code(err) != codes.AlreadyExists {
|
||||
return nil, errors.Wrap(err, "cloudKMS CreateCryptoKey failed")
|
||||
}
|
||||
// Create a new version if the key already exists.
|
||||
//
|
||||
// Note that it will have the same purpose, protection level and
|
||||
// algorithm than as previous one.
|
||||
req := &kmspb.CreateCryptoKeyVersionRequest{
|
||||
Parent: req.Name,
|
||||
CryptoKeyVersion: &kmspb.CryptoKeyVersion{
|
||||
State: kmspb.CryptoKeyVersion_ENABLED,
|
||||
},
|
||||
}
|
||||
response, err := k.client.CreateCryptoKeyVersion(ctx, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudKMS CreateCryptoKeyVersion failed")
|
||||
}
|
||||
crytoKeyName = response.Name
|
||||
} else {
|
||||
crytoKeyName = response.Name + "/cryptoKeyVersions/1"
|
||||
}
|
||||
|
||||
// Retrieve public key to add it to the response.
|
||||
pk, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{
|
||||
Name: crytoKeyName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed")
|
||||
}
|
||||
|
||||
return &apiv1.CreateKeyResponse{
|
||||
Name: crytoKeyName,
|
||||
PublicKey: pk,
|
||||
CreateSignerRequest: apiv1.CreateSignerRequest{
|
||||
SigningKey: crytoKeyName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *CloudKMS) createKeyRingIfNeeded(name string) error {
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
_, err := k.client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
parent, child := Parent(name)
|
||||
_, err = k.client.CreateKeyRing(ctx, &kmspb.CreateKeyRingRequest{
|
||||
Parent: parent,
|
||||
KeyRingId: child,
|
||||
})
|
||||
if err != nil && status.Code(err) != codes.AlreadyExists {
|
||||
return errors.Wrap(err, "cloudKMS CreateKeyRing failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPublicKey gets from Google's Cloud KMS a public key by name. Key names
|
||||
// follow the pattern:
|
||||
// projects/([^/]+)/locations/([a-zA-Z0-9_-]{1,63})/keyRings/([a-zA-Z0-9_-]{1,63})/cryptoKeys/([a-zA-Z0-9_-]{1,63})/cryptoKeyVersions/([a-zA-Z0-9_-]{1,63})
|
||||
func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
|
||||
if req.Name == "" {
|
||||
return nil, errors.New("createKeyRequest 'name' cannot be empty")
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
response, err := k.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed")
|
||||
}
|
||||
|
||||
pk, err := pemutil.ParseKey([]byte(response.Pem))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
func defaultContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 15*time.Second)
|
||||
}
|
||||
|
||||
// Parent splits a string in the format `key/value/key2/value2` in a parent and
|
||||
// child, for the previous string it will return `key/value` and `value2`.
|
||||
func Parent(name string) (string, string) {
|
||||
a, b := parent(name)
|
||||
a, _ = parent(a)
|
||||
return a, b
|
||||
}
|
||||
|
||||
func parent(name string) (string, string) {
|
||||
i := strings.LastIndex(name, "/")
|
||||
switch i {
|
||||
case -1:
|
||||
return "", name
|
||||
case 0:
|
||||
return "", name[i+1:]
|
||||
default:
|
||||
return name[:i], name[i+1:]
|
||||
}
|
||||
}
|
@ -0,0 +1,376 @@
|
||||
package cloudkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestParent(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
want1 string
|
||||
}{
|
||||
{"zero", args{"child"}, "", "child"},
|
||||
{"one", args{"parent/child"}, "", "child"},
|
||||
{"two", args{"grandparent/parent/child"}, "grandparent", "child"},
|
||||
{"three", args{"great-grandparent/grandparent/parent/child"}, "great-grandparent/grandparent", "child"},
|
||||
{"empty", args{""}, "", ""},
|
||||
{"root", args{"/"}, "", ""},
|
||||
{"child", args{"/child"}, "", "child"},
|
||||
{"parent", args{"parent/"}, "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := Parent(tt.args.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("Parent() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("Parent() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
skipOnCI bool
|
||||
args args
|
||||
want *CloudKMS
|
||||
wantErr bool
|
||||
}{
|
||||
{"fail authentication", true, args{context.Background(), apiv1.Options{}}, nil, true},
|
||||
{"fail credentials", false, args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.skipOnCI && os.Getenv("CI") == "true" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
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 !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCloudKMS(t *testing.T) {
|
||||
type args struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *CloudKMS
|
||||
}{
|
||||
{"ok", args{&MockClient{}}, &CloudKMS{&MockClient{}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NewCloudKMS(tt.args.client); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NewCloudKMS() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudKMS_Close(t *testing.T) {
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&MockClient{close: func() error { return nil }}}, false},
|
||||
{"fail", fields{&MockClient{close: func() error { return fmt.Errorf("an error") }}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &CloudKMS{
|
||||
client: tt.fields.client,
|
||||
}
|
||||
if err := k.Close(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudKMS.Close() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudKMS_CreateSigner(t *testing.T) {
|
||||
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.CreateSignerRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want crypto.Signer
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&MockClient{}}, args{&apiv1.CreateSignerRequest{SigningKey: keyName}}, &Signer{client: &MockClient{}, signingKey: keyName}, false},
|
||||
{"fail", fields{&MockClient{}}, args{&apiv1.CreateSignerRequest{SigningKey: ""}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &CloudKMS{
|
||||
client: tt.fields.client,
|
||||
}
|
||||
got, err := k.CreateSigner(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudKMS.CreateSigner() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudKMS_CreateKey(t *testing.T) {
|
||||
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c"
|
||||
testError := fmt.Errorf("an error")
|
||||
alreadyExists := status.Error(codes.AlreadyExists, "already exists")
|
||||
|
||||
pemBytes, err := ioutil.ReadFile("testdata/pub.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pk, err := pemutil.ParseKey(pemBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.CreateKeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.CreateKeyResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return &kmspb.KeyRing{}, nil
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return &kmspb.CryptoKey{Name: keyName}, nil
|
||||
},
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
&apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/1"}}, false},
|
||||
{"ok new key ring", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return nil, testError
|
||||
},
|
||||
createKeyRing: func(_ context.Context, _ *kmspb.CreateKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return nil, alreadyExists
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return &kmspb.CryptoKey{Name: keyName}, nil
|
||||
},
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 3072}},
|
||||
&apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/1"}}, false},
|
||||
{"ok new key version", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return &kmspb.KeyRing{}, nil
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return nil, alreadyExists
|
||||
},
|
||||
createCryptoKeyVersion: func(_ context.Context, _ *kmspb.CreateCryptoKeyVersionRequest, _ ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) {
|
||||
return &kmspb.CryptoKeyVersion{Name: keyName + "/cryptoKeyVersions/2"}, nil
|
||||
},
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
&apiv1.CreateKeyResponse{Name: keyName + "/cryptoKeyVersions/2", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: keyName + "/cryptoKeyVersions/2"}}, false},
|
||||
{"fail name", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{}}, nil, true},
|
||||
{"fail protection level", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.ProtectionLevel(100)}}, nil, true},
|
||||
{"fail signature algorithm", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SignatureAlgorithm(100)}}, nil, true},
|
||||
{"fail number of bits", fields{&MockClient{}}, args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.Software, SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 1024}},
|
||||
nil, true},
|
||||
{"fail create key ring", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return nil, testError
|
||||
},
|
||||
createKeyRing: func(_ context.Context, _ *kmspb.CreateKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
nil, true},
|
||||
{"fail create key", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return &kmspb.KeyRing{}, nil
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
nil, true},
|
||||
{"fail create key version", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return &kmspb.KeyRing{}, nil
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return nil, alreadyExists
|
||||
},
|
||||
createCryptoKeyVersion: func(_ context.Context, _ *kmspb.CreateCryptoKeyVersionRequest, _ ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
nil, true},
|
||||
{"fail get public key", fields{
|
||||
&MockClient{
|
||||
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return &kmspb.KeyRing{}, nil
|
||||
},
|
||||
createCryptoKey: func(_ context.Context, _ *kmspb.CreateCryptoKeyRequest, _ ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return &kmspb.CryptoKey{Name: keyName}, nil
|
||||
},
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}},
|
||||
args{&apiv1.CreateKeyRequest{Name: keyName, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
|
||||
nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &CloudKMS{
|
||||
client: tt.fields.client,
|
||||
}
|
||||
got, err := k.CreateKey(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudKMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudKMS.CreateKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudKMS_GetPublicKey(t *testing.T) {
|
||||
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
|
||||
testError := fmt.Errorf("an error")
|
||||
|
||||
pemBytes, err := ioutil.ReadFile("testdata/pub.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pk, err := pemutil.ParseKey(pemBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.GetPublicKeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want crypto.PublicKey
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{
|
||||
&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
|
||||
},
|
||||
}},
|
||||
args{&apiv1.GetPublicKeyRequest{Name: keyName}}, pk, false},
|
||||
{"fail name", fields{&MockClient{}}, args{&apiv1.GetPublicKeyRequest{}}, nil, true},
|
||||
{"fail get public key", fields{
|
||||
&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}},
|
||||
args{&apiv1.GetPublicKeyRequest{Name: keyName}}, nil, true},
|
||||
{"fail parse pem", fields{
|
||||
&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string("bad pem")}, nil
|
||||
},
|
||||
}},
|
||||
args{&apiv1.GetPublicKeyRequest{Name: keyName}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &CloudKMS{
|
||||
client: tt.fields.client,
|
||||
}
|
||||
got, err := k.GetPublicKey(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudKMS.GetPublicKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package cloudkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
|
||||
)
|
||||
|
||||
type MockClient struct {
|
||||
close func() error
|
||||
getPublicKey func(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error)
|
||||
asymmetricSign func(context.Context, *kmspb.AsymmetricSignRequest, ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error)
|
||||
createCryptoKey func(context.Context, *kmspb.CreateCryptoKeyRequest, ...gax.CallOption) (*kmspb.CryptoKey, error)
|
||||
getKeyRing func(context.Context, *kmspb.GetKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error)
|
||||
createKeyRing func(context.Context, *kmspb.CreateKeyRingRequest, ...gax.CallOption) (*kmspb.KeyRing, error)
|
||||
createCryptoKeyVersion func(context.Context, *kmspb.CreateCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error)
|
||||
}
|
||||
|
||||
func (m *MockClient) Close() error {
|
||||
return m.close()
|
||||
}
|
||||
|
||||
func (m *MockClient) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return m.getPublicKey(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (m *MockClient) AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
|
||||
return m.asymmetricSign(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (m *MockClient) CreateCryptoKey(ctx context.Context, req *kmspb.CreateCryptoKeyRequest, opts ...gax.CallOption) (*kmspb.CryptoKey, error) {
|
||||
return m.createCryptoKey(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (m *MockClient) GetKeyRing(ctx context.Context, req *kmspb.GetKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return m.getKeyRing(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (m *MockClient) CreateKeyRing(ctx context.Context, req *kmspb.CreateKeyRingRequest, opts ...gax.CallOption) (*kmspb.KeyRing, error) {
|
||||
return m.createKeyRing(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func (m *MockClient) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) {
|
||||
return m.createCryptoKeyVersion(ctx, req, opts...)
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package cloudkms
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
|
||||
)
|
||||
|
||||
// Signer implements a crypto.Signer using Google's Cloud KMS.
|
||||
type Signer struct {
|
||||
client KeyManagementClient
|
||||
signingKey string
|
||||
}
|
||||
|
||||
func NewSigner(c KeyManagementClient, signingKey string) *Signer {
|
||||
return &Signer{
|
||||
client: c,
|
||||
signingKey: signingKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Public returns the public key of this signer or an error.
|
||||
func (s *Signer) Public() crypto.PublicKey {
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
response, err := s.client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{
|
||||
Name: s.signingKey,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cloudKMS GetPublicKey failed")
|
||||
}
|
||||
|
||||
pk, err := pemutil.ParseKey([]byte(response.Pem))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pk
|
||||
}
|
||||
|
||||
// Sign signs digest with the private key stored in Google's Cloud KMS.
|
||||
func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
||||
req := &kmspb.AsymmetricSignRequest{
|
||||
Name: s.signingKey,
|
||||
Digest: &kmspb.Digest{},
|
||||
}
|
||||
|
||||
switch h := opts.HashFunc(); h {
|
||||
case crypto.SHA256:
|
||||
req.Digest.Digest = &kmspb.Digest_Sha256{
|
||||
Sha256: digest,
|
||||
}
|
||||
case crypto.SHA384:
|
||||
req.Digest.Digest = &kmspb.Digest_Sha384{
|
||||
Sha384: digest,
|
||||
}
|
||||
case crypto.SHA512:
|
||||
req.Digest.Digest = &kmspb.Digest_Sha512{
|
||||
Sha512: digest,
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported hash function %v", h)
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
response, err := s.client.AsymmetricSign(ctx, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudKMS AsymmetricSign failed")
|
||||
}
|
||||
|
||||
return response.Signature, nil
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package cloudkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
|
||||
)
|
||||
|
||||
func Test_newSigner(t *testing.T) {
|
||||
type args struct {
|
||||
c KeyManagementClient
|
||||
signingKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Signer
|
||||
}{
|
||||
{"ok", args{&MockClient{}, "signingKey"}, &Signer{client: &MockClient{}, signingKey: "signingKey"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NewSigner(tt.args.c, tt.args.signingKey); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("newSigner() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_signer_Public(t *testing.T) {
|
||||
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
|
||||
testError := fmt.Errorf("an error")
|
||||
|
||||
pemBytes, err := ioutil.ReadFile("testdata/pub.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pk, err := pemutil.ParseKey(pemBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
signingKey string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want crypto.PublicKey
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
|
||||
},
|
||||
}, keyName}, pk, false},
|
||||
{"fail get public key", fields{&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return nil, testError
|
||||
},
|
||||
}, keyName}, nil, true},
|
||||
{"fail parse pem", fields{
|
||||
&MockClient{
|
||||
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
|
||||
return &kmspb.PublicKey{Pem: string("bad pem")}, nil
|
||||
},
|
||||
}, keyName}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Signer{
|
||||
client: tt.fields.client,
|
||||
signingKey: tt.fields.signingKey,
|
||||
}
|
||||
got := s.Public()
|
||||
if _, ok := got.(error); ok != tt.wantErr {
|
||||
t.Errorf("signer.Public() error = %v, wantErr %v", got, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("signer.Public() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_signer_Sign(t *testing.T) {
|
||||
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
|
||||
okClient := &MockClient{
|
||||
asymmetricSign: func(_ context.Context, _ *kmspb.AsymmetricSignRequest, _ ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
|
||||
return &kmspb.AsymmetricSignResponse{Signature: []byte("ok signature")}, nil
|
||||
},
|
||||
}
|
||||
failClient := &MockClient{
|
||||
asymmetricSign: func(_ context.Context, _ *kmspb.AsymmetricSignRequest, _ ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) {
|
||||
return nil, fmt.Errorf("an error")
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
client KeyManagementClient
|
||||
signingKey string
|
||||
}
|
||||
type args struct {
|
||||
rand io.Reader
|
||||
digest []byte
|
||||
opts crypto.SignerOpts
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok sha256", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA256}, []byte("ok signature"), false},
|
||||
{"ok sha384", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA384}, []byte("ok signature"), false},
|
||||
{"ok sha512", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA512}, []byte("ok signature"), false},
|
||||
{"fail MD5", fields{okClient, keyName}, args{rand.Reader, []byte("digest"), crypto.MD5}, nil, true},
|
||||
{"fail asymmetric sign", fields{failClient, keyName}, args{rand.Reader, []byte("digest"), crypto.SHA256}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Signer{
|
||||
client: tt.fields.client,
|
||||
signingKey: tt.fields.signingKey,
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1
|
||||
veT13hakPZF9YzaNVZgujqK3d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w==
|
||||
-----END PUBLIC KEY-----
|
@ -0,0 +1,36 @@
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/certificates/kms/cloudkms"
|
||||
"github.com/smallstep/certificates/kms/softkms"
|
||||
)
|
||||
|
||||
// KeyManager is the interface implemented by all the KMS.
|
||||
type KeyManager interface {
|
||||
GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error)
|
||||
CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error)
|
||||
CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// New initializes a new KMS from the given type.
|
||||
func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) {
|
||||
if err := opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch apiv1.Type(strings.ToLower(opts.Type)) {
|
||||
case apiv1.DefaultKMS, apiv1.SoftKMS:
|
||||
return softkms.New(ctx, opts)
|
||||
case apiv1.CloudKMS:
|
||||
return cloudkms.New(ctx, opts)
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported kms type '%s'", opts.Type)
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/certificates/kms/cloudkms"
|
||||
"github.com/smallstep/certificates/kms/softkms"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
skipOnCI bool
|
||||
args args
|
||||
want KeyManager
|
||||
wantErr bool
|
||||
}{
|
||||
{"softkms", false, args{ctx, apiv1.Options{Type: "softkms"}}, &softkms.SoftKMS{}, false},
|
||||
{"default", false, args{ctx, apiv1.Options{}}, &softkms.SoftKMS{}, 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},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.skipOnCI && os.Getenv("CI") == "true" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
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 reflect.TypeOf(got) != reflect.TypeOf(tt.want) {
|
||||
t.Errorf("New() = %T, want %T", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package softkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/cli/crypto/keys"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
)
|
||||
|
||||
type algorithmAttributes struct {
|
||||
Type string
|
||||
Curve string
|
||||
}
|
||||
|
||||
// DefaultRSAKeySize is the default size for RSA keys.
|
||||
const DefaultRSAKeySize = 3072
|
||||
|
||||
var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]algorithmAttributes{
|
||||
apiv1.UnspecifiedSignAlgorithm: {"EC", "P-256"},
|
||||
apiv1.SHA256WithRSA: {"RSA", ""},
|
||||
apiv1.SHA384WithRSA: {"RSA", ""},
|
||||
apiv1.SHA512WithRSA: {"RSA", ""},
|
||||
apiv1.SHA256WithRSAPSS: {"RSA", ""},
|
||||
apiv1.SHA384WithRSAPSS: {"RSA", ""},
|
||||
apiv1.SHA512WithRSAPSS: {"RSA", ""},
|
||||
apiv1.ECDSAWithSHA256: {"EC", "P-256"},
|
||||
apiv1.ECDSAWithSHA384: {"EC", "P-384"},
|
||||
apiv1.ECDSAWithSHA512: {"EC", "P-521"},
|
||||
apiv1.PureEd25519: {"OKP", "Ed25519"},
|
||||
}
|
||||
|
||||
// generateKey is used for testing purposes.
|
||||
var generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) {
|
||||
if kty == "RSA" && size == 0 {
|
||||
size = DefaultRSAKeySize
|
||||
}
|
||||
return keys.GenerateKeyPair(kty, crv, size)
|
||||
}
|
||||
|
||||
// SoftKMS is a key manager that uses keys stored in disk.
|
||||
type SoftKMS struct{}
|
||||
|
||||
// New returns a new SoftKMS.
|
||||
func New(ctx context.Context, opts apiv1.Options) (*SoftKMS, error) {
|
||||
return &SoftKMS{}, nil
|
||||
}
|
||||
|
||||
// Close is a noop that just returns nil.
|
||||
func (k *SoftKMS) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSigner returns a new signer configured with the given signing key.
|
||||
func (k *SoftKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) {
|
||||
var opts []pemutil.Options
|
||||
if req.Password != nil {
|
||||
opts = append(opts, pemutil.WithPassword(req.Password))
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Signer != nil:
|
||||
return req.Signer, nil
|
||||
case len(req.SigningKeyPEM) != 0:
|
||||
v, err := pemutil.ParseKey(req.SigningKeyPEM, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, ok := v.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("signingKeyPEM is not a crypto.Signer")
|
||||
}
|
||||
return sig, nil
|
||||
case req.SigningKey != "":
|
||||
v, err := pemutil.Read(req.SigningKey, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, ok := v.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("signingKey is not a crypto.Signer")
|
||||
}
|
||||
return sig, nil
|
||||
default:
|
||||
return nil, errors.New("failed to load softKMS: please define signingKeyPEM or signingKey")
|
||||
}
|
||||
}
|
||||
|
||||
func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) {
|
||||
v, ok := signatureAlgorithmMapping[req.SignatureAlgorithm]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("softKMS does not support signature algorithm '%s'", req.SignatureAlgorithm)
|
||||
}
|
||||
|
||||
pub, priv, err := generateKey(v.Type, v.Curve, req.Bits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, ok := priv.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("softKMS createKey result is not a crypto.Signer: type %T", priv)
|
||||
}
|
||||
|
||||
return &apiv1.CreateKeyResponse{
|
||||
Name: req.Name,
|
||||
PublicKey: pub,
|
||||
PrivateKey: priv,
|
||||
CreateSignerRequest: apiv1.CreateSignerRequest{
|
||||
Signer: signer,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
|
||||
v, err := pemutil.Read(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case *x509.Certificate:
|
||||
return vv.PublicKey, nil
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
|
||||
return vv, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported public key type %T", v)
|
||||
}
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
package softkms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *SoftKMS
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{context.Background(), apiv1.Options{}}, &SoftKMS{}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.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 !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftKMS_Close(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &SoftKMS{}
|
||||
if err := k.Close(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftKMS.Close() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftKMS_CreateSigner(t *testing.T) {
|
||||
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pemBlock, err := pemutil.Serialize(pk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pemBlockPassword, err := pemutil.Serialize(pk, pemutil.WithPassword([]byte("pass")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Read and decode file using standard packages
|
||||
b, err := ioutil.ReadFile("testdata/priv.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, _ := pem.Decode(b)
|
||||
block.Bytes, err = x509.DecryptPEMBlock(block, []byte("pass"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pk2, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a public PEM
|
||||
b, err = x509.MarshalPKIXPublicKey(pk.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pub := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: b,
|
||||
})
|
||||
|
||||
type args struct {
|
||||
req *apiv1.CreateSignerRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want crypto.Signer
|
||||
wantErr bool
|
||||
}{
|
||||
{"signer", args{&apiv1.CreateSignerRequest{Signer: pk}}, pk, false},
|
||||
{"pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlock)}}, pk, false},
|
||||
{"pem password", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pem.EncodeToMemory(pemBlockPassword), Password: []byte("pass")}}, pk, false},
|
||||
{"file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("pass")}}, pk2, false},
|
||||
{"fail", args{&apiv1.CreateSignerRequest{}}, nil, true},
|
||||
{"fail bad pem", args{&apiv1.CreateSignerRequest{SigningKeyPEM: []byte("bad pem")}}, nil, true},
|
||||
{"fail bad password", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/priv.pem", Password: []byte("bad-pass")}}, nil, true},
|
||||
{"fail not a signer", args{&apiv1.CreateSignerRequest{SigningKeyPEM: pub}}, nil, true},
|
||||
{"fail not a signer from file", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/pub.pem"}}, nil, true},
|
||||
{"fail missing", args{&apiv1.CreateSignerRequest{SigningKey: "testdata/missing"}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &SoftKMS{}
|
||||
got, err := k.CreateSigner(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftKMS.CreateSigner() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func restoreGenerateKey() func() {
|
||||
oldGenerateKey := generateKey
|
||||
return func() {
|
||||
generateKey = oldGenerateKey
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftKMS_CreateKey(t *testing.T) {
|
||||
fn := restoreGenerateKey()
|
||||
defer fn()
|
||||
|
||||
p256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
edpub, edpriv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
req *apiv1.CreateKeyRequest
|
||||
}
|
||||
type params struct {
|
||||
kty string
|
||||
crv string
|
||||
size int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
generateKey func() (interface{}, interface{}, error)
|
||||
want *apiv1.CreateKeyResponse
|
||||
wantParams params
|
||||
wantErr bool
|
||||
}{
|
||||
{"p256", args{&apiv1.CreateKeyRequest{Name: "p256", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) {
|
||||
return p256.Public(), p256, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "p256", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false},
|
||||
{"rsa", args{&apiv1.CreateKeyRequest{Name: "rsa3072", SignatureAlgorithm: apiv1.SHA256WithRSA}}, func() (interface{}, interface{}, error) {
|
||||
return rsa2048.Public(), rsa2048, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "rsa3072", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 0}, false},
|
||||
{"rsa2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSA, Bits: 2048}}, func() (interface{}, interface{}, error) {
|
||||
return rsa2048.Public(), rsa2048, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false},
|
||||
{"rsaPSS2048", args{&apiv1.CreateKeyRequest{Name: "rsa2048", SignatureAlgorithm: apiv1.SHA256WithRSAPSS, Bits: 2048}}, func() (interface{}, interface{}, error) {
|
||||
return rsa2048.Public(), rsa2048, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "rsa2048", PublicKey: rsa2048.Public(), PrivateKey: rsa2048, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: rsa2048}}, params{"RSA", "", 2048}, false},
|
||||
{"ed25519", args{&apiv1.CreateKeyRequest{Name: "ed25519", SignatureAlgorithm: apiv1.PureEd25519}}, func() (interface{}, interface{}, error) {
|
||||
return edpub, edpriv, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "ed25519", PublicKey: edpub, PrivateKey: edpriv, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: edpriv}}, params{"OKP", "Ed25519", 0}, false},
|
||||
{"default", args{&apiv1.CreateKeyRequest{Name: "default"}}, func() (interface{}, interface{}, error) {
|
||||
return p256.Public(), p256, nil
|
||||
}, &apiv1.CreateKeyResponse{Name: "default", PublicKey: p256.Public(), PrivateKey: p256, CreateSignerRequest: apiv1.CreateSignerRequest{Signer: p256}}, params{"EC", "P-256", 0}, false},
|
||||
{"fail algorithm", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.SignatureAlgorithm(100)}}, func() (interface{}, interface{}, error) {
|
||||
return p256.Public(), p256, nil
|
||||
}, nil, params{}, true},
|
||||
{"fail generate key", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) {
|
||||
return nil, nil, fmt.Errorf("an error")
|
||||
}, nil, params{"EC", "P-256", 0}, true},
|
||||
{"fail no signer", args{&apiv1.CreateKeyRequest{Name: "fail", SignatureAlgorithm: apiv1.ECDSAWithSHA256}}, func() (interface{}, interface{}, error) {
|
||||
return 1, 2, nil
|
||||
}, nil, params{"EC", "P-256", 0}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &SoftKMS{}
|
||||
generateKey = func(kty, crv string, size int) (interface{}, interface{}, error) {
|
||||
if tt.wantParams.kty != kty {
|
||||
t.Errorf("GenerateKey() kty = %s, want %s", kty, tt.wantParams.kty)
|
||||
}
|
||||
if tt.wantParams.crv != crv {
|
||||
t.Errorf("GenerateKey() crv = %s, want %s", crv, tt.wantParams.crv)
|
||||
}
|
||||
if tt.wantParams.size != size {
|
||||
t.Errorf("GenerateKey() size = %d, want %d", size, tt.wantParams.size)
|
||||
}
|
||||
return tt.generateKey()
|
||||
}
|
||||
|
||||
got, err := k.CreateKey(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftKMS.CreateKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftKMS.CreateKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftKMS_GetPublicKey(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("testdata/pub.pem")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
block, _ := pem.Decode(b)
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
req *apiv1.GetPublicKeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want crypto.PublicKey
|
||||
wantErr bool
|
||||
}{
|
||||
{"key", args{&apiv1.GetPublicKeyRequest{Name: "testdata/pub.pem"}}, pub, false},
|
||||
{"cert", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.crt"}}, pub, false},
|
||||
{"fail not exists", args{&apiv1.GetPublicKeyRequest{Name: "testdata/missing"}}, nil, true},
|
||||
{"fail type", args{&apiv1.GetPublicKeyRequest{Name: "testdata/cert.key"}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &SoftKMS{}
|
||||
got, err := k.GetPublicKey(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftKMS.GetPublicKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateKey(t *testing.T) {
|
||||
type args struct {
|
||||
kty string
|
||||
crv string
|
||||
size int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantType interface{}
|
||||
wantType1 interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{"rsa2048", args{"RSA", "", 0}, &rsa.PublicKey{}, &rsa.PrivateKey{}, false},
|
||||
{"rsa2048", args{"RSA", "", 2048}, &rsa.PublicKey{}, &rsa.PrivateKey{}, false},
|
||||
{"p256", args{"EC", "P-256", 0}, &ecdsa.PublicKey{}, &ecdsa.PrivateKey{}, false},
|
||||
{"ed25519", args{"OKP", "Ed25519", 0}, ed25519.PublicKey{}, ed25519.PrivateKey{}, false},
|
||||
{"fail kty", args{"FOO", "", 0}, nil, nil, true},
|
||||
{"fail crv", args{"EC", "P-123", 0}, nil, nil, true},
|
||||
{"fail size", args{"RSA", "", 1}, nil, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := generateKey(tt.args.kty, tt.args.crv, tt.args.size)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("generateKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if reflect.TypeOf(got) != reflect.TypeOf(tt.wantType) {
|
||||
t.Errorf("generateKey() got = %T, want %T", got, tt.wantType)
|
||||
}
|
||||
if reflect.TypeOf(got1) != reflect.TypeOf(tt.wantType1) {
|
||||
t.Errorf("generateKey() got1 = %T, want %T", got1, tt.wantType1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBpzCCAU2gAwIBAgIQWaY8KIDAfak8aYljelf8eTAKBggqhkjOPQQDAjAdMRsw
|
||||
GQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wHhcNMjAwMTE2MDAwNDU4WhcNMjAw
|
||||
MTE3MDAwNDU4WjAdMRswGQYDVQQDExJ0ZXN0LnNtYWxsc3RlcC5jb20wWTATBgcq
|
||||
hkjOPQIBBggqhkjOPQMBBwNCAATlU8P9blFefSWuzYx2g215NJn6yHW95PXeFqQ9
|
||||
kX1jNo1VmC6Oord3We37iM8QJT4QP9ZDUaAVmJUZSjd+W8H/o28wbTAOBgNVHQ8B
|
||||
Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW
|
||||
BBTn0wonKkm2lLRNYZrKhUukiynvqzAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl
|
||||
cC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAJ5XqryBIY1X4fl/9l0isV69eQfA0Qo5
|
||||
1mjervUcEnOWAiBsmN4frz5YVw7i4UXChVBeZLZfJOKvn5eyh2gEzoq1+w==
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEICB6lIrMa9fVQJtdAYS4qmdYQ1BHJsEQDx8zxL38gA8toAoGCCqGSM49
|
||||
AwEHoUQDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1veT13hakPZF9YzaNVZgujqK3
|
||||
d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,8 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,1fcec5dfbf3327f61bfe5ab6ae8a0626
|
||||
|
||||
V39b/pNHMbP80TXSHLsUY6UOTCzf3KwIxvj1e7S9brNMJJc9b3UiloMBJIYBkl00
|
||||
NKI8JU4jSlcerR58DqsTHIELiX6a+RJLe3/iR2/5Gru+CmmWJ68jQu872WCgh6Ms
|
||||
o8TzhyGx74ETmdKn5CdtylsnKMa9heW3tBLFAbNCgKc=
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5VPD/W5RXn0lrs2MdoNteTSZ+sh1
|
||||
veT13hakPZF9YzaNVZgujqK3d1nt+4jPECU+ED/WQ1GgFZiVGUo3flvB/w==
|
||||
-----END PUBLIC KEY-----
|
Loading…
Reference in New Issue