From 81f07754a8d6aaa6d8f5c8984ea4d1b8fce9566e Mon Sep 17 00:00:00 2001 From: "sudesh.shetty" Date: Wed, 11 Aug 2021 11:20:00 -0400 Subject: [PATCH] feat: WACI holder APIs for vcwallet - Implemented Universal wallet interfaces discussed in https://github.com/w3c-ccg/universal-wallet-interop-spec/issues/99 - Added bindings for SDK, REST, JS - Closes #2928 - Part of #2433 Signed-off-by: sudesh.shetty --- cmd/aries-js-worker/src/agent-rest-client.js | 12 + cmd/aries-js-worker/src/aries.js | 32 + pkg/client/vcwallet/client.go | 77 +++ pkg/client/vcwallet/client_test.go | 232 ++++++- pkg/controller/command/vcwallet/command.go | 176 +++++- .../command/vcwallet/command_test.go | 364 ++++++++++- pkg/controller/command/vcwallet/models.go | 51 ++ pkg/controller/rest/vcwallet/models.go | 50 ++ pkg/controller/rest/vcwallet/operation.go | 88 ++- .../rest/vcwallet/operation_test.go | 275 +++++++- .../presentproof/mock_presentproof.go | 83 +++ pkg/wallet/options.go | 86 +++ pkg/wallet/wallet.go | 276 +++++++- pkg/wallet/wallet_test.go | 597 +++++++++++++++++- 14 files changed, 2347 insertions(+), 52 deletions(-) create mode 100644 pkg/mock/didcomm/protocol/presentproof/mock_presentproof.go diff --git a/cmd/aries-js-worker/src/agent-rest-client.js b/cmd/aries-js-worker/src/agent-rest-client.js index 1925ff624e..c63f4d4bc5 100644 --- a/cmd/aries-js-worker/src/agent-rest-client.js +++ b/cmd/aries-js-worker/src/agent-rest-client.js @@ -456,6 +456,18 @@ const pkgs = { path: "/vcwallet/create-key-pair", method: "POST", }, + Connect: { + path: "/vcwallet/connect", + method: "POST", + }, + ProposePresentation: { + path: "/vcwallet/propose-presentation", + method: "POST", + }, + PresentProof: { + path: "/vcwallet/present-proof", + method: "POST", + }, }, ld: { AddContexts: { diff --git a/cmd/aries-js-worker/src/aries.js b/cmd/aries-js-worker/src/aries.js index ae14758b11..c2df58bad6 100644 --- a/cmd/aries-js-worker/src/aries.js +++ b/cmd/aries-js-worker/src/aries.js @@ -1271,6 +1271,38 @@ const Aries = function (opts) { createKeyPair: async function (req) { return invoke(aw, pending, this.pkgname, "CreateKeyPair", req, "timeout while creating key pair from wallet") }, + + /** + * + * accepts out-of-band invitation and performs DID exchange from wallet. + * + * @returns {Promise} + */ + connect: async function (req) { + return invoke(aw, pending, this.pkgname, "Connect", req, "timeout while performing DID connect from wallet") + }, + + /** + * + * accepts out-of-band invitation and sends propose presentation message to sender. + * + * Returns request presentation message response. + * + * @returns {Promise} + */ + proposePresentation: async function (req) { + return invoke(aw, pending, this.pkgname, "ProposePresentation", req, "timeout while proposing presentation from wallet") + }, + + /** + * + * sends presentation as present proof message. + * + * @returns {Promise} + */ + presentProof: async function (req) { + return invoke(aw, pending, this.pkgname, "PresentProof", req, "timeout while performing present proof from wallet") + }, }, /** * JSON-LD management API. diff --git a/pkg/client/vcwallet/client.go b/pkg/client/vcwallet/client.go index d819221879..221518cf7b 100644 --- a/pkg/client/vcwallet/client.go +++ b/pkg/client/vcwallet/client.go @@ -13,8 +13,10 @@ import ( "github.com/piprate/json-gold/ld" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/crypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/kms" @@ -34,6 +36,17 @@ type provider interface { VDRegistry() vdr.Registry Crypto() crypto.Crypto JSONLDDocumentLoader() ld.DocumentLoader + didCommProvider // to be used only if wallet needs to be participated in DIDComm. +} + +// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. +// TODO: using wallet KMS instead of provider KMS. +// TODO: reconcile Protocol storage with wallet store. +type didCommProvider interface { + KMS() kms.KeyManager + ServiceEndpoint() string + ProtocolStateStorageProvider() storage.Provider + Service(id string) (interface{}, error) } // walletAuth is auth function which returns wallet unlock token. @@ -336,3 +349,67 @@ func (c *Client) CreateKeyPair(keyType kms.KeyType) (*wallet.KeyPair, error) { return c.wallet.CreateKeyPair(auth, keyType) } + +// Connect accepts out-of-band invitations and performs DID exchange. +// +// Args: +// - invitation: out-of-band invitation. +// - options: connection options. +// +// Returns: +// - connection ID if DID exchange is successful. +// - error if operation false. +// +func (c *Client) Connect(invitation *outofband.Invitation, options ...wallet.ConnectOptions) (string, error) { + auth, err := c.auth() + if err != nil { + return "", err + } + + return c.wallet.Connect(auth, invitation, options...) +} + +// ProposePresentation accepts out-of-band invitation and sends message proposing presentation +// from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - invitation: out-of-band invitation from relying party. +// - options: options for accepting invitation and send propose presentation message. +// +// Returns: +// - DIDCommMsgMap containing request presentation message if operation is successful. +// - error if operation fails. +// +func (c *Client) ProposePresentation(invitation *outofband.Invitation, options ...wallet.ProposePresentationOption) (*service.DIDCommMsgMap, error) { //nolint: lll + auth, err := c.auth() + if err != nil { + return nil, err + } + + return c.wallet.ProposePresentation(auth, invitation, options...) +} + +// PresentProof sends message present proof message from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - thID: thread ID (action ID) of request presentation. +// - presentation: presentation to be sent. +// +// Returns: +// - error if operation fails. +// +// TODO: wait for acknowledgement option to be added. +func (c *Client) PresentProof(thID string, presentation *verifiable.Presentation) error { + auth, err := c.auth() + if err != nil { + return err + } + + return c.wallet.PresentProof(auth, thID, presentation) +} diff --git a/pkg/client/vcwallet/client_test.go b/pkg/client/vcwallet/client_test.go index 95ecb1d70d..b5604a4b18 100644 --- a/pkg/client/vcwallet/client_test.go +++ b/pkg/client/vcwallet/client_test.go @@ -19,7 +19,12 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/presexch" "github.com/hyperledger/aries-framework-go/pkg/doc/util" @@ -28,11 +33,15 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" "github.com/hyperledger/aries-framework-go/pkg/kms" cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto" + mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" + mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" + mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" "github.com/hyperledger/aries-framework-go/pkg/mock/secretlock" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" "github.com/hyperledger/aries-framework-go/pkg/secretlock/local/masterlock/pbkdf2" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/pkg/wallet" "github.com/hyperledger/aries-framework-go/spi/storage" @@ -426,12 +435,9 @@ func TestCreateProfile(t *testing.T) { require.NoError(t, err) vcWallet, err := New(sampleUserID, mockctx) - require.NoError(t, err) - require.NotEmpty(t, vcWallet) - - err = vcWallet.Open(wallet.WithUnlockByAuthorizationToken(sampleRemoteKMSAuth)) require.Error(t, err) require.Contains(t, err.Error(), sampleClientErr) + require.Empty(t, vcWallet) }) } @@ -1531,15 +1537,229 @@ func TestClient_CreateKeyPair(t *testing.T) { }) } +func TestClient_Connect(t *testing.T) { + sampleUser := uuid.New().String() + mockctx := newMockProvider(t) + + err := CreateProfile(sampleUser, mockctx, wallet.WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test did connect success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + defer vcWallet.Close() + + connectionID, err := vcWallet.Connect(&outofband.Invitation{}) + require.NoError(t, err) + require.Equal(t, sampleConnID, connectionID) + }) + + t.Run("test did connect failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleClientErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + defer vcWallet.Close() + + connectionID, err := vcWallet.Connect(&outofband.Invitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleClientErr) + require.Empty(t, connectionID) + }) + + t.Run("test did connect failure - auth error", func(t *testing.T) { + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + connectionID, err := vcWallet.Connect(&outofband.Invitation{}) + require.True(t, errors.Is(err, ErrWalletLocked)) + require.Empty(t, connectionID) + }) +} + +func TestClient_ProposePresentation(t *testing.T) { + sampleUser := uuid.New().String() + mockctx := newMockProvider(t) + + err := CreateProfile(sampleUser, mockctx, wallet.WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test propose presentation success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + thID := uuid.New().String() + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentation{ + Comment: "mock msg", + }), + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + defer vcWallet.Close() + + msg, err := vcWallet.ProposePresentation(&outofband.Invitation{}) + require.NoError(t, err) + require.NotEmpty(t, msg) + }) + + t.Run("test propose presentation failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleClientErr) + }, + } + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + defer vcWallet.Close() + + msg, err := vcWallet.ProposePresentation(&outofband.Invitation{}) + require.Error(t, err) + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - auth error", func(t *testing.T) { + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + msg, err := vcWallet.ProposePresentation(&outofband.Invitation{}) + require.True(t, errors.Is(err, ErrWalletLocked)) + require.Empty(t, msg) + }) +} + +func TestClient_PresentProof(t *testing.T) { + sampleUser := uuid.New().String() + mockctx := newMockProvider(t) + + err := CreateProfile(sampleUser, mockctx, wallet.WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test present proof success", func(t *testing.T) { + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + defer vcWallet.Close() + + err = vcWallet.PresentProof(uuid.New().String(), &verifiable.Presentation{}) + require.NoError(t, err) + }) + + t.Run("test present proof failure - auth error", func(t *testing.T) { + vcWallet, err := New(sampleUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + err = vcWallet.PresentProof(uuid.New().String(), &verifiable.Presentation{}) + require.True(t, errors.Is(err, ErrWalletLocked)) + }) +} + func newMockProvider(t *testing.T) *mockprovider.Provider { t.Helper() loader, err := ldtestutil.DocumentLoader() require.NoError(t, err) + serviceMap := map[string]interface{}{ + presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, + outofbandSvc.Name: &mockoutofband.MockOobService{}, + } + return &mockprovider.Provider{ - StorageProviderValue: mockstorage.NewMockStoreProvider(), - DocumentLoaderValue: loader, + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + ServiceMap: serviceMap, } } diff --git a/pkg/controller/command/vcwallet/command.go b/pkg/controller/command/vcwallet/command.go index 8782a95979..93180b2fac 100644 --- a/pkg/controller/command/vcwallet/command.go +++ b/pkg/controller/command/vcwallet/command.go @@ -23,6 +23,7 @@ import ( "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/kms" "github.com/hyperledger/aries-framework-go/pkg/kms/webkms" "github.com/hyperledger/aries-framework-go/pkg/wallet" "github.com/hyperledger/aries-framework-go/spi/storage" @@ -79,6 +80,15 @@ const ( // ProfileExistsErrorCode for errors while checking if profile exists for a wallet user. ProfileExistsErrorCode + + // DIDConnectErrorCode for errors while performing DID connect in wallet. + DIDConnectErrorCode + + // ProposePresentationErrorCode for errors while proposing presentation. + ProposePresentationErrorCode + + // PresentProofErrorCode for errors while presenting proof from wallet. + PresentProofErrorCode ) // All command operations. @@ -86,28 +96,34 @@ const ( CommandName = "vcwallet" // command methods. - CreateProfileMethod = "CreateProfile" - UpdateProfileMethod = "UpdateProfile" - ProfileExistsMethod = "ProfileExists" - OpenMethod = "Open" - CloseMethod = "Close" - AddMethod = "Add" - RemoveMethod = "Remove" - GetMethod = "Get" - GetAllMethod = "GetAll" - QueryMethod = "Query" - IssueMethod = "Issue" - ProveMethod = "Prove" - VerifyMethod = "Verify" - DeriveMethod = "Derive" - CreateKeyPairMethod = "CreateKeyPair" + CreateProfileMethod = "CreateProfile" + UpdateProfileMethod = "UpdateProfile" + ProfileExistsMethod = "ProfileExists" + OpenMethod = "Open" + CloseMethod = "Close" + AddMethod = "Add" + RemoveMethod = "Remove" + GetMethod = "Get" + GetAllMethod = "GetAll" + QueryMethod = "Query" + IssueMethod = "Issue" + ProveMethod = "Prove" + VerifyMethod = "Verify" + DeriveMethod = "Derive" + CreateKeyPairMethod = "CreateKeyPair" + ConnectMethod = "Connect" + ProposePresentationMethod = "ProposePresentation" + PresentProofMethod = "PresentProof" ) // miscellaneous constants for the vc wallet command controller. const ( // log constants. - logSuccess = "success" - logUserIDKey = "userID" + logSuccess = "success" + logUserIDKey = "userID" + connectionIDString = "connectionID" + invitationIDString = "invitationID" + LabelString = "label" emptyRawLength = 4 @@ -153,6 +169,17 @@ type provider interface { VDRegistry() vdr.Registry Crypto() crypto.Crypto JSONLDDocumentLoader() ld.DocumentLoader + didCommProvider // to be used only if wallet needs to be participated in DIDComm. +} + +// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. +// TODO: using wallet KMS instead of provider KMS. +// TODO: reconcile Protocol storage with wallet store. +type didCommProvider interface { + KMS() kms.KeyManager + ServiceEndpoint() string + ProtocolStateStorageProvider() storage.Provider + Service(id string) (interface{}, error) } // Command contains operations provided by verifiable credential wallet controller. @@ -194,6 +221,9 @@ func (o *Command) GetHandlers() []command.Handler { cmdutil.NewCommandHandler(CommandName, VerifyMethod, o.Verify), cmdutil.NewCommandHandler(CommandName, DeriveMethod, o.Derive), cmdutil.NewCommandHandler(CommandName, CreateKeyPairMethod, o.CreateKeyPair), + cmdutil.NewCommandHandler(CommandName, ConnectMethod, o.Connect), + cmdutil.NewCommandHandler(CommandName, ProposePresentationMethod, o.ProposePresentation), + cmdutil.NewCommandHandler(CommandName, PresentProofMethod, o.PresentProof), } } @@ -685,6 +715,118 @@ func (o *Command) CreateKeyPair(rw io.Writer, req io.Reader) command.Error { return nil } +// Connect accepts out-of-band invitations and performs DID exchange. +func (o *Command) Connect(rw io.Writer, req io.Reader) command.Error { + request := &ConnectRequest{} + + err := json.NewDecoder(req).Decode(&request) + if err != nil { + logutil.LogInfo(logger, CommandName, ConnectMethod, err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, err) + } + + vcWallet, err := wallet.New(request.UserID, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, ConnectMethod, err.Error()) + + return command.NewExecuteError(DIDConnectErrorCode, err) + } + + connectionID, err := vcWallet.Connect(request.Auth, request.Invitation, + wallet.WithConnectTimeout(request.Timeout), wallet.WithReuseDID(request.ReuseConnection), + wallet.WithReuseAnyConnection(), wallet.WithMyLabel(request.MyLabel), + wallet.WithRouterConnections(request.RouterConnections)) + if err != nil { + logutil.LogInfo(logger, CommandName, ConnectMethod, err.Error()) + + return command.NewExecuteError(DIDConnectErrorCode, err) + } + + command.WriteNillableResponse(rw, &ConnectResponse{ConnectionID: connectionID}, logger) + + logutil.LogDebug(logger, CommandName, ConnectMethod, logSuccess, + logutil.CreateKeyValueString(logUserIDKey, request.UserID), + logutil.CreateKeyValueString(invitationIDString, request.Invitation.ID), + logutil.CreateKeyValueString(LabelString, request.MyLabel), + logutil.CreateKeyValueString(connectionIDString, connectionID)) + + return nil +} + +// ProposePresentation accepts out-of-band invitation and sends message proposing presentation +// from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +func (o *Command) ProposePresentation(rw io.Writer, req io.Reader) command.Error { + request := &ProposePresentationRequest{} + + err := json.NewDecoder(req).Decode(&request) + if err != nil { + logutil.LogInfo(logger, CommandName, ProposePresentationMethod, err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, err) + } + + vcWallet, err := wallet.New(request.UserID, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, ProposePresentationMethod, err.Error()) + + return command.NewExecuteError(ProposePresentationErrorCode, err) + } + + msg, err := vcWallet.ProposePresentation(request.Auth, request.Invitation, + wallet.WithFromDID(request.FromDID), wallet.WithPresentProofTimeout(request.Timeout)) + if err != nil { + logutil.LogInfo(logger, CommandName, ProposePresentationMethod, err.Error()) + + return command.NewExecuteError(ProposePresentationErrorCode, err) + } + + command.WriteNillableResponse(rw, &ProposePresentationResponse{PresentationRequest: msg}, logger) + + logutil.LogDebug(logger, CommandName, ProposePresentationMethod, logSuccess, + logutil.CreateKeyValueString(logUserIDKey, request.UserID)) + + return nil +} + +// PresentProof sends message present proof message from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +func (o *Command) PresentProof(rw io.Writer, req io.Reader) command.Error { + request := &PresentProofRequest{} + + err := json.NewDecoder(req).Decode(&request) + if err != nil { + logutil.LogInfo(logger, CommandName, PresentProofMethod, err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, err) + } + + vcWallet, err := wallet.New(request.UserID, o.ctx) + if err != nil { + logutil.LogInfo(logger, CommandName, PresentProofMethod, err.Error()) + + return command.NewExecuteError(PresentProofErrorCode, err) + } + + err = vcWallet.PresentProof(request.Auth, request.ThreadID, request.Presentation) + if err != nil { + logutil.LogInfo(logger, CommandName, PresentProofMethod, err.Error()) + + return command.NewExecuteError(PresentProofErrorCode, err) + } + + logutil.LogDebug(logger, CommandName, PresentProofMethod, logSuccess, + logutil.CreateKeyValueString(logUserIDKey, request.UserID)) + + return nil +} + // prepareProfileOptions prepares options for creating wallet profile. func prepareProfileOptions(rqst *CreateOrUpdateProfileRequest) []wallet.ProfileOptions { var options []wallet.ProfileOptions diff --git a/pkg/controller/command/vcwallet/command_test.go b/pkg/controller/command/vcwallet/command_test.go index 4012591086..48dea5da04 100644 --- a/pkg/controller/command/vcwallet/command_test.go +++ b/pkg/controller/command/vcwallet/command_test.go @@ -20,16 +20,26 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + outofbandClient "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/controller/command" + "github.com/hyperledger/aries-framework-go/pkg/controller/command/outofband" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" "github.com/hyperledger/aries-framework-go/pkg/kms" + mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" + mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" + mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/pkg/wallet" ) @@ -240,7 +250,7 @@ func TestNew(t *testing.T) { cmd := New(newMockProvider(t), &Config{}) require.NotNil(t, cmd) - require.Len(t, cmd.GetHandlers(), 15) + require.Len(t, cmd.GetHandlers(), 18) }) } @@ -1817,6 +1827,347 @@ func TestCommand_CreateKeyPair(t *testing.T) { }) } +func TestCommand_Connect(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user01" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("successfully perform DID connect", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + cmd := New(mockctx, &Config{}) + + request := &ConnectRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + var b bytes.Buffer + cmdErr := cmd.Connect(&b, getReader(t, &request)) + require.NoError(t, cmdErr) + + var response ConnectResponse + require.NoError(t, json.NewDecoder(&b).Decode(&response)) + require.NotEmpty(t, response) + require.Equal(t, sampleConnID, response.ConnectionID) + }) + + t.Run("did connect failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + cmd := New(mockctx, &Config{}) + + request := &ConnectRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + var b bytes.Buffer + cmdErr := cmd.Connect(&b, getReader(t, &request)) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ExecuteError, DIDConnectErrorCode, sampleCommandError) + validateError(t, cmdErr, command.ExecuteError, DIDConnectErrorCode, "failed to accept invitation") + require.Empty(t, b.Bytes()) + }) + + t.Run("did connect failure - invalid request", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + var b bytes.Buffer + cmdErr := cmd.Connect(&b, bytes.NewBufferString("--")) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ValidationError, InvalidRequestErrorCode, "invalid character") + require.Empty(t, b.Bytes()) + }) + + t.Run("attempt to didconnect with invalid profile", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + request := &ConnectRequest{ + WalletAuth: WalletAuth{UserID: sampleUserID, Auth: sampleFakeTkn}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + var b bytes.Buffer + cmdErr := cmd.Connect(&b, getReader(t, &request)) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ExecuteError, DIDConnectErrorCode, "failed to get VC wallet profile") + require.Empty(t, b.Bytes()) + }) +} + +func TestCommand_ProposePresentation(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user02" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("successfully send propose presentation", func(t *testing.T) { + sampleConnID := uuid.New().String() + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + thID := uuid.New().String() + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentation{ + Comment: "mock msg", + }), + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + cmd := New(mockctx, &Config{}) + + request := &ProposePresentationRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + Invitation: &outofbandClient.Invitation{}, + } + + var b bytes.Buffer + cmdErr := cmd.ProposePresentation(&b, getReader(t, &request)) + require.NoError(t, cmdErr) + + var response ProposePresentationResponse + require.NoError(t, json.NewDecoder(&b).Decode(&response)) + require.NotEmpty(t, response) + require.NotEmpty(t, response.PresentationRequest) + }) + + t.Run("failed to send propose presentation", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + cmd := New(mockctx, &Config{}) + + request := &ConnectRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + var b bytes.Buffer + cmdErr := cmd.ProposePresentation(&b, getReader(t, &request)) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ExecuteError, ProposePresentationErrorCode, sampleCommandError) + validateError(t, cmdErr, command.ExecuteError, ProposePresentationErrorCode, "failed to accept invitation") + require.Empty(t, b.Bytes()) + }) + + t.Run("failed to send propose presentation - invalid request", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + var b bytes.Buffer + cmdErr := cmd.ProposePresentation(&b, bytes.NewBufferString("--")) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ValidationError, InvalidRequestErrorCode, "invalid character") + require.Empty(t, b.Bytes()) + }) + + t.Run("failed to send propose presentation - invalid profile", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + request := &ConnectRequest{ + WalletAuth: WalletAuth{UserID: sampleUserID, Auth: sampleFakeTkn}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + var b bytes.Buffer + cmdErr := cmd.ProposePresentation(&b, getReader(t, &request)) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ExecuteError, ProposePresentationErrorCode, "failed to get VC wallet profile") + require.Empty(t, b.Bytes()) + }) +} + +func TestCommand_PresentProof(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user03" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("successfully present proof", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + request := &PresentProofRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + ThreadID: uuid.New().String(), + Presentation: &verifiable.Presentation{}, + } + + var b bytes.Buffer + cmdErr := cmd.PresentProof(&b, getReader(t, &request)) + require.NoError(t, cmdErr) + }) + + t.Run("failed to present proof", func(t *testing.T) { + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionContinueFunc: func(string, presentproofSvc.Opt) error { + return fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + cmd := New(mockctx, &Config{}) + + request := &PresentProofRequest{ + WalletAuth: WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + ThreadID: uuid.New().String(), + Presentation: &verifiable.Presentation{}, + } + + var b bytes.Buffer + cmdErr := cmd.PresentProof(&b, getReader(t, &request)) + validateError(t, cmdErr, command.ExecuteError, PresentProofErrorCode, sampleCommandError) + require.Empty(t, b.Bytes()) + }) + + t.Run("failed to present proof - invalid request", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + var b bytes.Buffer + cmdErr := cmd.PresentProof(&b, bytes.NewBufferString("--")) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ValidationError, InvalidRequestErrorCode, "invalid character") + require.Empty(t, b.Bytes()) + }) + + t.Run("failed to present proof - invalid profile", func(t *testing.T) { + cmd := New(mockctx, &Config{}) + + request := &PresentProofRequest{ + WalletAuth: WalletAuth{UserID: sampleUserID, Auth: token}, + ThreadID: uuid.New().String(), + Presentation: &verifiable.Presentation{}, + } + + var b bytes.Buffer + cmdErr := cmd.PresentProof(&b, getReader(t, &request)) + require.Error(t, cmdErr) + + validateError(t, cmdErr, command.ExecuteError, PresentProofErrorCode, "failed to get VC wallet profile") + require.Empty(t, b.Bytes()) + }) +} + func createSampleUserProfile(t *testing.T, ctx *mockprovider.Provider, request *CreateOrUpdateProfileRequest) { cmd := New(ctx, &Config{}) require.NotNil(t, cmd) @@ -1884,9 +2235,16 @@ func newMockProvider(t *testing.T) *mockprovider.Provider { loader, err := ldtestutil.DocumentLoader() require.NoError(t, err) + serviceMap := map[string]interface{}{ + presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, + outofbandSvc.Name: &mockoutofband.MockOobService{}, + } + return &mockprovider.Provider{ - StorageProviderValue: mockstorage.NewMockStoreProvider(), - DocumentLoaderValue: loader, + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + ServiceMap: serviceMap, } } diff --git a/pkg/controller/command/vcwallet/models.go b/pkg/controller/command/vcwallet/models.go index ea68cb049c..7bb82e859f 100644 --- a/pkg/controller/command/vcwallet/models.go +++ b/pkg/controller/command/vcwallet/models.go @@ -10,6 +10,9 @@ import ( "encoding/json" "time" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" + outofbandCmd "github.com/hyperledger/aries-framework-go/pkg/controller/command/outofband" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/wallet" @@ -306,3 +309,51 @@ type CreateKeyPairRequest struct { type CreateKeyPairResponse struct { *wallet.KeyPair } + +// ConnectRequest is request model for wallet DID connect operation. +type ConnectRequest struct { + WalletAuth + + outofbandCmd.AcceptInvitationArgs + + // Timeout (in milliseconds) waiting for connection status to be completed + Timeout time.Duration `json:"timeout,omitempty"` +} + +// ConnectResponse is response model from wallet DID connection operation. +type ConnectResponse struct { + // connection ID of the connection established. + ConnectionID string `json:"connectionID"` +} + +// ProposePresentationRequest is request model for performing propose presentation operation from wallet. +type ProposePresentationRequest struct { + WalletAuth + + // out-of-band invitation to establish connection and send propose presentation message. + Invitation *outofband.Invitation `json:"invitation"` + + // Optional From DID option to customize sender DID. + FromDID string `json:"from,omitempty"` + + // Timeout (in milliseconds) waiting for connection status to be completed + Timeout time.Duration `json:"timeout,omitempty"` +} + +// ProposePresentationResponse is response model from wallet propose presentation operation. +type ProposePresentationResponse struct { + // response request presentation message from relying party. + PresentationRequest *service.DIDCommMsgMap `json:"presentationRequest,omitempty"` +} + +// PresentProofRequest is request model from wallet present proof operation. +// Supported attachment MIME type "application/ld+json". +type PresentProofRequest struct { + WalletAuth + + // Thread ID from request presentation response + ThreadID string `json:"threadID,omitempty"` + + // presentation to be sent as part of present proof message. + Presentation *verifiable.Presentation `json:"presentation,omitempty"` +} diff --git a/pkg/controller/rest/vcwallet/models.go b/pkg/controller/rest/vcwallet/models.go index 10104d899b..1932a161f2 100644 --- a/pkg/controller/rest/vcwallet/models.go +++ b/pkg/controller/rest/vcwallet/models.go @@ -262,6 +262,56 @@ type checkProfileRequest struct { // nolint: unused,deadcode ID string `json:"id"` } +// connectRequest is request model for wallet DID connect operation. +// +// swagger:parameters connectReq +type connectRequest struct { // nolint: unused,deadcode + // Params for connecting to wallet for DIDComm. + // + // in: body + Params *vcwallet.ConnectRequest +} + +// connectResponse is response model from wallet DID connect operation. +// +// swagger:response connectRes +type connectResponse struct { + // wallet connect response. + // + // in: body + Response *vcwallet.ConnectResponse `json:"response"` +} + +// proposePresentationRequest is request model for performing propose presentation operation from wallet. +// +// swagger:parameters proposePresReq +type proposePresentationRequest struct { // nolint: unused,deadcode + // Params for proposing presentation from wallet. + // + // in: body + Params *vcwallet.ProposePresentationRequest +} + +// proposePresentationResponse is response model from wallet propose presentation operation. +// +// swagger:response proposePresRes +type proposePresentationResponse struct { + // response containing request presentation message from relyinig party. + // + // in: body + Response *vcwallet.ProposePresentationResponse `json:"response"` +} + +// presentProofRequest is request model for performing present proof operation from wallet. +// +// swagger:parameters presentProofReq +type presentProofRequest struct { // nolint: unused,deadcode + // Params for accepting presentation request and sending present proof message to relying party. + // + // in: body + Params *vcwallet.PresentProofRequest +} + // emptyRes model // // swagger:response emptyRes diff --git a/pkg/controller/rest/vcwallet/operation.go b/pkg/controller/rest/vcwallet/operation.go index a8442a7c14..2af35636fb 100644 --- a/pkg/controller/rest/vcwallet/operation.go +++ b/pkg/controller/rest/vcwallet/operation.go @@ -19,6 +19,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/controller/rest" "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/kms" "github.com/hyperledger/aries-framework-go/spi/storage" ) @@ -27,21 +28,24 @@ const ( OperationID = "/vcwallet" // command Paths. - CreateProfilePath = OperationID + "/create-profile" - UpdateProfilePath = OperationID + "/update-profile" - ProfileExistsPath = OperationID + "profile/{id}" - OpenPath = OperationID + "/open" - ClosePath = OperationID + "/close" - AddPath = OperationID + "/add" - RemovePath = OperationID + "/remove" - GetPath = OperationID + "/get" - GetAllPath = OperationID + "/getall" - QueryPath = OperationID + "/query" - IssuePath = OperationID + "/issue" - ProvePath = OperationID + "/prove" - VerifyPath = OperationID + "/verify" - DerivePath = OperationID + "/derive" - CreateKeyPairPath = OperationID + "/create-key-pair" + CreateProfilePath = OperationID + "/create-profile" + UpdateProfilePath = OperationID + "/update-profile" + ProfileExistsPath = OperationID + "profile/{id}" + OpenPath = OperationID + "/open" + ClosePath = OperationID + "/close" + AddPath = OperationID + "/add" + RemovePath = OperationID + "/remove" + GetPath = OperationID + "/get" + GetAllPath = OperationID + "/getall" + QueryPath = OperationID + "/query" + IssuePath = OperationID + "/issue" + ProvePath = OperationID + "/prove" + VerifyPath = OperationID + "/verify" + DerivePath = OperationID + "/derive" + CreateKeyPairPath = OperationID + "/create-key-pair" + ConnectPath = OperationID + "/connect" + ProposePresentationPath = OperationID + "/propose-presentation" + PresentProofPath = OperationID + "/present-proof" ) // provider contains dependencies for the verifiable credential wallet command controller @@ -51,6 +55,17 @@ type provider interface { VDRegistry() vdr.Registry Crypto() crypto.Crypto JSONLDDocumentLoader() ld.DocumentLoader + didCommProvider // to be used only if wallet needs to be participated in DIDComm. +} + +// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. +// TODO: using wallet KMS instead of provider KMS. +// TODO: reconcile Protocol storage with wallet store. +type didCommProvider interface { + KMS() kms.KeyManager + ServiceEndpoint() string + ProtocolStateStorageProvider() storage.Provider + Service(id string) (interface{}, error) } // Operation contains REST operations provided by verifiable credential wallet. @@ -93,6 +108,9 @@ func (o *Operation) registerHandler() { cmdutil.NewHTTPHandler(VerifyPath, http.MethodPost, o.Verify), cmdutil.NewHTTPHandler(DerivePath, http.MethodPost, o.Derive), cmdutil.NewHTTPHandler(CreateKeyPairPath, http.MethodPost, o.CreateKeyPair), + cmdutil.NewHTTPHandler(ConnectPath, http.MethodPost, o.Connect), + cmdutil.NewHTTPHandler(ProposePresentationPath, http.MethodPost, o.ProposePresentation), + cmdutil.NewHTTPHandler(PresentProofPath, http.MethodPost, o.PresentProof), } } @@ -321,6 +339,46 @@ func (o *Operation) CreateKeyPair(rw http.ResponseWriter, req *http.Request) { rest.Execute(o.command.CreateKeyPair, rw, req.Body) } +// Connect swagger:route POST /vcwallet/connect vcwallet connectReq +// +// accepts out-of-band invitations and performs DID exchange. +// +// Responses: +// default: genericError +// 200: connectRes +func (o *Operation) Connect(rw http.ResponseWriter, req *http.Request) { + rest.Execute(o.command.Connect, rw, req.Body) +} + +// ProposePresentation swagger:route POST /vcwallet/propose-presentation vcwallet proposePresReq +// +// accepts out-of-band invitation and sends message proposing presentation +// from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Responses: +// default: genericError +// 200: proposePresRes +func (o *Operation) ProposePresentation(rw http.ResponseWriter, req *http.Request) { + rest.Execute(o.command.ProposePresentation, rw, req.Body) +} + +// PresentProof swagger:route POST /vcwallet/present-proof vcwallet presentProofReq +// +// sends message present proof message from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Responses: +// default: genericError +// 200: emptyRes +func (o *Operation) PresentProof(rw http.ResponseWriter, req *http.Request) { + rest.Execute(o.command.PresentProof, rw, req.Body) +} + // getIDFromRequest returns ID from request. func getIDFromRequest(rw http.ResponseWriter, req *http.Request) (string, bool) { id := mux.Vars(req)["id"] diff --git a/pkg/controller/rest/vcwallet/operation_test.go b/pkg/controller/rest/vcwallet/operation_test.go index 0896ce76c0..a74272055e 100644 --- a/pkg/controller/rest/vcwallet/operation_test.go +++ b/pkg/controller/rest/vcwallet/operation_test.go @@ -21,16 +21,26 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/require" + outofbandClient "github.com/hyperledger/aries-framework-go/pkg/client/outofband" + "github.com/hyperledger/aries-framework-go/pkg/controller/command/outofband" "github.com/hyperledger/aries-framework-go/pkg/controller/command/vcwallet" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" "github.com/hyperledger/aries-framework-go/pkg/kms" + mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" + mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" + mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/pkg/wallet" ) @@ -240,7 +250,7 @@ func TestNew(t *testing.T) { cmd := New(newMockProvider(t), &vcwallet.Config{}) require.NotNil(t, cmd) - require.Len(t, cmd.GetRESTHandlers(), 15) + require.Len(t, cmd.GetRESTHandlers(), 18) }) } @@ -1376,6 +1386,258 @@ func TestOperation_CreateKeyPair(t *testing.T) { }) } +func TestOperation_Connect(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user-01" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &vcwallet.CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &vcwallet.UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("wallet connect operation success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + request := &vcwallet.ConnectRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + rq := httptest.NewRequest(http.MethodPost, ConnectPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.Connect(rw, rq) + require.Equal(t, rw.Code, http.StatusOK) + + var r connectResponse + require.NoError(t, json.NewDecoder(rw.Body).Decode(&r.Response)) + require.NotEmpty(t, r) + require.NotEmpty(t, r.Response) + require.Equal(t, sampleConnID, r.Response.ConnectionID) + }) + + t.Run("wallet connect operation failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + request := &vcwallet.ConnectRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + AcceptInvitationArgs: outofband.AcceptInvitationArgs{ + Invitation: &outofbandClient.Invitation{}, + MyLabel: "sample-label", + }, + } + + rq := httptest.NewRequest(http.MethodPost, ConnectPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.Connect(rw, rq) + require.Equal(t, rw.Code, http.StatusInternalServerError) + require.Contains(t, rw.Body.String(), sampleCommandError) + }) +} + +func TestOperation_ProposePresentation(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user-02" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &vcwallet.CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &vcwallet.UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("wallet propose presentation success", func(t *testing.T) { + sampleConnID := uuid.New().String() + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + thID := uuid.New().String() + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentation{ + Comment: "mock msg", + }), + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + request := &vcwallet.ProposePresentationRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + Invitation: &outofbandClient.Invitation{}, + } + + rq := httptest.NewRequest(http.MethodPost, ConnectPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.ProposePresentation(rw, rq) + require.Equal(t, rw.Code, http.StatusOK) + + var r proposePresentationResponse + require.NoError(t, json.NewDecoder(rw.Body).Decode(&r.Response)) + require.NotEmpty(t, r) + require.NotEmpty(t, r.Response) + require.NotEmpty(t, r.Response.PresentationRequest) + }) + + t.Run("wallet propose presentation failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + request := &vcwallet.ProposePresentationRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + Invitation: &outofbandClient.Invitation{}, + } + + rq := httptest.NewRequest(http.MethodPost, ProposePresentationPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.ProposePresentation(rw, rq) + require.Equal(t, rw.Code, http.StatusInternalServerError) + require.Contains(t, rw.Body.String(), sampleCommandError) + }) +} + +func TestOperation_PresentProof(t *testing.T) { + const sampleDIDCommUser = "sample-didcomm-user-03" + + mockctx := newMockProvider(t) + + createSampleUserProfile(t, mockctx, &vcwallet.CreateOrUpdateProfileRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + token, lock := unlockWallet(t, mockctx, &vcwallet.UnlockWalletRequest{ + UserID: sampleDIDCommUser, + LocalKMSPassphrase: samplePassPhrase, + }) + + defer lock() + + t.Run("wallet present proof success", func(t *testing.T) { + request := &vcwallet.PresentProofRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + ThreadID: uuid.New().String(), + Presentation: &verifiable.Presentation{}, + } + + rq := httptest.NewRequest(http.MethodPost, PresentProofPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.PresentProof(rw, rq) + require.Equal(t, rw.Code, http.StatusOK) + }) + + t.Run("wallet present proof failure", func(t *testing.T) { + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionContinueFunc: func(string, presentproofSvc.Opt) error { + return fmt.Errorf(sampleCommandError) + }, + } + + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + request := &vcwallet.PresentProofRequest{ + WalletAuth: vcwallet.WalletAuth{UserID: sampleDIDCommUser, Auth: token}, + ThreadID: uuid.New().String(), + Presentation: &verifiable.Presentation{}, + } + + rq := httptest.NewRequest(http.MethodPost, PresentProofPath, getReader(t, request)) + rw := httptest.NewRecorder() + + cmd := New(mockctx, &vcwallet.Config{}) + cmd.PresentProof(rw, rq) + require.Equal(t, rw.Code, http.StatusInternalServerError) + require.Contains(t, rw.Body.String(), sampleCommandError) + }) +} + func createSampleUserProfile(t *testing.T, ctx *mockprovider.Provider, request *vcwallet.CreateOrUpdateProfileRequest) { cmd := New(ctx, &vcwallet.Config{}) require.NotNil(t, cmd) @@ -1432,9 +1694,16 @@ func newMockProvider(t *testing.T) *mockprovider.Provider { loader, err := ldtestutil.DocumentLoader() require.NoError(t, err) + serviceMap := map[string]interface{}{ + presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, + outofbandSvc.Name: &mockoutofband.MockOobService{}, + } + return &mockprovider.Provider{ - StorageProviderValue: mockstorage.NewMockStoreProvider(), - DocumentLoaderValue: loader, + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + ServiceMap: serviceMap, } } diff --git a/pkg/mock/didcomm/protocol/presentproof/mock_presentproof.go b/pkg/mock/didcomm/protocol/presentproof/mock_presentproof.go new file mode 100644 index 0000000000..a589cc9dd3 --- /dev/null +++ b/pkg/mock/didcomm/protocol/presentproof/mock_presentproof.go @@ -0,0 +1,83 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package presentproof + +import ( + "github.com/google/uuid" + + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" + + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" +) + +// MockPresentProofSvc mock present proof service. +type MockPresentProofSvc struct { + service.Action + service.Message + ProtocolName string + HandleFunc func(service.DIDCommMsg) (string, error) + HandleOutboundFunc func(msg service.DIDCommMsg, myDID, theirDID string) (string, error) + AcceptFunc func(string) bool + ActionsFunc func() ([]presentproof.Action, error) + ActionContinueFunc func(string, presentproof.Opt) error +} + +// HandleInbound msg. +func (m *MockPresentProofSvc) HandleInbound(msg service.DIDCommMsg, ctx service.DIDCommContext) (string, error) { + if m.HandleFunc != nil { + return m.HandleFunc(msg) + } + + return uuid.New().String(), nil +} + +// HandleOutbound msg. +func (m *MockPresentProofSvc) HandleOutbound(msg service.DIDCommMsg, myDID, theirDID string) (string, error) { + if m.HandleOutboundFunc != nil { + return m.HandleOutboundFunc(msg, myDID, theirDID) + } + + return "", nil +} + +// Accept msg checks the msg type. +func (m *MockPresentProofSvc) Accept(msgType string) bool { + if m.AcceptFunc != nil { + return m.AcceptFunc(msgType) + } + + return true +} + +// Name return service name. +func (m *MockPresentProofSvc) Name() string { + if m.ProtocolName != "" { + return m.ProtocolName + } + + return "present-proof" +} + +func (m *MockPresentProofSvc) Actions() ([]presentproof.Action, error) { + if m.ActionsFunc != nil { + return m.ActionsFunc() + } + + return []presentproof.Action{}, nil +} + +func (m *MockPresentProofSvc) ActionContinue(piID string, opt presentproof.Opt) error { + if m.ActionContinueFunc != nil { + return m.ActionContinueFunc(piID, opt) + } + + return nil +} + +func (m *MockPresentProofSvc) ActionStop(piID string, err error) error { + return nil +} diff --git a/pkg/wallet/options.go b/pkg/wallet/options.go index 5b04045a95..b97a9b313a 100644 --- a/pkg/wallet/options.go +++ b/pkg/wallet/options.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hyperledger/aries-framework-go/component/storage/edv" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/kms/webkms" "github.com/hyperledger/aries-framework-go/pkg/secretlock" @@ -295,3 +296,88 @@ func FilterByCollection(collectionID string) GetAllContentsOptions { opts.collectionID = collectionID } } + +// connectOpts contains options for wallet's DIDComm connect features. +type connectOpts struct { + outofband.EventOptions + // timeout duration to wait before waiting for status 'completed'. + timeout time.Duration +} + +// ConnectOptions options for accepting incoming out-of-band invitation and connecting. +type ConnectOptions func(opts *connectOpts) + +// WithMyLabel option for providing label to be shared with the other agent during the subsequent did-exchange. +func WithMyLabel(label string) ConnectOptions { + return func(opts *connectOpts) { + opts.Label = label + } +} + +// WithReuseAnyConnection option to use any recognized DID in the services array for a reusable connection. +func WithReuseAnyConnection() ConnectOptions { + return func(opts *connectOpts) { + opts.ReuseAny = true + } +} + +// WithReuseDID option to provide DID to be used when reusing a connection. +func WithReuseDID(did string) ConnectOptions { + return func(opts *connectOpts) { + opts.ReuseDID = did + } +} + +// WithRouterConnections option to provide for router connections to be used. +func WithRouterConnections(conns ...string) ConnectOptions { + return func(opts *connectOpts) { + opts.Connections = conns + } +} + +// WithConnectTimeout option providing connect timeout, to wait for connection status to be 'completed'. +func WithConnectTimeout(timeout time.Duration) ConnectOptions { + return func(opts *connectOpts) { + opts.timeout = timeout + } +} + +// getOobMessageOptions gets out-of-band message options to accept invitation from connect opts. +func getOobMessageOptions(opts *connectOpts) []outofband.MessageOption { + var result []outofband.MessageOption + + if len(opts.Connections) > 0 { + result = append(result, outofband.WithRouterConnections(opts.Connections...)) + } + + if opts.ReuseAny { + result = append(result, outofband.ReuseAnyConnection()) + } + + return append(result, outofband.ReuseConnection(opts.ReuseDID)) +} + +// proposePresOpts contains options for proposing presentation from wallet. +type proposePresOpts struct { + // optional from DID option to customize message sender DID. + from string + // timeout duration to wait for request presentation response from relying party. + timeout time.Duration +} + +// ProposePresentationOption options for proposing presentation from wallet. +type ProposePresentationOption func(opts *proposePresOpts) + +// WithFromDID option for providing customized from DID for sending propose presentation message. +func WithFromDID(from string) ProposePresentationOption { + return func(opts *proposePresOpts) { + opts.from = from + } +} + +// WithPresentProofTimeout to provide timeout duration to wait for request presentation response from relying party. +func WithPresentProofTimeout(timeout time.Duration) ProposePresentationOption { + return func(opts *proposePresOpts) { + opts.timeout = timeout + } +} diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index 0289c78510..43097d9646 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -7,15 +7,24 @@ SPDX-License-Identifier: Apache-2.0 package wallet import ( + "context" "encoding/base64" "encoding/json" "errors" "fmt" + "time" + "github.com/google/uuid" "github.com/piprate/json-gold/ld" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" + "github.com/hyperledger/aries-framework-go/pkg/client/presentproof" "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/crypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/signer" @@ -26,6 +35,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/kms" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/spi/storage" ) @@ -41,8 +51,15 @@ const ( // miscellaneous constants. const ( - bbsContext = "https://w3id.org/security/bbs/v1" - emptyRawLength = 4 + bbsContext = "https://w3id.org/security/bbs/v1" + emptyRawLength = 4 + msgEventBufferSize = 10 + presentProofMimeType = "application/ld+json" + + // timeout constants. + defaultDIDExchangeTimeOut = 120 * time.Second + defaultWaitForRequestPresentationTimeOut = 120 * time.Second + retryDelay = 500 * time.Millisecond ) // proof options. @@ -64,6 +81,17 @@ type provider interface { VDRegistry() vdr.Registry Crypto() crypto.Crypto JSONLDDocumentLoader() ld.DocumentLoader + didCommProvider // to be used only if wallet needs to be participated in DIDComm. +} + +// didCommProvider to be used only if wallet needs to be participated in DIDComm operation. +// TODO: using wallet KMS instead of provider KMS. +// TODO: reconcile Protocol storage with wallet store. +type didCommProvider interface { + KMS() kms.KeyManager + ServiceEndpoint() string + ProtocolStateStorageProvider() storage.Provider + Service(id string) (interface{}, error) } type provable interface { @@ -92,6 +120,15 @@ type Wallet struct { // document loader for JSON-LD contexts jsonldDocumentLoader ld.DocumentLoader + + // present proof client + presentProofClient *presentproof.Client + + // out of band client + oobClient *outofband.Client + + // connection lookup + connectionLookup *connection.Lookup } // New returns new verifiable credential wallet for given user. @@ -109,6 +146,21 @@ func New(userID string, ctx provider) (*Wallet, error) { return nil, fmt.Errorf("failed to get VC wallet profile: %w", err) } + presentProofClient, err := presentproof.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize present proof client: %w", err) + } + + oobClient, err := outofband.New(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize out-of-band client: %w", err) + } + + connectionLookup, err := connection.NewLookup(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize connection lookup: %w", err) + } + return &Wallet{ userID: userID, profile: profile, @@ -117,6 +169,9 @@ func New(userID string, ctx provider) (*Wallet, error) { contents: newContentStore(ctx.StorageProvider(), profile), vdr: ctx.VDRegistry(), jsonldDocumentLoader: ctx.JSONLDDocumentLoader(), + presentProofClient: presentProofClient, + oobClient: oobClient, + connectionLookup: connectionLookup, }, nil } @@ -542,6 +597,124 @@ func (c *Wallet) CreateKeyPair(authToken string, keyType kms.KeyType) (*KeyPair, }, nil } +// Connect accepts out-of-band invitations and performs DID exchange. +// +// Args: +// - authToken: authorization for performing create key pair operation. +// - invitation: out-of-band invitation. +// - options: connection options. +// +// Returns: +// - connection ID if DID exchange is successful. +// - error if operation false. +// +func (c *Wallet) Connect(authToken string, invitation *outofband.Invitation, options ...ConnectOptions) (string, error) { //nolint: lll + statusCh := make(chan service.StateMsg, msgEventBufferSize) + + err := c.oobClient.RegisterMsgEvent(statusCh) + if err != nil { + return "", fmt.Errorf("failed to register msg event : %w", err) + } + + defer func() { + e := c.oobClient.UnregisterMsgEvent(statusCh) + if e != nil { + logger.Warnf("Failed to unregister msg event for connect: %w", e) + } + }() + + opts := &connectOpts{} + for _, opt := range options { + opt(opts) + } + + connID, err := c.oobClient.AcceptInvitation(invitation, opts.Label, getOobMessageOptions(opts)...) + if err != nil { + return "", fmt.Errorf("failed to accept invitation : %w", err) + } + + if opts.timeout == 0 { + opts.timeout = defaultDIDExchangeTimeOut + } + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + err = waitForConnect(ctx, statusCh, connID) + if err != nil { + return "", fmt.Errorf("wallet connect failed : %w", err) + } + + return connID, nil +} + +// ProposePresentation accepts out-of-band invitation and sends message proposing presentation +// from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - authToken: authorization for performing operation. +// - invitation: out-of-band invitation from relying party. +// - options: options for accepting invitation and send propose presentation message. +// +// Returns: +// - DIDCommMsgMap containing request presentation message if operation is successful. +// - error if operation fails. +// +func (c *Wallet) ProposePresentation(authToken string, invitation *outofband.Invitation, options ...ProposePresentationOption) (*service.DIDCommMsgMap, error) { //nolint: lll + connID, err := c.Connect(authToken, invitation) + if err != nil { + return nil, fmt.Errorf("failed to perform did connection : %w", err) + } + + connRecord, err := c.connectionLookup.GetConnectionRecord(connID) + if err != nil { + return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) + } + + opts := preparePresentProofOpts(connRecord, options...) + + thID, err := c.presentProofClient.SendProposePresentation(&presentproof.ProposePresentation{}, connRecord.MyDID, + opts.from) + if err != nil { + return nil, fmt.Errorf("failed to propose presentation from wallet: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) + defer cancel() + + return c.waitForRequestPresentation(ctx, thID) +} + +// PresentProof sends message present proof message from wallet to relying party. +// +// Currently Supporting +// [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) +// +// Args: +// - authToken: authorization for performing operation. +// - thID: thread ID (action ID) of request presentation. +// - presentation: presentation to be sent. +// +// Returns: +// - error if operation fails. +// +// TODO: wait for acknowledgement option to be added. +func (c *Wallet) PresentProof(authToken, thID string, presentation *verifiable.Presentation) error { + return c.presentProofClient.AcceptRequestPresentation(thID, &presentproof.Presentation{ + Type: presentproofSvc.PresentationMsgType, + PresentationsAttach: []decorator.Attachment{{ + ID: uuid.New().String(), + MimeType: presentProofMimeType, + Data: decorator.AttachmentData{ + JSON: presentation, + }, + }}, + }, nil) +} + //nolint: funlen,gocyclo func (c *Wallet) resolveOptionsToPresent(auth string, credentials ...ProveOptions) (*verifiable.Presentation, error) { var allCredentials []*verifiable.Credential @@ -763,16 +936,92 @@ func (c *Wallet) validateVerificationMethod(didDoc *did.Doc, opts *ProofOptions, return fmt.Errorf("unable to find '%s' for given verification method", supportedRelationships[relationship]) } +func (c *Wallet) waitForRequestPresentation(ctx context.Context, piid string) (*service.DIDCommMsgMap, error) { + done := make(chan *service.DIDCommMsgMap) + + go func() { + for { + actions, err := c.presentProofClient.Actions() + if err != nil { + continue + } + + if len(actions) > 0 { + for _, action := range actions { + if action.PIID == piid { + done <- &action.Msg + return + } + } + } + + select { + default: + time.Sleep(retryDelay) + case <-ctx.Done(): + return + } + } + }() + + select { + case msg := <-done: + return msg, nil + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for request presentation message") + } +} + +func waitForConnect(ctx context.Context, didStateMsgs chan service.StateMsg, connID string) error { + done := make(chan struct{}) + + go func() { + for msg := range didStateMsgs { + if msg.Type != service.PostState || msg.StateID != didexchange.StateIDCompleted { + continue + } + + var event didexchange.Event + + switch p := msg.Properties.(type) { + case didexchange.Event: + event = p + default: + logger.Warnf("failed to cast didexchange event properties") + + continue + } + + if event.ConnectionID() == connID { + logger.Debugf( + "Received connection complete event for invitationID=%s connectionID=%s", + event.InvitationID(), event.ConnectionID()) + + close(done) + + break + } + } + }() + + select { + case <-done: + return nil + case <-ctx.Done(): + return fmt.Errorf("time out waiting for did exchange state 'completed'") + } +} + // addContext adds context if not found in given data model. -func addContext(v interface{}, context string) { +func addContext(v interface{}, ldcontext string) { if vc, ok := v.(*verifiable.Credential); ok { for _, ctx := range vc.Context { - if ctx == context { + if ctx == ldcontext { return } } - vc.Context = append(vc.Context, context) + vc.Context = append(vc.Context, ldcontext) } } @@ -796,3 +1045,20 @@ func updateProfile(auth string, profile *profile) error { return nil } + +func preparePresentProofOpts(connRecord *connection.Record, options ...ProposePresentationOption) *proposePresOpts { + opts := &proposePresOpts{} + for _, opt := range options { + opt(opts) + } + + if opts.from == "" { + opts.from = connRecord.TheirDID + } + + if opts.timeout == 0 { + opts.timeout = defaultWaitForRequestPresentationTimeOut + } + + return opts +} diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index 821f580135..710ea88db3 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -23,8 +23,13 @@ import ( "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/storage/edv" + "github.com/hyperledger/aries-framework-go/pkg/client/outofband" "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" "github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" + outofbandSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband" + presentproofSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/presentproof" "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/doc/presexch" "github.com/hyperledger/aries-framework-go/pkg/doc/util" @@ -34,12 +39,16 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/kms/webkms" cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto" + mockdidexchange "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/didexchange" + mockoutofband "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/outofband" + mockpresentproof "github.com/hyperledger/aries-framework-go/pkg/mock/didcomm/protocol/presentproof" mockkms "github.com/hyperledger/aries-framework-go/pkg/mock/kms" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" "github.com/hyperledger/aries-framework-go/pkg/mock/secretlock" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" "github.com/hyperledger/aries-framework-go/pkg/secretlock/local/masterlock/pbkdf2" + "github.com/hyperledger/aries-framework-go/pkg/store/connection" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/spi/storage" ) @@ -542,7 +551,7 @@ func TestNew(t *testing.T) { require.Contains(t, err.Error(), "profile does not exist") }) - t.Run("test update wallet failure - save profile error", func(t *testing.T) { + t.Run("test get wallet failure - save profile error", func(t *testing.T) { mockctx := newMockProvider(t) mockctx.StorageProviderValue = &mockstorage.MockStoreProvider{ ErrOpenStoreHandle: fmt.Errorf(sampleWalletErr), @@ -553,6 +562,47 @@ func TestNew(t *testing.T) { require.Empty(t, wallet) require.Contains(t, err.Error(), sampleWalletErr) }) + + t.Run("test get wallet failure - present proof client initialize error", func(t *testing.T) { + mockctx := newMockProvider(t) + delete(mockctx.ServiceMap, presentproofSvc.Name) + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + require.Contains(t, err.Error(), "failed to initialize present proof client") + }) + + t.Run("test get wallet failure - oob client initialize error", func(t *testing.T) { + mockctx := newMockProvider(t) + delete(mockctx.ServiceMap, outofbandSvc.Name) + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + require.Contains(t, err.Error(), "failed to initialize out-of-band client") + }) + + t.Run("test get wallet failure - connection lookup initialize error", func(t *testing.T) { + mockctx := newMockProvider(t) + mockStoreProvider := mockstorage.NewMockStoreProvider() + mockStoreProvider.FailNamespace = "didexchange" + mockctx.StorageProviderValue = mockStoreProvider + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + require.Contains(t, err.Error(), "failed to initialize connection lookup") + }) } func TestWallet_OpenClose(t *testing.T) { @@ -2446,15 +2496,556 @@ func TestWallet_CreateKeyPair(t *testing.T) { }) } +func TestWallet_Connect(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test did connect success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := wallet.Connect(token, &outofband.Invitation{}) + require.NoError(t, err) + require.Equal(t, sampleConnID, connectionID) + }) + + t.Run("test did connect failure - accept invitation failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to accept invitation") + require.Empty(t, connectionID) + }) + + t.Run("test did connect failure - register event failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to register msg event") + require.Empty(t, connectionID) + }) + + t.Run("test did connect failure - state not completed", func(t *testing.T) { + mockctx.ServiceMap[outofbandSvc.Name] = &mockoutofband.MockOobService{} + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := wallet.Connect(token, &outofband.Invitation{}, WithConnectTimeout(1*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "time out waiting for did exchange state 'completed'") + require.Empty(t, connectionID) + }) + + t.Run("test did connect success - with warnings", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PreState, + StateID: didexchange.StateIDCompleted, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + } + + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + UnregisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + connectionID, err := wallet.Connect(token, &outofband.Invitation{}) + require.NoError(t, err) + require.Equal(t, sampleConnID, connectionID) + }) + + t.Run("test oob connect options", func(t *testing.T) { + options := []ConnectOptions{ + WithConnectTimeout(10 * time.Second), + WithRouterConnections("sample-conn"), + WithMyLabel("sample-label"), + WithReuseAnyConnection(), + WithReuseDID("sample-did"), + } + + opts := &connectOpts{} + for _, opt := range options { + opt(opts) + } + + require.Equal(t, opts.timeout, 10*time.Second) + require.Equal(t, opts.Connections[0], "sample-conn") + require.Equal(t, opts.MyLabel(), "sample-label") + require.Equal(t, opts.ReuseDID, "sample-did") + require.True(t, opts.ReuseAny) + + require.Len(t, getOobMessageOptions(opts), 3) + }) +} + +func TestWallet_ProposePresentation(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test propose presentation success", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + thID := uuid.New().String() + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return []presentproofSvc.Action{ + { + PIID: thID, + Msg: service.NewDIDCommMsgMap(&presentproofSvc.RequestPresentation{ + Comment: "mock msg", + }), + }, + }, nil + }, + HandleFunc: func(service.DIDCommMsg) (string, error) { + return thID, nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}) + require.NoError(t, err) + require.NotEmpty(t, msg) + }) + + t.Run("test propose presentation failure - did connect failure", func(t *testing.T) { + oobSvc := &mockoutofband.MockOobService{ + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + return fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to perform did connection") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - no connection found", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to lookup connection") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - failed to send", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return "", fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + require.Contains(t, err.Error(), "failed to propose presentation from wallet") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - timeout waiting for presentation request", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + MyDID: "did:mydid", + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}, WithPresentProofTimeout(600*time.Millisecond)) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for request presentation message") + require.Empty(t, msg) + }) + + t.Run("test propose presentation failure - action error", func(t *testing.T) { + sampleConnID := uuid.New().String() + + oobSvc := &mockoutofband.MockOobService{ + AcceptInvitationHandle: func(*outofbandSvc.Invitation, outofbandSvc.Options) (string, error) { + return sampleConnID, nil + }, + RegisterMsgEventHandle: func(ch chan<- service.StateMsg) error { + ch <- service.StateMsg{ + Type: service.PostState, + StateID: didexchange.StateIDCompleted, + Properties: &mockdidexchange.MockEventProperties{ConnID: sampleConnID}, + } + + return nil + }, + } + + ppSvc := &mockpresentproof.MockPresentProofSvc{ + HandleFunc: func(service.DIDCommMsg) (string, error) { + return uuid.New().String(), nil + }, + ActionsFunc: func() ([]presentproofSvc.Action, error) { + return nil, fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[outofbandSvc.Name] = oobSvc + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + store, err := mockctx.StorageProvider().OpenStore(connection.Namespace) + require.NoError(t, err) + + record := &connection.Record{ + ConnectionID: sampleConnID, + TheirDID: "did:theirDID", + } + recordBytes, err := json.Marshal(record) + require.NoError(t, err) + require.NoError(t, store.Put(fmt.Sprintf("conn_%s", sampleConnID), recordBytes)) + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + msg, err := wallet.ProposePresentation(token, &outofband.Invitation{}, WithPresentProofTimeout(1*time.Millisecond), + WithFromDID("did:sample:from")) + require.Error(t, err) + require.Contains(t, err.Error(), "timeout waiting for request presentation message") + require.Empty(t, msg) + }) +} + +func TestWallet_PresentProof(t *testing.T) { + sampleDIDCommUser := uuid.New().String() + mockctx := newMockProvider(t) + err := CreateProfile(sampleDIDCommUser, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("test present proof success", func(t *testing.T) { + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + err = wallet.PresentProof(token, uuid.New().String(), &verifiable.Presentation{}) + require.NoError(t, err) + }) + + t.Run("test present proof failure", func(t *testing.T) { + ppSvc := &mockpresentproof.MockPresentProofSvc{ + ActionContinueFunc: func(string, presentproofSvc.Opt) error { + return fmt.Errorf(sampleWalletErr) + }, + } + + mockctx.ServiceMap[presentproofSvc.Name] = ppSvc + + wallet, err := New(sampleDIDCommUser, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, token) + + defer wallet.Close() + + err = wallet.PresentProof(token, uuid.New().String(), &verifiable.Presentation{}) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + }) +} + func newMockProvider(t *testing.T) *mockprovider.Provider { t.Helper() loader, err := ldtestutil.DocumentLoader() require.NoError(t, err) + serviceMap := map[string]interface{}{ + presentproofSvc.Name: &mockpresentproof.MockPresentProofSvc{}, + outofbandSvc.Name: &mockoutofband.MockOobService{}, + } + return &mockprovider.Provider{ - StorageProviderValue: mockstorage.NewMockStoreProvider(), - DocumentLoaderValue: loader, + StorageProviderValue: mockstorage.NewMockStoreProvider(), + ProtocolStateStorageProviderValue: mockstorage.NewMockStoreProvider(), + DocumentLoaderValue: loader, + ServiceMap: serviceMap, } }