From a73172dd48d1cdada1428073df4d026f75f0be34 Mon Sep 17 00:00:00 2001 From: "sudesh.shetty" Date: Thu, 29 Apr 2021 19:48:23 -0400 Subject: [PATCH] feat: vc wallet command controller for managin profiles - added create & update profile command controllers - Part of #2770 Signed-off-by: sudesh.shetty --- pkg/controller/command/vcwallet/command.go | 152 +++++++++ .../command/vcwallet/command_test.go | 316 ++++++++++++++++++ pkg/controller/command/vcwallet/models.go | 43 +++ 3 files changed, 511 insertions(+) create mode 100644 pkg/controller/command/vcwallet/command.go create mode 100644 pkg/controller/command/vcwallet/command_test.go create mode 100644 pkg/controller/command/vcwallet/models.go diff --git a/pkg/controller/command/vcwallet/command.go b/pkg/controller/command/vcwallet/command.go new file mode 100644 index 0000000000..475fdbb677 --- /dev/null +++ b/pkg/controller/command/vcwallet/command.go @@ -0,0 +1,152 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package vcwallet + +import ( + "encoding/json" + "io" + + "github.com/piprate/json-gold/ld" + + "github.com/hyperledger/aries-framework-go/pkg/common/log" + "github.com/hyperledger/aries-framework-go/pkg/controller/command" + "github.com/hyperledger/aries-framework-go/pkg/controller/internal/cmdutil" + "github.com/hyperledger/aries-framework-go/pkg/crypto" + "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" + "github.com/hyperledger/aries-framework-go/pkg/internal/logutil" + "github.com/hyperledger/aries-framework-go/pkg/wallet" + "github.com/hyperledger/aries-framework-go/spi/storage" +) + +var logger = log.New("aries-framework/command/vcwallet") + +// Error codes. +const ( + // InvalidRequestErrorCode is typically a code for invalid requests. + InvalidRequestErrorCode = command.Code(iota + command.VCWallet) + + // CreateProfileErrorCode for errors during create wallet profile operations. + CreateProfileErrorCode + + // UpdateProfileErrorCode for errors during update wallet profile operations. + UpdateProfileErrorCode +) + +// All command operations. +const ( + CommandName = "vcwallet" + + // command methods. + CreateProfileMethod = "CreateProfile" + UpdateProfileMethod = "UpdateProfile" +) + +// miscellaneous constants for the vc wallet command controller. +const () + +// provider contains dependencies for the verifiable credential wallet command controller +// and is typically created by using aries.Context(). +type provider interface { + StorageProvider() storage.Provider + VDRegistry() vdr.Registry + Crypto() crypto.Crypto + JSONLDDocumentLoader() ld.DocumentLoader +} + +// Command contains operations provided by verifiable credential wallet controller. +type Command struct { + ctx provider +} + +// New returns new verifiable credential wallet controller command instance. +func New(p provider) *Command { + return &Command{ctx: p} +} + +// GetHandlers returns list of all commands supported by this controller command. +func (o *Command) GetHandlers() []command.Handler { + return []command.Handler{ + cmdutil.NewCommandHandler(CommandName, CreateProfileMethod, o.CreateProfile), + cmdutil.NewCommandHandler(CommandName, UpdateProfileMethod, o.UpdateProfile), + } +} + +// CreateProfile creates new wallet profile for given user. +func (o *Command) CreateProfile(rw io.Writer, req io.Reader) command.Error { + request := &CreateOrUpdateProfileRequest{} + + err := json.NewDecoder(req).Decode(request) + if err != nil { + logutil.LogInfo(logger, CommandName, CreateProfileMethod, err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, err) + } + + // create profile. + err = wallet.CreateProfile(request.UserID, o.ctx, prepareProfileOptions(request)...) + if err != nil { + logutil.LogInfo(logger, CommandName, CreateProfileMethod, err.Error()) + + return command.NewExecuteError(CreateProfileErrorCode, err) + } + + // create EDV keys if profile is using local kms. + if request.LocalKMSPassphrase != "" && request.EDVConfiguration != nil { + err = wallet.CreateDataVaultKeyPairs(request.UserID, o.ctx, wallet.WithUnlockByPassphrase(request.LocalKMSPassphrase)) + if err != nil { + logutil.LogInfo(logger, CommandName, CreateProfileMethod, err.Error()) + + return command.NewExecuteError(CreateProfileErrorCode, err) + } + } + + return nil +} + +// UpdateProfile updates an existing wallet profile for given user. +func (o *Command) UpdateProfile(rw io.Writer, req io.Reader) command.Error { + request := &CreateOrUpdateProfileRequest{} + + err := json.NewDecoder(req).Decode(&request) + if err != nil { + logutil.LogInfo(logger, CommandName, UpdateProfileMethod, err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, err) + } + + // update profile. + err = wallet.UpdateProfile(request.UserID, o.ctx, prepareProfileOptions(request)...) + if err != nil { + logutil.LogInfo(logger, CommandName, UpdateProfileMethod, err.Error()) + + return command.NewExecuteError(UpdateProfileErrorCode, err) + } + + return nil +} + +// prepareProfileOptions prepares options for creating wallet profile. +func prepareProfileOptions(rqst *CreateOrUpdateProfileRequest) []wallet.ProfileOptions { + var options []wallet.ProfileOptions + + if rqst.LocalKMSPassphrase != "" { + options = append(options, wallet.WithPassphrase(rqst.LocalKMSPassphrase)) + } + + if rqst.KeyStoreURL != "" { + options = append(options, wallet.WithKeyServerURL(rqst.KeyStoreURL)) + } + + if rqst.EDVConfiguration != nil { + options = append(options, wallet.WithEDVStorage( + rqst.EDVConfiguration.ServerURL, rqst.EDVConfiguration.VaultID, + rqst.EDVConfiguration.EncryptionKeyID, rqst.EDVConfiguration.MACKeyID, + )) + } + + return options +} diff --git a/pkg/controller/command/vcwallet/command_test.go b/pkg/controller/command/vcwallet/command_test.go new file mode 100644 index 0000000000..c249236f65 --- /dev/null +++ b/pkg/controller/command/vcwallet/command_test.go @@ -0,0 +1,316 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package vcwallet + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/controller/command" + "github.com/hyperledger/aries-framework-go/pkg/internal/jsonldtest" + mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" + mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" + "github.com/hyperledger/aries-framework-go/pkg/wallet" +) + +const ( + sampleUserID = "sample-user01" + samplePassPhrase = "fakepassphrase" + sampleKeyStoreURL = "sample/keyserver/test" + sampleEDVServerURL = "sample-edv-url" + sampleEDVVaultID = "sample-edv-vault-id" + sampleEDVEncryptionKID = "sample-edv-encryption-kid" + sampleEDVMacKID = "sample-edv-mac-kid" + sampleCommandError = "sample-command-error-01" +) + +func TestNew(t *testing.T) { + t.Run("successfully create new command instance", func(t *testing.T) { + cmd := New(newMockProvider(t)) + require.NotNil(t, cmd) + + handlers := cmd.GetHandlers() + require.Equal(t, 2, len(handlers)) + }) +} + +func TestCommand_CreateProfile(t *testing.T) { + t.Run("successfully create a new wallet profile (localkms)", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + LocalKMSPassphrase: samplePassPhrase, + } + + var b bytes.Buffer + cmdErr := cmd.CreateProfile(&b, getReader(t, &request)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err := wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + }) + + t.Run("successfully create a new wallet profile (webkms/remotekms)", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + KeyStoreURL: sampleKeyStoreURL, + } + + var b bytes.Buffer + cmdErr := cmd.CreateProfile(&b, getReader(t, &request)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err := wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + }) + + t.Run("successfully create a new wallet profile with EDV configuration", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + // create with remote kms. + request := &CreateOrUpdateProfileRequest{ + UserID: uuid.New().String(), + KeyStoreURL: sampleKeyStoreURL, + EDVConfiguration: &EDVConfiguration{ + ServerURL: sampleEDVServerURL, + VaultID: sampleEDVVaultID, + MACKeyID: sampleEDVMacKID, + EncryptionKeyID: sampleEDVEncryptionKID, + }, + } + + var b1 bytes.Buffer + cmdErr := cmd.CreateProfile(&b1, getReader(t, &request)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err := wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + + // create with local kms. + request = &CreateOrUpdateProfileRequest{ + UserID: uuid.New().String(), + LocalKMSPassphrase: samplePassPhrase, + EDVConfiguration: &EDVConfiguration{ + ServerURL: sampleEDVServerURL, + VaultID: sampleEDVVaultID, + }, + } + + var b2 bytes.Buffer + cmdErr = cmd.CreateProfile(&b2, getReader(t, &request)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err = wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + }) + + t.Run("failed to create duplicate profile", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + LocalKMSPassphrase: samplePassPhrase, + } + + var b1 bytes.Buffer + cmdErr := cmd.CreateProfile(&b1, getReader(t, &request)) + require.NoError(t, cmdErr) + + request = &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + KeyStoreURL: sampleKeyStoreURL, + } + + var b2 bytes.Buffer + cmdErr = cmd.CreateProfile(&b2, getReader(t, &request)) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Type(), command.ExecuteError) + require.Equal(t, cmdErr.Code(), CreateProfileErrorCode) + }) + + t.Run("failed to create profile due to invalid settings", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + } + + var b1 bytes.Buffer + cmdErr := cmd.CreateProfile(&b1, getReader(t, &request)) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Code(), CreateProfileErrorCode) + require.Equal(t, cmdErr.Type(), command.ExecuteError) + }) + + t.Run("failed to create profile due to invalid request", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + var b1 bytes.Buffer + cmdErr := cmd.CreateProfile(&b1, bytes.NewBufferString("--")) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Code(), InvalidRequestErrorCode) + require.Equal(t, cmdErr.Type(), command.ValidationError) + }) + + t.Run("failed to create profile due to EDV key set creation failure", func(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + mockStProv, ok := mockctx.StorageProviderValue.(*mockstorage.MockStoreProvider) + require.True(t, ok) + require.NotEmpty(t, mockStProv) + + mockStProv.Store.ErrGet = errors.New(sampleCommandError) + + request := &CreateOrUpdateProfileRequest{ + UserID: uuid.New().String(), + LocalKMSPassphrase: samplePassPhrase, + EDVConfiguration: &EDVConfiguration{ + ServerURL: sampleEDVServerURL, + VaultID: sampleEDVVaultID, + }, + } + + var b1 bytes.Buffer + cmdErr := cmd.CreateProfile(&b1, getReader(t, &request)) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Code(), CreateProfileErrorCode) + require.Equal(t, cmdErr.Type(), command.ExecuteError) + require.Contains(t, cmdErr.Error(), sampleCommandError) + }) +} + +func TestCommand_UpdateProfile(t *testing.T) { + mockctx := newMockProvider(t) + + cmd := New(mockctx) + require.NotNil(t, cmd) + + createRqst := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + LocalKMSPassphrase: samplePassPhrase, + } + + var c bytes.Buffer + cmdErr := cmd.CreateProfile(&c, getReader(t, &createRqst)) + require.NoError(t, cmdErr) + + t.Run("successfully update a wallet profile", func(t *testing.T) { + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + KeyStoreURL: sampleKeyStoreURL, + } + + var b bytes.Buffer + cmdErr := cmd.UpdateProfile(&b, getReader(t, &createRqst)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err := wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + }) + + t.Run("successfully update a wallet profile with EDV configuration", func(t *testing.T) { + // create with remote kms. + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + KeyStoreURL: sampleKeyStoreURL, + EDVConfiguration: &EDVConfiguration{ + ServerURL: sampleEDVServerURL, + VaultID: sampleEDVVaultID, + MACKeyID: sampleEDVMacKID, + EncryptionKeyID: sampleEDVEncryptionKID, + }, + } + + var b1 bytes.Buffer + cmdErr := cmd.UpdateProfile(&b1, getReader(t, &request)) + require.NoError(t, cmdErr) + + // if wallet instance can be creates it means profile exists + walletInstance, err := wallet.New(request.UserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, walletInstance) + }) + + t.Run("failed to update profile due to invalid settings", func(t *testing.T) { + request := &CreateOrUpdateProfileRequest{ + UserID: sampleUserID, + } + + var b1 bytes.Buffer + cmdErr := cmd.UpdateProfile(&b1, getReader(t, &request)) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Code(), UpdateProfileErrorCode) + require.Equal(t, cmdErr.Type(), command.ExecuteError) + }) + + t.Run("failed to update profile due to invalid request", func(t *testing.T) { + var b1 bytes.Buffer + cmdErr := cmd.UpdateProfile(&b1, bytes.NewBufferString("--")) + require.Error(t, cmdErr) + require.Equal(t, cmdErr.Code(), InvalidRequestErrorCode) + require.Equal(t, cmdErr.Type(), command.ValidationError) + }) +} + +func getReader(t *testing.T, v interface{}) io.Reader { + vcReqBytes, err := json.Marshal(v) + require.NoError(t, err) + + return bytes.NewBuffer(vcReqBytes) +} + +func newMockProvider(t *testing.T) *mockprovider.Provider { + t.Helper() + + loader, err := jsonldtest.DocumentLoader() + require.NoError(t, err) + + return &mockprovider.Provider{ + StorageProviderValue: mockstorage.NewMockStoreProvider(), + JSONLDDocumentLoaderValue: loader, + } +} diff --git a/pkg/controller/command/vcwallet/models.go b/pkg/controller/command/vcwallet/models.go new file mode 100644 index 0000000000..bdaecae057 --- /dev/null +++ b/pkg/controller/command/vcwallet/models.go @@ -0,0 +1,43 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package vcwallet + +// CreateOrUpdateProfileRequest is request model for +// creating a new wallet profile or updating an existing wallet profile. +type CreateOrUpdateProfileRequest struct { + // Unique identifier to identify wallet user + UserID string `json:"userID"` + + // passphrase for local kms for key operations. + // Optional, if this option is provided then wallet for this profile will use local KMS for key operations. + LocalKMSPassphrase string `json:"localKMSPassphrase,omitempty"` + + // passphrase for web/remote kms for key operations. + // Optional, if this option is provided then wallet for this profile will use web/remote KMS for key operations. + KeyStoreURL string `json:"keyStoreURL,omitempty"` + + // edv configuration for storing wallet contents for this profile + // Optional, if not provided then agent storage provider will be used as store provider. + EDVConfiguration *EDVConfiguration `json:"edvConfiguration,omitempty"` +} + +// EDVConfiguration contains configuration for EDV settings for profile creation. +type EDVConfiguration struct { + // EDV server URL for storing wallet contents. + ServerURL string `json:"serverURL,omitempty"` + + // EDV vault ID for storing the wallet contents. + VaultID string `json:"vaultID,omitempty"` + + // Encryption key ID of already existing key in wallet profile kms. + // If profile is using localkms then wallet will create this key set for wallet user. + EncryptionKeyID string `json:"encryptionKID,omitempty"` + + // MAC operation key ID of already existing key in wallet profile kms. + // If profile is using localkms then wallet will create this key set for wallet user. + MACKeyID string `json:"macKID,omitempty"` +}