Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for softkms and cloudkms uris on requests #240

Merged
merged 7 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought I had: when other people use our kms package, they may want to have this same logic available to them when handling certain requests/responses. We could expose these helpers in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason for this method is to support backward compatibility. The CreateKey method now returns the URI that you should use, in this case, cloudkms:projects/id/...

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