Skip to content
Open
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/googleapis/gax-go/v2 v2.22.0
github.com/peterbourgon/diskv/v3 v3.0.1
github.com/pkg/errors v0.9.1
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36
github.com/schollz/jsonstore v1.1.0
github.com/smallstep/go-attestation v0.4.4-0.20260603212853-e1a87a0b07d9
github.com/stretchr/testify v1.11.1
Expand Down Expand Up @@ -102,6 +103,7 @@ require (
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
Expand Down Expand Up @@ -1456,6 +1458,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
4 changes: 3 additions & 1 deletion kms/apiv1/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ const (
// PlatformKMS is the KMS implementation that uses TPMKMS on Windows and
// Linux and MacKMS on macOS.
PlatformKMS Type = "kms"
// ScalewayKMS is a KMS implementation using Scaleway Key Manager.
ScalewayKMS Type = "scwkms"
)

// TypeOf returns the type of of the given uri.
Expand All @@ -213,7 +215,7 @@ func (t Type) Validate() error {
switch typ {
case DefaultKMS, SoftKMS: // Go crypto based kms.
return nil
case CloudKMS, AmazonKMS, AzureKMS: // Cloud based kms.
case CloudKMS, AmazonKMS, AzureKMS, ScalewayKMS: // Cloud based kms.
return nil
case YubiKey, PKCS11, TPMKMS: // Hardware based kms.
return nil
Expand Down
2 changes: 2 additions & 0 deletions kms/kms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/awskms"
"go.step.sm/crypto/kms/cloudkms"
"go.step.sm/crypto/kms/scwkms"
"go.step.sm/crypto/kms/softkms"
)

Expand Down Expand Up @@ -40,6 +41,7 @@ func TestNew(t *testing.T) {
{"uri", false, args{ctx, apiv1.Options{URI: "softkms:foo=bar"}}, &softkms.SoftKMS{}, false},
{"awskms", false, args{ctx, apiv1.Options{Type: "awskms"}}, &awskms.KMS{}, false},
{"cloudkms", true, args{ctx, apiv1.Options{Type: "cloudkms"}}, &cloudkms.CloudKMS{}, failCloudKMS},
{"scwkms", false, args{ctx, apiv1.Options{Type: "scwkms"}}, &scwkms.ScalewayKMS{}, false},
{"fail validation", false, args{ctx, apiv1.Options{Type: "foobar"}}, nil, true},
}
for _, tt := range tests {
Expand Down
124 changes: 124 additions & 0 deletions kms/scwkms/decrypter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//go:build !noscwkms

package scwkms

import (
"crypto"
"crypto/rsa"
"errors"
"fmt"
"io"

km "github.com/scaleway/scaleway-sdk-go/api/key_manager/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/scw"

"go.step.sm/crypto/kms/apiv1"
)

// CreateDecrypter implements the apiv1.Decrypter interface and returns a
// crypto.Decrypter backed by a Scaleway asymmetric encryption key.
//
// Scaleway only supports RSA-OAEP-SHA256 for asymmetric decryption.
func (k *ScalewayKMS) CreateDecrypter(req *apiv1.CreateDecrypterRequest) (crypto.Decrypter, error) {
if req.DecryptionKey == "" {
return nil, errors.New("scwkms CreateDecrypter: 'decryptionKey' cannot be empty")
}
return NewDecrypter(k.client, req.DecryptionKey)
}

// Decrypter implements crypto.Decrypter using Scaleway Key Manager.
type Decrypter struct {
client KeyManagementClient
keyID string
region scw.Region
publicKey crypto.PublicKey
}

// NewDecrypter creates a new crypto.Decrypter backed by the given Scaleway key.
func NewDecrypter(client KeyManagementClient, decryptionKey string) (*Decrypter, error) {
keyID, region := parseKeyName(decryptionKey, "")

decrypter := &Decrypter{
client: client,
keyID: keyID,
region: region,
}
if err := decrypter.preloadKey(); err != nil {
return nil, err
}

return decrypter, nil
}

func (d *Decrypter) preloadKey() error {
response, err := d.client.GetPublicKey(&km.GetPublicKeyRequest{
Region: d.region,
KeyID: d.keyID,
})
if err != nil {
return fmt.Errorf("scwkms GetPublicKey failed: %w", err)
}

d.publicKey, err = parsePublicKeyPEM([]byte(response.Pem))
return err
}

// Public returns the public key of this decrypter.
func (d *Decrypter) Public() crypto.PublicKey {
return d.publicKey
}

// validateOAEPOptions validates the RSA OAEP options provided.
// Scaleway only supports RSA-OAEP-SHA256; labels are not supported.
func validateOAEPOptions(o *rsa.OAEPOptions) error {
if o == nil {
return nil
}
if len(o.Label) > 0 {
return errors.New("scwkms does not support RSA-OAEP label")
}
switch o.Hash {
case crypto.Hash(0), crypto.SHA256:
return nil
default:
return fmt.Errorf("scwkms does not support hash algorithm %q with RSA-OAEP (only SHA-256 is supported)", o.Hash)
}
}

// Decrypt decrypts the given ciphertext using the Scaleway Key Manager
// asymmetric decryption API.
//
// Only RSA-OAEP-SHA256 is supported by Scaleway. Labels are not supported.
// The opts argument must be nil, a *rsa.OAEPOptions (with Hash 0 or SHA-256
// and no Label), or will produce an error.
func (d *Decrypter) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) ([]byte, error) {
if opts == nil {
opts = &rsa.OAEPOptions{}
}
switch o := opts.(type) {
case *rsa.OAEPOptions:
if err := validateOAEPOptions(o); err != nil {
return nil, err
}
case *rsa.PKCS1v15DecryptOptions:
return nil, errors.New("scwkms does not support PKCS#1 v1.5 decryption")
default:
return nil, errors.New("scwkms Decrypt: invalid options type")
}

response, err := d.client.Decrypt(&km.DecryptRequest{
Region: d.region,
KeyID: d.keyID,
Ciphertext: ciphertext,
// AssociatedData is nil: it is only used for symmetric AES-GCM keys,
// not for RSA-OAEP asymmetric decryption.
})
if err != nil {
return nil, fmt.Errorf("scwkms Decrypt failed: %w", err)
}

return response.Plaintext, nil
}

// Compile-time assertion.
var _ apiv1.Decrypter = (*ScalewayKMS)(nil)
Loading