diff --git a/README.md b/README.md index dd90ed88..55aab828 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ as they may have been used for past encryption operations. * * Azure KeyVault (uses envelopes) * * GCP CKMS (uses envelopes) * * Huawei Cloud KMS (uses envelopes) + * * IBM Key Protect (uses envelopes) * * OCI KMS (uses envelopes) * * Tencent Cloud KMS (uses envelopes) * * Vault Transit mount diff --git a/go.mod b/go.mod index 8808cb35..e9068df5 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 github.com/Azure/go-autorest/autorest/to v0.3.0 github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect + github.com/IBM/keyprotect-go-client v0.5.0 github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f github.com/aws/aws-sdk-go v1.30.27 github.com/golang/protobuf v1.4.2 diff --git a/go.sum b/go.sum index bb90dc60..7469e73b 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/IBM/keyprotect-go-client v0.5.0 h1:kTvP7BC723b8fVXnaNTUXOKBsYRechK24dNDxvPfCEU= +github.com/IBM/keyprotect-go-client v0.5.0/go.mod h1:5TwDM/4FRJq1ZOlwQL1xFahLWQ3TveR88VmL1u3njyI= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -197,6 +199,7 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= @@ -204,6 +207,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -324,6 +328,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -665,6 +670,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= diff --git a/wrapper.go b/wrapper.go index 64ee34c4..d60a6a25 100644 --- a/wrapper.go +++ b/wrapper.go @@ -14,6 +14,7 @@ const ( AzureKeyVault = "azurekeyvault" GCPCKMS = "gcpckms" HuaweiCloudKMS = "huaweicloudkms" + IBMKP = "ibmkp" MultiWrapper = "multiwrapper" OCIKMS = "ocikms" PKCS11 = "pkcs11" diff --git a/wrappers/ibmkp/ibmkp.go b/wrappers/ibmkp/ibmkp.go new file mode 100644 index 00000000..bddf876f --- /dev/null +++ b/wrappers/ibmkp/ibmkp.go @@ -0,0 +1,249 @@ +package ibmkp + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "os" + "sync/atomic" + + kp "github.com/IBM/keyprotect-go-client" + wrapping "github.com/hashicorp/go-kms-wrapping" +) + +// These constants contain the accepted env vars +const ( + EnvIBMApiKey = "IBMCLOUD_API_KEY" + EnvIBMKPEndpoint = "IBMCLOUD_KP_ENDPOINT" + EnvIBMKPInstanceID = "IBMCLOUD_KP_INSTANCE_ID" + EnvIBMKPKeyID = "IBMCLOUD_KP_KEY_ID" +) + +// Wrapper represents credentials and Key information for the KMS Key used to +// encryption and decryption +type Wrapper struct { + endpoint string + apiKey string + instanceID string + keyID string + + currentKeyID *atomic.Value + + client *kp.Client +} + +// Ensure that we are implementing Wrapper +var _ wrapping.Wrapper = (*Wrapper)(nil) + +// NewWrapper creates a new IBMKP wrapper with the provided options +func NewWrapper(opts *wrapping.WrapperOptions) *Wrapper { + if opts == nil { + opts = new(wrapping.WrapperOptions) + } + k := &Wrapper{ + currentKeyID: new(atomic.Value), + } + k.currentKeyID.Store("") + return k +} + +// SetConfig sets the fields on the Wrapper object based on +// values from the config parameter. +// +// Order of precedence IBM Key Protect values: +// * Environment variable +// * Value from Vault configuration file +func (k *Wrapper) SetConfig(config map[string]string) (map[string]string, error) { + if config == nil { + config = map[string]string{} + } + + // Check and set API Key + switch { + case os.Getenv(EnvIBMApiKey) != "": + k.apiKey = os.Getenv(EnvIBMApiKey) + case config["api_key"] != "": + k.apiKey = config["api_key"] + default: + return nil, fmt.Errorf("'api_key' was not found for IBM Key Protect wrapper configuration") + } + + // Check and set Endpoint + switch { + case os.Getenv(EnvIBMKPEndpoint) != "": + k.endpoint = os.Getenv(EnvIBMKPEndpoint) + case config["endpoint"] != "": + k.endpoint = config["endpoint"] + default: + k.endpoint = kp.DefaultBaseURL + } + + // Check and set instanceID + switch { + case os.Getenv(EnvIBMKPInstanceID) != "": + k.instanceID = os.Getenv(EnvIBMKPInstanceID) + case config["instance_id"] != "": + k.instanceID = config["instance_id"] + default: + return nil, fmt.Errorf("'instance_id' was not found for IBM Key Protect wrapper configuration") + } + + // Check and set keyID + switch { + case os.Getenv(EnvIBMKPKeyID) != "": + k.keyID = os.Getenv(EnvIBMKPKeyID) + case config["key_id"] != "": + k.keyID = config["key_id"] + default: + return nil, fmt.Errorf("'key_id' was not found for IBM Key Protect wrapper configuration") + } + + // Check and set k.client + if k.client == nil { + client, err := k.GetIBMKPClient() + if err != nil { + return nil, fmt.Errorf("error initializing IBM Key Protect wrapping client: %w", err) + } + + // Test the client connection using provided key ID + key, err := client.GetKeyMetadata(context.Background(), k.keyID) + if err != nil { + return nil, fmt.Errorf("error fetching IBM Key Protect wrapping key information: %w", err) + } + if key == nil || key.ID == "" { + return nil, errors.New("no key information returned") + } + k.currentKeyID.Store(key.ID) + + k.client = client + } + + // Map that holds non-sensitive configuration info + wrappingInfo := make(map[string]string) + wrappingInfo["endpoint"] = k.endpoint + wrappingInfo["instance_id"] = k.instanceID + wrappingInfo["key_id"] = k.keyID + + return wrappingInfo, nil +} + +// Init is called during core.Initialize. No-op at the moment. +func (k *Wrapper) Init(_ context.Context) error { + return nil +} + +// Finalize is called during shutdown. This is a no-op since +// Wrapper doesn't require any cleanup. +func (k *Wrapper) Finalize(_ context.Context) error { + return nil +} + +// Type returns the wrapping type for this particular Wrapper implementation +func (k *Wrapper) Type() string { + return wrapping.IBMKP +} + +// KeyID returns the last known key id +func (k *Wrapper) KeyID() string { + return k.currentKeyID.Load().(string) +} + +// HMACKeyID returns the last known HMAC key id +func (k *Wrapper) HMACKeyID() string { + return "" +} + +// Encrypt is used to encrypt the master key using the the AWS CMK. +// This returns the ciphertext, and/or any errors from this +// call. This should be called after the KMS client has been instantiated. +func (k *Wrapper) Encrypt(ctx context.Context, plaintext, aad []byte) (blob *wrapping.EncryptedBlobInfo, err error) { + if plaintext == nil { + return nil, fmt.Errorf("given plaintext for encryption is nil") + } + + env, err := wrapping.NewEnvelope(nil).Encrypt(plaintext, aad) + if err != nil { + return nil, fmt.Errorf("error wrapping data: %w", err) + } + + if k.client == nil { + return nil, fmt.Errorf("nil client") + } + + envelopKeyBase64 := []byte(base64.StdEncoding.EncodeToString(env.Key)) + ciphertext, err := k.client.Wrap(ctx, k.keyID, envelopKeyBase64, nil) + if err != nil { + return nil, fmt.Errorf("error encrypting data: %w", err) + } + + k.currentKeyID.Store(k.keyID) + + ret := &wrapping.EncryptedBlobInfo{ + Ciphertext: env.Ciphertext, + IV: env.IV, + KeyInfo: &wrapping.KeyInfo{ + KeyID: k.keyID, + WrappedKey: ciphertext, + }, + } + + return ret, nil +} + +// Decrypt is used to decrypt the ciphertext. This should be called after Init. +func (k *Wrapper) Decrypt(ctx context.Context, in *wrapping.EncryptedBlobInfo, aad []byte) (pt []byte, err error) { + if in == nil { + return nil, errors.New("given input for decryption is nil") + } + + if in.KeyInfo == nil { + return nil, errors.New("key info is nil") + } + + envelopKeyBase64, err := k.client.Unwrap(ctx, in.KeyInfo.KeyID, in.KeyInfo.WrappedKey, nil) + if err != nil { + return nil, err + } + + envelopKey, err := base64.StdEncoding.DecodeString(string(envelopKeyBase64)) + if err != nil { + return nil, err + } + + envInfo := &wrapping.EnvelopeInfo{ + Key: envelopKey, + IV: in.IV, + Ciphertext: in.Ciphertext, + } + + plaintext, err := wrapping.NewEnvelope(nil).Decrypt(envInfo, aad) + if err != nil { + return nil, fmt.Errorf("error decrypting data with envelope: %w", err) + } + + return plaintext, nil +} + +func (k *Wrapper) getConfigAPIKey() kp.ClientConfig { + return kp.ClientConfig{ + BaseURL: k.endpoint, + APIKey: k.apiKey, + TokenURL: kp.DefaultTokenURL, + InstanceID: k.instanceID, + Verbose: kp.VerboseFailOnly, + } +} + +// GetIBMKPClient returns an instance of the KMS client. +func (k *Wrapper) GetIBMKPClient() (*kp.Client, error) { + + options := k.getConfigAPIKey() + api, err := kp.New(options, kp.DefaultTransport()) + if err != nil { + return nil, err + } + + return api, nil + +} diff --git a/wrappers/ibmkp/ibmkp_acc_test.go b/wrappers/ibmkp/ibmkp_acc_test.go new file mode 100644 index 00000000..78197187 --- /dev/null +++ b/wrappers/ibmkp/ibmkp_acc_test.go @@ -0,0 +1,110 @@ +package ibmkp + +// These tests execute real calls. They require: +// 1. IBM Cloud account +// https://cloud.ibm.com/docs/overview?topic=overview-quickstart_lite +// +// 2. IBM Key Protect instance +// https://cloud.ibm.com/docs/key-protect?topic=key-protect-provision +// +// 3. IBM Key Protect's Root Key +// https://cloud.ibm.com/docs/key-protect?topic=key-protect-create-root-keys +// +// 4. IBM Cloud Service ID with an API Key +// https://cloud.ibm.com/docs/account?topic=account-serviceids +// +// 5. Grant 'Reader' access to Service ID (step 4) into Root Key (step 3) +// https://cloud.ibm.com/docs/key-protect?topic=key-protect-grant-access-keys +// +// No costs are involved to setup IBM Cloud environment because IBM Cloud account +// can be created for free and IBM Key Protect allows up to 20 keys for free. +// +// To run this test, the following env variables need to be set: +// - IBMCLOUD_API_KEY created on step 4 +// - IBMCLOUD_KP_INSTANCE_ID created on step 2 +// - IBMCLOUD_KP_KEY_ID created on step 3 + +import ( + "context" + "os" + "reflect" + "testing" +) + +const ( + TestIBMApiKey = "notARealApiKey" + TestIBMKPInstanceID = "a6493c3a-5b29-4ac3-9eaa-deadbeef3bfd" + TestIBMKPKeyID = "1234abcd-abcd-asdf-3dea-beefdeadabcd" +) + +func TestIBMKP_SetConfig(t *testing.T) { + + checkAndSetEnvVars(t) + + s := NewWrapper(nil) + instanceID := os.Getenv(EnvIBMKPInstanceID) + os.Unsetenv(EnvIBMKPInstanceID) + + // Attempt to set config, expect failure due to missing config + _, err := s.SetConfig(nil) + if err == nil { + t.Fatal("expected error when IBM Key Protect Key Vault config values are not provided") + } + + os.Setenv(EnvIBMKPInstanceID, instanceID) + + _, err = s.SetConfig(nil) + if err != nil { + t.Fatal(err) + } +} + +func TestIBMKP_Lifecycle(t *testing.T) { + + checkAndSetEnvVars(t) + + s := NewWrapper(nil) + _, err := s.SetConfig(nil) + if err != nil { + t.Fatalf("err: %s", err.Error()) + } + + // Test Encrypt and Decrypt calls + input := []byte("foo") + swi, err := s.Encrypt(context.Background(), input, nil) + if err != nil { + t.Fatalf("error encrypting: %s", err.Error()) + } + + pt, err := s.Decrypt(context.Background(), swi, nil) + if err != nil { + t.Fatalf("error decrypting: %s", err.Error()) + } + + if !reflect.DeepEqual(input, pt) { + t.Fatalf("expected %s, got %s", input, pt) + } +} + +// checkAndSetEnvVars check and sets the required env vars. It will skip tests that are +// not ran as acceptance tests since they require calling to external APIs. +func checkAndSetEnvVars(t *testing.T) { + t.Helper() + + // Skip tests if we are not running acceptance tests + if os.Getenv("VAULT_ACC") == "" { + t.SkipNow() + } + + if os.Getenv(EnvIBMApiKey) == "" { + os.Setenv(EnvIBMApiKey, TestIBMApiKey) + } + + if os.Getenv(EnvIBMKPInstanceID) == "" { + os.Setenv(EnvIBMKPInstanceID, TestIBMKPInstanceID) + } + + if os.Getenv(EnvIBMKPKeyID) == "" { + os.Setenv(EnvIBMKPKeyID, TestIBMKPKeyID) + } +}