diff --git a/pkg/didcomm/dispatcher/api.go b/pkg/didcomm/dispatcher/api.go index 004b5f8df..3f430556e 100644 --- a/pkg/didcomm/dispatcher/api.go +++ b/pkg/didcomm/dispatcher/api.go @@ -19,6 +19,12 @@ type Service interface { // Outbound interface type Outbound interface { + // Sends the message after packing with the sender key and recipient keys. Send(interface{}, string, *service.Destination) error + + // Sends the message after packing with the keys derived from DIDs. SendToDID(msg interface{}, myDID, theirDID string) error + + // Forward forwards the message without packing to the destination. + Forward(interface{}, *service.Destination) error } diff --git a/pkg/didcomm/dispatcher/outbound.go b/pkg/didcomm/dispatcher/outbound.go index e18e64cbf..6f8ba3fd0 100644 --- a/pkg/didcomm/dispatcher/outbound.go +++ b/pkg/didcomm/dispatcher/outbound.go @@ -45,7 +45,7 @@ func (o *OutboundDispatcher) SendToDID(msg interface{}, myDID, theirDID string) return nil } -// Send msg +// Send sends the message after packing with the sender key and recipient keys. func (o *OutboundDispatcher) Send(msg interface{}, senderVerKey string, des *service.Destination) error { for _, v := range o.outboundTransports { if !v.AcceptRecipient(des.RecipientKeys) { @@ -87,6 +87,8 @@ func (o *OutboundDispatcher) Send(msg interface{}, senderVerKey string, des *ser // set the return route option des.TransportReturnRoute = o.transportReturnRoute + packedMsg = createForwardMessage(packedMsg, des) + _, err = v.Send(packedMsg, des) if err != nil { return fmt.Errorf("failed to send msg using outbound transport: %w", err) @@ -97,3 +99,48 @@ func (o *OutboundDispatcher) Send(msg interface{}, senderVerKey string, des *ser return fmt.Errorf("no outbound transport found for serviceEndpoint: %s", des.ServiceEndpoint) } + +// Forward forwards the message without packing to the destination. +func (o *OutboundDispatcher) Forward(msg interface{}, des *service.Destination) error { + for _, v := range o.outboundTransports { + if !v.AcceptRecipient(des.RecipientKeys) { + if !v.Accept(des.ServiceEndpoint) { + continue + } + } + + req, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("failed marshal to bytes: %w", err) + } + + _, err = v.Send(req, des) + if err != nil { + return fmt.Errorf("failed to send msg using outbound transport: %w", err) + } + + return nil + } + + return fmt.Errorf("no outbound transport found for serviceEndpoint: %s", des.ServiceEndpoint) +} + +func createForwardMessage(msg []byte, des *service.Destination) []byte { + // TODO https://github.com/hyperledger/aries-framework-go/issues/807#issuecomment-566126744 message needs to + // be packed with anon crypt if the request needs through routed ie. des.RoutingKeys != nil + // psuedocode: + // if des.RoutingKeys != nil { + // // create forward message: + // forward := &Forward{ + // Type: "https://didcomm.org/routing/1.0/forward", + // ID: uuid.New().String(), + // To: "destinationRecKey", + // Msg: packedMsg, + // } + // + // // pack above message using anon crypt + // + // // return the message + // } + return msg +} diff --git a/pkg/didcomm/dispatcher/outbound_test.go b/pkg/didcomm/dispatcher/outbound_test.go index fefbc168a..eb197dec1 100644 --- a/pkg/didcomm/dispatcher/outbound_test.go +++ b/pkg/didcomm/dispatcher/outbound_test.go @@ -136,6 +136,33 @@ func TestOutboundDispatcherTransportReturnRoute(t *testing.T) { }) } +func TestOutboundDispatcher_Forward(t *testing.T) { + t.Run("test forward - success", func(t *testing.T) { + o := NewOutbound(&mockProvider{ + packagerValue: &mockpackager.Packager{}, + outboundTransportsValue: []transport.OutboundTransport{&mockdidcomm.MockOutboundTransport{AcceptValue: true}}, + }) + require.NoError(t, o.Forward("data", &service.Destination{ServiceEndpoint: "url"})) + }) + + t.Run("test forward - no outbound transport found", func(t *testing.T) { + o := NewOutbound(&mockProvider{packagerValue: &mockpackager.Packager{}, + outboundTransportsValue: []transport.OutboundTransport{&mockdidcomm.MockOutboundTransport{AcceptValue: false}}}) + err := o.Forward("data", &service.Destination{ServiceEndpoint: "url"}) + require.Error(t, err) + require.Contains(t, err.Error(), "no outbound transport found for serviceEndpoint: url") + }) + + t.Run("test forward - outbound send failure", func(t *testing.T) { + o := NewOutbound(&mockProvider{packagerValue: &mockpackager.Packager{}, + outboundTransportsValue: []transport.OutboundTransport{ + &mockdidcomm.MockOutboundTransport{AcceptValue: true, SendErr: fmt.Errorf("send error")}}}) + err := o.Forward("data", &service.Destination{ServiceEndpoint: "url"}) + require.Error(t, err) + require.Contains(t, err.Error(), "send error") + }) +} + // mockProvider mock provider type mockProvider struct { packagerValue commontransport.Packager diff --git a/pkg/didcomm/protocol/route/models.go b/pkg/didcomm/protocol/route/models.go index 16c43059b..7bd1d3e4b 100644 --- a/pkg/didcomm/protocol/route/models.go +++ b/pkg/didcomm/protocol/route/models.go @@ -50,3 +50,13 @@ type UpdateResponse struct { Action string `json:"action,omitempty"` Result string `json:"result,omitempty"` } + +// Forward route forward message. +// nolint lll - url in the next line is long +// https://github.com/hyperledger/aries-rfcs/blob/master/concepts/0094-cross-domain-messaging/README.md#corerouting10forward +type Forward struct { + Type string `json:"@type,omitempty"` + ID string `json:"@id,omitempty"` + To string `json:"@to,omitempty"` + Msg interface{} `json:"@msg,omitempty"` +} diff --git a/pkg/didcomm/protocol/route/service.go b/pkg/didcomm/protocol/route/service.go index 9287664f7..5160314b3 100644 --- a/pkg/didcomm/protocol/route/service.go +++ b/pkg/didcomm/protocol/route/service.go @@ -39,6 +39,9 @@ const ( // KeyListUpdateResponseMsgType defines the route coordination key list update message response type. KeylistUpdateResponseMsgType = CoordinationSpec + "keylist_update_response" + + // ForwardMsgType defines the route forward message type. + ForwardMsgType = "https://didcomm.org/routing/1.0/forward" ) // constants for key list update processing @@ -92,7 +95,7 @@ func New(prov provider) (*Service, error) { } // HandleInbound handles inbound route coordination messages. -func (s *Service) HandleInbound(msg *service.DIDCommMsg) (string, error) { +func (s *Service) HandleInbound(msg *service.DIDCommMsg) (string, error) { // nolint gocyclo (5 switch cases) // perform action on inbound message asynchronously go func() { switch msg.Header.Type { @@ -112,6 +115,10 @@ func (s *Service) HandleInbound(msg *service.DIDCommMsg) (string, error) { if err := s.handleKeylistUpdateResponse(msg); err != nil { logger.Errorf("handle route keylist update response error : %s", err) } + case ForwardMsgType: + if err := s.handleForward(msg); err != nil { + logger.Errorf("handle forward error : %s", err) + } } }() @@ -126,7 +133,7 @@ func (s *Service) HandleOutbound(msg *service.DIDCommMsg, destination *service.D // Accept checks whether the service can handle the message type. func (s *Service) Accept(msgType string) bool { switch msgType { - case RequestMsgType, GrantMsgType, KeylistUpdateMsgType, KeylistUpdateResponseMsgType: + case RequestMsgType, GrantMsgType, KeylistUpdateMsgType, KeylistUpdateResponseMsgType, ForwardMsgType: return true } @@ -197,7 +204,7 @@ func (s *Service) handleKeylistUpdate(msg *service.DIDCommMsg) error { val := "" result := success - err = s.routeStore.Put(v.RecipientKey, []byte(val)) + err = s.routeStore.Put(dataKey(v.RecipientKey), []byte(val)) if err != nil { logger.Errorf("failed to add the route key to store : %s", err) @@ -246,3 +253,29 @@ func (s *Service) handleKeylistUpdateResponse(msg *service.DIDCommMsg) error { return nil } + +func (s *Service) handleForward(msg *service.DIDCommMsg) error { + // unmarshal the payload + forward := &Forward{} + + err := json.Unmarshal(msg.Payload, forward) + if err != nil { + return fmt.Errorf("forward message unmarshal : %w", err) + } + + // TODO Open question - https://github.com/hyperledger/aries-framework-go/issues/965 Mismatch between Route + // Coordination and Forward RFC. For now assume, the TO field contains the recipient key. + _, err = s.routeStore.Get(dataKey(forward.To)) + if err != nil { + return fmt.Errorf("route key fetch : %w", err) + } + + // TODO https://github.com/hyperledger/aries-framework-go/issues/725 get destination details from the + // did retrieved from previous Get call. + + return s.outbound.Forward(forward.Msg, nil) +} + +func dataKey(id string) string { + return "route-" + id +} diff --git a/pkg/didcomm/protocol/route/service_test.go b/pkg/didcomm/protocol/route/service_test.go index be1567a00..8fbf318fe 100644 --- a/pkg/didcomm/protocol/route/service_test.go +++ b/pkg/didcomm/protocol/route/service_test.go @@ -43,6 +43,7 @@ func TestServiceAccept(t *testing.T) { require.Equal(t, true, s.Accept(GrantMsgType)) require.Equal(t, true, s.Accept(KeylistUpdateMsgType)) require.Equal(t, true, s.Accept(KeylistUpdateResponseMsgType)) + require.Equal(t, true, s.Accept(ForwardMsgType)) require.Equal(t, false, s.Accept("unsupported msg type")) } @@ -177,7 +178,7 @@ func TestServiceUpdateKeyListMsg(t *testing.T) { update := make(map[string]updateResult) update["ABC"] = updateResult{action: add, result: success} update["XYZ"] = updateResult{action: remove, result: serverError} - update[""] = updateResult{action: add, result: serverError} + update[""] = updateResult{action: add, result: success} svc, err := New(&mockProvider{ @@ -244,3 +245,67 @@ func TestServiceKeylistUpdateResponseMsg(t *testing.T) { require.Contains(t, err.Error(), "route keylist update response message unmarshal") }) } + +func TestServiceForwardMsg(t *testing.T) { + t.Run("test service handle inbound forward msg - success", func(t *testing.T) { + to := randomID() + svc, err := New(&mockProvider{}) + require.NoError(t, err) + + err = svc.routeStore.Put(to, []byte("did:example:123")) + require.NoError(t, err) + + msgID := randomID() + + id, err := svc.HandleInbound(generateForwardMsgPayload(t, msgID, to, nil)) + require.NoError(t, err) + require.Equal(t, msgID, id) + }) + + t.Run("test service handle forward msg - success", func(t *testing.T) { + svc, err := New(&mockProvider{}) + require.NoError(t, err) + + msg := &service.DIDCommMsg{Payload: []byte("invalid json")} + + err = svc.handleForward(msg) + require.Error(t, err) + require.Contains(t, err.Error(), "forward message unmarshal") + }) + + t.Run("test service handle forward msg - route key fetch fail", func(t *testing.T) { + to := randomID() + msgID := randomID() + + svc, err := New(&mockProvider{}) + require.NoError(t, err) + + err = svc.handleForward(generateForwardMsgPayload(t, msgID, to, nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "route key fetch") + }) + + t.Run("test service handle forward msg - validate forward message content", func(t *testing.T) { + to := randomID() + msgID := randomID() + + content := "packed message destined to the recipient through router" + msg := generateForwardMsgPayload(t, msgID, to, content) + + svc, err := New(&mockProvider{ + outbound: &mockOutbound{validateForward: func(msg interface{}) error { + require.Equal(t, content, msg) + + return nil + }, + }, + }) + require.NoError(t, err) + + err = svc.routeStore.Put(dataKey(to), []byte("did:example:123")) + require.NoError(t, err) + + err = svc.handleForward(msg) + require.NoError(t, err) + }) +} diff --git a/pkg/didcomm/protocol/route/support_test.go b/pkg/didcomm/protocol/route/support_test.go index 1bf0d27cf..25c79962f 100644 --- a/pkg/didcomm/protocol/route/support_test.go +++ b/pkg/didcomm/protocol/route/support_test.go @@ -59,7 +59,8 @@ func (p *mockProvider) KMS() kms.KeyManager { // mock outbound type mockOutbound struct { - validateSend func(msg interface{}) error + validateSend func(msg interface{}) error + validateForward func(msg interface{}) error } func (m *mockOutbound) Send(msg interface{}, senderVerKey string, des *service.Destination) error { @@ -70,6 +71,10 @@ func (m *mockOutbound) SendToDID(msg interface{}, myDID, theirDID string) error return nil } +func (m *mockOutbound) Forward(msg interface{}, des *service.Destination) error { + return m.validateForward(msg) +} + func generateRequestMsgPayload(t *testing.T, id string) *service.DIDCommMsg { requestBytes, err := json.Marshal(&Request{ Type: RequestMsgType, @@ -124,6 +129,21 @@ func generateKeylistUpdateResponseMsgPayload(t *testing.T, id string, updates [] return didMsg } +func generateForwardMsgPayload(t *testing.T, id, to string, msg interface{}) *service.DIDCommMsg { + requestBytes, err := json.Marshal(&Forward{ + Type: ForwardMsgType, + ID: id, + To: to, + Msg: msg, + }) + require.NoError(t, err) + + didMsg, err := service.NewDIDCommMsg(requestBytes) + require.NoError(t, err) + + return didMsg +} + func randomID() string { return uuid.New().String() } diff --git a/pkg/internal/mock/didcomm/dispatcher/mock_outbound.go b/pkg/internal/mock/didcomm/dispatcher/mock_outbound.go index ad9c9c0e4..468b37a0f 100644 --- a/pkg/internal/mock/didcomm/dispatcher/mock_outbound.go +++ b/pkg/internal/mock/didcomm/dispatcher/mock_outbound.go @@ -23,3 +23,8 @@ func (m *MockOutbound) Send(msg interface{}, senderVerKey string, des *service.D func (m *MockOutbound) SendToDID(msg interface{}, myDID, theirDID string) error { return nil } + +// Forward msg +func (m *MockOutbound) Forward(msg interface{}, des *service.Destination) error { + return nil +}