Skip to content

Commit

Permalink
Merge pull request #240 from smallstep/softkms-uris
Browse files Browse the repository at this point in the history
Add support for softkms and cloudkms uris on requests
  • Loading branch information
maraino committed May 25, 2023
2 parents fa2bf11 + ecd8cc5 commit 80680d7
Show file tree
Hide file tree
Showing 16 changed files with 194 additions and 26 deletions.
4 changes: 2 additions & 2 deletions kms/awskms/awskms.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import (
"go.step.sm/crypto/pemutil"
)

// Scheme is the scheme used in uris.
const Scheme = "awskms"
// Scheme is the scheme used in uris, the string "awskms".
const Scheme = string(apiv1.AmazonKMS)

// KMS implements a KMS using AWS Key Management Service.
type KMS struct {
Expand Down
5 changes: 3 additions & 2 deletions kms/azurekms/key_vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ func init() {
})
}

// Scheme is the scheme used for the Azure Key Vault uris.
const Scheme = "azurekms"
// Scheme is the scheme used for the Azure Key Vault uris, the string
// "azurekms".
const Scheme = string(apiv1.AzureKMS)

var (
valueTrue = true
Expand Down
4 changes: 2 additions & 2 deletions kms/capi/capi.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
"golang.org/x/sys/windows"
)

// Scheme is the scheme used in uris.
const Scheme = "capi"
// Scheme is the scheme used in uris, the string "capi".
const Scheme = string(apiv1.CAPIKMS)

const (
ProviderNameArg = "provider"
Expand Down
34 changes: 28 additions & 6 deletions kms/cloudkms/cloudkms.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"google.golang.org/api/option"
)

// Scheme is the scheme used in uris.
const Scheme = "cloudkms"
// Scheme is the scheme used in uris, the string "cloudkms".
const Scheme = string(apiv1.CloudKMS)

const pendingGenerationRetries = 10

Expand Down Expand Up @@ -158,7 +158,7 @@ func (k *CloudKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer,
if req.SigningKey == "" {
return nil, errors.New("signing key cannot be empty")
}
return NewSigner(k.client, req.SigningKey)
return NewSigner(k.client, resourceName(req.SigningKey))
}

// CreateKey creates in Google's Cloud KMS a new asymmetric key for signing.
Expand Down Expand Up @@ -190,9 +190,12 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo

var crytoKeyName string

// resource is the plain Google Cloud KMS resource name
resource := resourceName(req.Name)

// 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)
keyRing, keyID := Parent(resource)
if err := k.createKeyRingIfNeeded(keyRing); err != nil {
return nil, err
}
Expand Down Expand Up @@ -221,7 +224,7 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo
// Note that it will have the same purpose, protection level and
// algorithm than as previous one.
req := &kmspb.CreateCryptoKeyVersionRequest{
Parent: req.Name,
Parent: resource,
CryptoKeyVersion: &kmspb.CryptoKeyVersion{
State: kmspb.CryptoKeyVersion_ENABLED,
},
Expand All @@ -235,6 +238,9 @@ func (k *CloudKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespo
crytoKeyName = response.Name + "/cryptoKeyVersions/1"
}

// Use uri format for the keys
crytoKeyName = uri.NewOpaque(Scheme, crytoKeyName).String()

// Sleep deterministically to avoid retries because of PENDING_GENERATING.
// One second is often enough.
if protectionLevel == kmspb.ProtectionLevel_HSM {
Expand Down Expand Up @@ -290,7 +296,7 @@ func (k *CloudKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKe
return nil, errors.New("createKeyRequest 'name' cannot be empty")
}

response, err := k.getPublicKeyWithRetries(req.Name, pendingGenerationRetries)
response, err := k.getPublicKeyWithRetries(resourceName(req.Name), pendingGenerationRetries)
if err != nil {
return nil, errors.Wrap(err, "cloudKMS GetPublicKey failed")
}
Expand Down Expand Up @@ -357,3 +363,19 @@ func parent(name string) (string, string) {
return name[:i], name[i+1:]
}
}

// resourceName returns the resource name in the given string. The resource name
// can be the same string, the value of the resource field or the encoded opaque
// data:
// - projects/id/locations/global/keyRings/ring/cryptoKeys/root-key/cryptoKeyVersions/1
// - cloudkms:resource=projects/id/locations/global/keyRings/ring/cryptoKeys/root-key/cryptoKeyVersions/1
// - cloudkms:projects/id/locations/global/keyRings/ring/cryptoKeys/root-key/cryptoKeyVersions/1
func resourceName(name string) string {
if u, err := uri.ParseWithScheme(Scheme, name); err == nil {
if r := u.Get("resource"); r != "" {
return r
}
return u.Opaque
}
return name
}
51 changes: 47 additions & 4 deletions kms/cloudkms/cloudkms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"crypto"
"fmt"
"net/url"
"os"
"reflect"
"testing"

"cloud.google.com/go/kms/apiv1/kmspb"
gax "github.com/googleapis/gax-go/v2"
"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/uri"
"go.step.sm/crypto/pemutil"
"google.golang.org/api/option"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -73,6 +75,7 @@ func TestNew(t *testing.T) {
}{
{"ok", args{context.Background(), apiv1.Options{}}, &CloudKMS{client: &MockClient{}}, false},
{"ok with uri", args{context.Background(), apiv1.Options{URI: "cloudkms:"}}, &CloudKMS{client: &MockClient{}}, false},
{"ok resource uri", args{context.Background(), apiv1.Options{URI: "cloudkms:projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"}}, &CloudKMS{client: &MockClient{}}, false},
{"fail credentials", args{context.Background(), apiv1.Options{CredentialsFile: "testdata/missing"}}, nil, true},
{"fail with uri", args{context.Background(), apiv1.Options{URI: "cloudkms:credentials-file=testdata/missing"}}, nil, true},
{"fail schema", args{context.Background(), apiv1.Options{URI: "pkcs11:"}}, nil, true},
Expand Down Expand Up @@ -165,6 +168,8 @@ func TestCloudKMS_Close(t *testing.T) {

func TestCloudKMS_CreateSigner(t *testing.T) {
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
keyURI := uri.NewOpaque(Scheme, keyName).String()

pemBytes, err := os.ReadFile("testdata/pub.pem")
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -192,6 +197,11 @@ func TestCloudKMS_CreateSigner(t *testing.T) {
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
},
}}, args{&apiv1.CreateSignerRequest{SigningKey: keyName}}, &Signer{client: &MockClient{}, signingKey: keyName, publicKey: pk}, false},
{"ok with uri", fields{&MockClient{
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
},
}}, args{&apiv1.CreateSignerRequest{SigningKey: keyURI}}, &Signer{client: &MockClient{}, signingKey: keyName, publicKey: pk}, false},
{"fail", fields{&MockClient{
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
return nil, fmt.Errorf("test error")
Expand Down Expand Up @@ -220,6 +230,7 @@ func TestCloudKMS_CreateSigner(t *testing.T) {

func TestCloudKMS_CreateKey(t *testing.T) {
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c"
keyURI := uri.NewOpaque(Scheme, keyName).String()
testError := fmt.Errorf("an error")
alreadyExists := status.Error(codes.AlreadyExists, "already exists")

Expand Down Expand Up @@ -259,7 +270,21 @@ func TestCloudKMS_CreateKey(t *testing.T) {
},
}},
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},
&apiv1.CreateKeyResponse{Name: "cloudkms:" + keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: "cloudkms:" + keyName + "/cryptoKeyVersions/1"}}, false},
{"ok with uri", 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: keyURI, ProtectionLevel: apiv1.HSM, SignatureAlgorithm: apiv1.ECDSAWithSHA256}},
&apiv1.CreateKeyResponse{Name: "cloudkms:" + keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: "cloudkms:" + keyName + "/cryptoKeyVersions/1"}}, false},
{"ok new key ring", fields{
&MockClient{
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
Expand All @@ -276,7 +301,7 @@ func TestCloudKMS_CreateKey(t *testing.T) {
},
}},
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},
&apiv1.CreateKeyResponse{Name: "cloudkms:" + keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: "cloudkms:" + keyName + "/cryptoKeyVersions/1"}}, false},
{"ok new key version", fields{
&MockClient{
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
Expand All @@ -293,7 +318,7 @@ func TestCloudKMS_CreateKey(t *testing.T) {
},
}},
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},
&apiv1.CreateKeyResponse{Name: "cloudkms:" + keyName + "/cryptoKeyVersions/2", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: "cloudkms:" + keyName + "/cryptoKeyVersions/2"}}, false},
{"ok with retries", fields{
&MockClient{
getKeyRing: func(_ context.Context, _ *kmspb.GetKeyRingRequest, _ ...gax.CallOption) (*kmspb.KeyRing, error) {
Expand All @@ -311,7 +336,7 @@ func TestCloudKMS_CreateKey(t *testing.T) {
},
}},
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},
&apiv1.CreateKeyResponse{Name: "cloudkms:" + keyName + "/cryptoKeyVersions/1", PublicKey: pk, CreateSignerRequest: apiv1.CreateSignerRequest{SigningKey: "cloudkms:" + keyName + "/cryptoKeyVersions/1"}}, 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},
Expand Down Expand Up @@ -387,6 +412,10 @@ func TestCloudKMS_CreateKey(t *testing.T) {

func TestCloudKMS_GetPublicKey(t *testing.T) {
keyName := "projects/p/locations/l/keyRings/k/cryptoKeys/c/cryptoKeyVersions/1"
keyURI := uri.NewOpaque(Scheme, keyName).String()
keyResource := uri.New(Scheme, url.Values{
"resource": []string{keyName},
}).String()
testError := fmt.Errorf("an error")

pemBytes, err := os.ReadFile("testdata/pub.pem")
Expand Down Expand Up @@ -419,6 +448,20 @@ func TestCloudKMS_GetPublicKey(t *testing.T) {
},
}},
args{&apiv1.GetPublicKeyRequest{Name: keyName}}, pk, false},
{"ok with uri", fields{
&MockClient{
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
},
}},
args{&apiv1.GetPublicKeyRequest{Name: keyURI}}, pk, false},
{"ok with resource uri", fields{
&MockClient{
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
return &kmspb.PublicKey{Pem: string(pemBytes)}, nil
},
}},
args{&apiv1.GetPublicKeyRequest{Name: keyResource}}, pk, false},
{"ok with retries", fields{
&MockClient{
getPublicKey: func(_ context.Context, _ *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) {
Expand Down
4 changes: 2 additions & 2 deletions kms/pkcs11/pkcs11.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import (
"go.step.sm/crypto/kms/uri"
)

// Scheme is the scheme used in uris.
const Scheme = "pkcs11"
// Scheme is the scheme used in uris, the string "pkcs11".
const Scheme = string(apiv1.PKCS11)

// DefaultRSASize is the number of bits of a new RSA key if no size has been
// specified.
Expand Down
27 changes: 22 additions & 5 deletions kms/softkms/softkms.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import (
"github.com/pkg/errors"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/uri"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x25519"
)

type algorithmAttributes struct {
Type string
Curve string
}

// Scheme is the scheme used in uris, the string "softkms".
const Scheme = string(apiv1.SoftKMS)

// DefaultRSAKeySize is the default size for RSA keys.
const DefaultRSAKeySize = 3072

Expand Down Expand Up @@ -84,7 +89,7 @@ func (k *SoftKMS) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, e
}
return sig, nil
case req.SigningKey != "":
v, err := pemutil.Read(req.SigningKey, opts...)
v, err := pemutil.Read(filename(req.SigningKey), opts...)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -116,7 +121,7 @@ func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon
}

return &apiv1.CreateKeyResponse{
Name: req.Name,
Name: filename(req.Name),
PublicKey: pub,
PrivateKey: priv,
CreateSignerRequest: apiv1.CreateSignerRequest{
Expand All @@ -127,16 +132,18 @@ func (k *SoftKMS) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespon

// GetPublicKey returns the public key from the file passed in the request name.
func (k *SoftKMS) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) {
v, err := pemutil.Read(req.Name)
v, err := pemutil.Read(filename(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:
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey, x25519.PublicKey:
return vv, nil
case crypto.Signer:
return vv.Public(), nil
default:
return nil, errors.Errorf("unsupported public key type %T", v)
}
Expand All @@ -163,7 +170,7 @@ func (k *SoftKMS) CreateDecrypter(req *apiv1.CreateDecrypterRequest) (crypto.Dec
}
return decrypter, nil
case req.DecryptionKey != "":
v, err := pemutil.Read(req.DecryptionKey, opts...)
v, err := pemutil.Read(filename(req.DecryptionKey), opts...)
if err != nil {
return nil, err
}
Expand All @@ -176,3 +183,13 @@ func (k *SoftKMS) CreateDecrypter(req *apiv1.CreateDecrypterRequest) (crypto.Dec
return nil, errors.New("failed to load softKMS: please define decryptionKeyPEM or decryptionKey")
}
}

func filename(s string) string {
if u, err := uri.ParseWithScheme(Scheme, s); err == nil {
if f := u.Get("path"); f != "" {
return f
}
return u.Opaque
}
return s
}
Loading

0 comments on commit 80680d7

Please sign in to comment.