github.com/hyperledger/aries-framework-go@v0.3.2/pkg/client/outofband/client.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package outofband
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  
    13  	"github.com/google/uuid"
    14  
    15  	"github.com/hyperledger/aries-framework-go/pkg/common/model"
    16  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
    17  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator"
    18  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange"
    19  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator"
    20  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofband"
    21  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/transport"
    22  	"github.com/hyperledger/aries-framework-go/pkg/doc/did"
    23  	"github.com/hyperledger/aries-framework-go/pkg/doc/util/kmsdidkey"
    24  	"github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
    25  	"github.com/hyperledger/aries-framework-go/pkg/kms"
    26  )
    27  
    28  type (
    29  	// Invitation is this protocol's `invitation` message.
    30  	Invitation outofband.Invitation
    31  	// Action contains helpful information about action.
    32  	Action outofband.Action
    33  )
    34  
    35  const (
    36  	// InvitationMsgType is the '@type' for the invitation message.
    37  	InvitationMsgType = outofband.InvitationMsgType
    38  	// HandshakeReuseMsgType is the '@type' for the handshake reuse message.
    39  	HandshakeReuseMsgType = outofband.HandshakeReuseMsgType
    40  	// HandshakeReuseAcceptedMsgType is the '@type' for the handshake reuse accepted message.
    41  	HandshakeReuseAcceptedMsgType = outofband.HandshakeReuseAcceptedMsgType
    42  )
    43  
    44  // EventOptions are is a container of options that you can pass to an event's
    45  // Continue function to customize the reaction to incoming out-of-band messages.
    46  type EventOptions struct {
    47  	// Label will be shared with the other agent during the subsequent did-exchange.
    48  	Label string
    49  	// Connections allows specifying router connections.
    50  	Connections []string
    51  	ReuseAny    bool
    52  	ReuseDID    string
    53  }
    54  
    55  // RouterConnections return router connections.
    56  func (e *EventOptions) RouterConnections() []string {
    57  	return e.Connections
    58  }
    59  
    60  // MyLabel will be shared with the other agent during the subsequent did-exchange.
    61  func (e *EventOptions) MyLabel() string {
    62  	return e.Label
    63  }
    64  
    65  // ReuseAnyConnection signals whether to use any recognized DID in the services array for a reusable connection.
    66  func (e *EventOptions) ReuseAnyConnection() bool {
    67  	return e.ReuseAny
    68  }
    69  
    70  // ReuseConnection returns the DID to be used when reusing a connection.
    71  func (e *EventOptions) ReuseConnection() string {
    72  	return e.ReuseDID
    73  }
    74  
    75  // Event is a container of out-of-band protocol-specific properties for DIDCommActions and StateMsgs.
    76  type Event interface {
    77  	// ConnectionID of the connection record, once it's created.
    78  	// This becomes available in a post-state event unless an error condition is encountered.
    79  	ConnectionID() string
    80  	// Error is non-nil if an error is encountered.
    81  	Error() error
    82  }
    83  
    84  // MessageOption allow you to customize the way out-of-band messages are built.
    85  type MessageOption func(*message)
    86  
    87  type message struct {
    88  	Label              string
    89  	Goal               string
    90  	GoalCode           string
    91  	RouterConnections  []string
    92  	Service            []interface{}
    93  	HandshakeProtocols []string
    94  	Attachments        []*decorator.Attachment
    95  	Accept             []string
    96  	ReuseAnyConnection bool
    97  	ReuseConnection    string
    98  }
    99  
   100  func (m *message) RouterConnection() string {
   101  	if len(m.RouterConnections) == 0 {
   102  		return ""
   103  	}
   104  
   105  	return m.RouterConnections[0]
   106  }
   107  
   108  // OobService defines the outofband service.
   109  type OobService interface {
   110  	service.Event
   111  	AcceptInvitation(*outofband.Invitation, outofband.Options) (string, error)
   112  	SaveInvitation(*outofband.Invitation) error
   113  	Actions() ([]outofband.Action, error)
   114  	ActionContinue(string, outofband.Options) error
   115  	ActionStop(string, error) error
   116  }
   117  
   118  // Provider provides the dependencies for the client.
   119  type Provider interface {
   120  	ServiceEndpoint() string
   121  	Service(id string) (interface{}, error)
   122  	KMS() kms.KeyManager
   123  	KeyType() kms.KeyType
   124  	KeyAgreementType() kms.KeyType
   125  	MediaTypeProfiles() []string
   126  }
   127  
   128  // Client for the Out-Of-Band protocol:
   129  // https://github.com/hyperledger/aries-rfcs/blob/master/features/0434-outofband/README.md
   130  type Client struct {
   131  	service.Event
   132  	didDocSvcFunc     func(routerConnID string, accept []string) (*did.Service, error)
   133  	oobService        OobService
   134  	mediaTypeProfiles []string
   135  }
   136  
   137  // New returns a new Client for the Out-Of-Band protocol.
   138  func New(p Provider) (*Client, error) {
   139  	s, err := p.Service(outofband.Name)
   140  	if err != nil {
   141  		return nil, fmt.Errorf("failed to look up service %s : %w", outofband.Name, err)
   142  	}
   143  
   144  	oobSvc, ok := s.(OobService)
   145  	if !ok {
   146  		return nil, fmt.Errorf("failed to cast service %s as a dependency", outofband.Name)
   147  	}
   148  
   149  	mtp := p.MediaTypeProfiles()
   150  
   151  	if len(mtp) == 0 {
   152  		mtp = []string{transport.MediaTypeAIP2RFC0019Profile}
   153  	}
   154  
   155  	client := &Client{
   156  		Event:             oobSvc,
   157  		oobService:        oobSvc,
   158  		mediaTypeProfiles: mtp,
   159  	}
   160  
   161  	client.didDocSvcFunc = client.didServiceBlockFunc(p)
   162  
   163  	return client, nil
   164  }
   165  
   166  // CreateInvitation creates and saves an out-of-band invitation.
   167  // Services are required in the RFC, but optional in this implementation. If not provided, a default will be assigned.
   168  // TODO HandShakeProtocols are optional in the RFC and as arguments to this function.
   169  //  However, if not provided, a default will be assigned for you.
   170  func (c *Client) CreateInvitation(services []interface{}, opts ...MessageOption) (*Invitation, error) {
   171  	msg := &message{}
   172  
   173  	for _, opt := range opts {
   174  		opt(msg)
   175  	}
   176  
   177  	inv := &Invitation{
   178  		ID:        uuid.New().String(),
   179  		Type:      InvitationMsgType,
   180  		Label:     msg.Label,
   181  		Goal:      msg.Goal,
   182  		GoalCode:  msg.GoalCode,
   183  		Services:  services,
   184  		Accept:    msg.Accept,
   185  		Protocols: msg.HandshakeProtocols,
   186  		Requests:  msg.Attachments,
   187  	}
   188  
   189  	if len(inv.Accept) == 0 {
   190  		inv.Accept = c.mediaTypeProfiles
   191  	}
   192  
   193  	if len(inv.Services) == 0 {
   194  		svc, err := c.didDocSvcFunc(msg.RouterConnection(), inv.Accept)
   195  		if err != nil {
   196  			return nil, fmt.Errorf("failed to create a new inlined did doc service block : %w", err)
   197  		}
   198  
   199  		inv.Services = []interface{}{svc}
   200  	} else {
   201  		err := validateServices(inv.Services...)
   202  		if err != nil {
   203  			return nil, fmt.Errorf("invalid service: %w", err)
   204  		}
   205  	}
   206  
   207  	if len(inv.Protocols) == 0 {
   208  		// TODO should be injected into client
   209  		//  https://github.com/hyperledger/aries-framework-go/issues/1691
   210  		inv.Protocols = []string{didexchange.PIURI}
   211  	}
   212  
   213  	cast := outofband.Invitation(*inv)
   214  
   215  	err := c.oobService.SaveInvitation(&cast)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("failed to save outofband invitation : %w", err)
   218  	}
   219  
   220  	return inv, nil
   221  }
   222  
   223  // Actions returns unfinished actions for the async usage.
   224  func (c *Client) Actions() ([]Action, error) {
   225  	actions, err := c.oobService.Actions()
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	result := make([]Action, len(actions))
   231  	for i, action := range actions {
   232  		result[i] = Action(action)
   233  	}
   234  
   235  	return result, nil
   236  }
   237  
   238  // ActionContinue allows continuing with the protocol after an action event was triggered.
   239  func (c *Client) ActionContinue(piID, label string, opts ...MessageOption) error {
   240  	msg := &message{}
   241  
   242  	for _, opt := range opts {
   243  		opt(msg)
   244  	}
   245  
   246  	return c.oobService.ActionContinue(piID, &EventOptions{
   247  		Label:       label,
   248  		Connections: msg.RouterConnections,
   249  		ReuseAny:    msg.ReuseAnyConnection,
   250  		ReuseDID:    msg.ReuseConnection,
   251  	})
   252  }
   253  
   254  // ActionStop stops the protocol after an action event was triggered.
   255  func (c *Client) ActionStop(piID string, err error) error {
   256  	return c.oobService.ActionStop(piID, err)
   257  }
   258  
   259  // AcceptInvitation from another agent and return the ID of the new connection records.
   260  func (c *Client) AcceptInvitation(i *Invitation, myLabel string, opts ...MessageOption) (string, error) {
   261  	msg := &message{}
   262  
   263  	for _, opt := range opts {
   264  		opt(msg)
   265  	}
   266  
   267  	cast := outofband.Invitation(*i)
   268  
   269  	connID, err := c.oobService.AcceptInvitation(
   270  		&cast,
   271  		&EventOptions{
   272  			Label:       myLabel,
   273  			ReuseAny:    msg.ReuseAnyConnection,
   274  			ReuseDID:    msg.ReuseConnection,
   275  			Connections: msg.RouterConnections,
   276  		},
   277  	)
   278  	if err != nil {
   279  		return "", fmt.Errorf("out-of-band service failed to accept invitation : %w", err)
   280  	}
   281  
   282  	return connID, err
   283  }
   284  
   285  // WithLabel allows you to specify the label on the message.
   286  func WithLabel(l string) MessageOption {
   287  	return func(m *message) {
   288  		m.Label = l
   289  	}
   290  }
   291  
   292  // WithGoal allows you to specify the `goal` and `goalCode` for the message.
   293  func WithGoal(goal, goalCode string) MessageOption {
   294  	return func(m *message) {
   295  		m.Goal = goal
   296  		m.GoalCode = goalCode
   297  	}
   298  }
   299  
   300  // WithRouterConnections allows you to specify the router connections.
   301  func WithRouterConnections(conn ...string) MessageOption {
   302  	return func(m *message) {
   303  		for _, c := range conn {
   304  			// filters out empty connections
   305  			if c != "" {
   306  				m.RouterConnections = append(m.RouterConnections, c)
   307  			}
   308  		}
   309  	}
   310  }
   311  
   312  // WithHandshakeProtocols allows you to customize the handshake_protocols to include in the Invitation.
   313  func WithHandshakeProtocols(proto ...string) MessageOption {
   314  	return func(m *message) {
   315  		m.HandshakeProtocols = proto
   316  	}
   317  }
   318  
   319  // WithAttachments allows you to include attachments in the Invitation.
   320  func WithAttachments(a ...*decorator.Attachment) MessageOption {
   321  	return func(m *message) {
   322  		m.Attachments = a
   323  	}
   324  }
   325  
   326  // WithAccept will set the given media type profiles in the Invitation's `accept` property.
   327  // Only valid values from RFC 0044 are supported.
   328  func WithAccept(a ...string) MessageOption {
   329  	return func(m *message) {
   330  		m.Accept = a
   331  	}
   332  }
   333  
   334  // ReuseAnyConnection is used when accepting an invitation with either AcceptInvitation or ActionContinue.
   335  // The `services` array will be scanned until it finds a recognized DID entry and send a `handshake-reuse` message
   336  // to its did-communication service endpoint.
   337  // Cannot be used together with ReuseConnection.
   338  func ReuseAnyConnection() MessageOption {
   339  	return func(m *message) {
   340  		m.ReuseAnyConnection = true
   341  	}
   342  }
   343  
   344  // ReuseConnection is used when accepting an invitation with either AcceptInvitation or ActionContinue.
   345  // 'did' must be an entry in the Invitation's 'services' array. A `handshake-reuse` message is sent to its
   346  // did-communication service endpoint.
   347  // Cannot be used together with ReuseAnyConnection.
   348  func ReuseConnection(theirDID string) MessageOption {
   349  	return func(m *message) {
   350  		m.ReuseConnection = theirDID
   351  	}
   352  }
   353  
   354  func validateServices(svcs ...interface{}) error {
   355  	for i := range svcs {
   356  		switch svc := svcs[i].(type) {
   357  		case string:
   358  			_, err := did.Parse(svc)
   359  			if err != nil {
   360  				return fmt.Errorf("invalid DID [%s]: %w", svc, err)
   361  			}
   362  		case did.Service, *did.Service:
   363  		default:
   364  			return fmt.Errorf("unsupported service data type: %+v", svc)
   365  		}
   366  	}
   367  
   368  	return nil
   369  }
   370  
   371  // DidDocServiceFunc returns a function that returns a DID doc `service` entry.
   372  // Used when no service entries are specified when creating messages.
   373  //nolint:funlen,gocyclo
   374  func (c *Client) didServiceBlockFunc(p Provider) func(routerConnID string, accept []string) (*did.Service, error) {
   375  	return func(routerConnID string, accept []string) (*did.Service, error) {
   376  		var (
   377  			keyType            kms.KeyType
   378  			didCommServiceType string
   379  			sp                 model.Endpoint
   380  		)
   381  
   382  		useDIDCommV2 := isDIDCommV2(accept)
   383  		// TODO https://github.com/hyperledger/aries-framework-go/issues/623 'alias' should be passed as arg and persisted
   384  		//  with connection record
   385  		if useDIDCommV2 {
   386  			keyType = p.KeyAgreementType()
   387  			didCommServiceType = vdr.DIDCommV2ServiceType
   388  			sp = model.NewDIDCommV2Endpoint([]model.DIDCommV2Endpoint{{
   389  				URI:    p.ServiceEndpoint(),
   390  				Accept: p.MediaTypeProfiles(),
   391  			}})
   392  		} else {
   393  			keyType = p.KeyType()
   394  			didCommServiceType = vdr.DIDCommServiceType
   395  			sp = model.NewDIDCommV1Endpoint(p.ServiceEndpoint())
   396  		}
   397  
   398  		if string(keyType) == "" {
   399  			keyType = kms.ED25519Type
   400  		}
   401  
   402  		_, pubKey, err := p.KMS().CreateAndExportPubKeyBytes(keyType)
   403  		if err != nil {
   404  			return nil, fmt.Errorf("didServiceBlockFunc: failed to create and extract public SigningKey bytes: %w", err)
   405  		}
   406  
   407  		s, err := p.Service(mediator.Coordination)
   408  		if err != nil {
   409  			return nil, fmt.Errorf("didServiceBlockFunc: failed Coordinate Mediate service: %w", err)
   410  		}
   411  
   412  		routeSvc, ok := s.(mediator.ProtocolService)
   413  		if !ok {
   414  			return nil, errors.New("didServiceBlockFunc: cast service to Route Service failed")
   415  		}
   416  
   417  		didKey, err := kmsdidkey.BuildDIDKeyByKeyType(pubKey, keyType)
   418  		if err != nil {
   419  			return nil, fmt.Errorf("didServiceBlockFunc: failed to build did:key for key type '%v': %w", keyType, err)
   420  		}
   421  
   422  		if routerConnID == "" {
   423  			return &did.Service{
   424  				ID:              uuid.New().String(),
   425  				Type:            didCommServiceType,
   426  				RecipientKeys:   []string{didKey},
   427  				ServiceEndpoint: sp,
   428  			}, nil
   429  		}
   430  
   431  		// get the route configs
   432  		serviceEndpoint, routingKeys, err := mediator.GetRouterConfig(routeSvc, routerConnID, p.ServiceEndpoint())
   433  		if err != nil {
   434  			return nil, fmt.Errorf("didServiceBlockFunc: create invitation - fetch router config : %w", err)
   435  		}
   436  
   437  		if err = mediator.AddKeyToRouter(routeSvc, routerConnID, didKey); err != nil {
   438  			return nil, fmt.Errorf("didServiceBlockFunc: create invitation - failed to add key to the router : %w", err)
   439  		}
   440  
   441  		var svc *did.Service
   442  
   443  		if useDIDCommV2 {
   444  			sp = model.NewDIDCommV2Endpoint([]model.DIDCommV2Endpoint{{
   445  				URI:         serviceEndpoint,
   446  				Accept:      accept,
   447  				RoutingKeys: routingKeys,
   448  			}})
   449  			svc = &did.Service{
   450  				ID:              uuid.New().String(),
   451  				Type:            didCommServiceType,
   452  				RecipientKeys:   []string{didKey},
   453  				ServiceEndpoint: sp,
   454  			}
   455  		} else {
   456  			sp = model.NewDIDCommV1Endpoint(serviceEndpoint)
   457  			svc = &did.Service{
   458  				ID:              uuid.New().String(),
   459  				Type:            didCommServiceType,
   460  				RecipientKeys:   []string{didKey},
   461  				ServiceEndpoint: sp,
   462  				RoutingKeys:     routingKeys,
   463  				Accept:          accept,
   464  			}
   465  		}
   466  
   467  		return svc, nil
   468  	}
   469  }
   470  
   471  func isDIDCommV2(accept []string) bool {
   472  	for _, a := range accept {
   473  		switch a {
   474  		case transport.MediaTypeDIDCommV2Profile, transport.MediaTypeAIP2RFC0587Profile,
   475  			transport.MediaTypeV2EncryptedEnvelope, transport.MediaTypeV2EncryptedEnvelopeV1PlaintextPayload,
   476  			transport.MediaTypeV1EncryptedEnvelope:
   477  			return true
   478  		}
   479  	}
   480  
   481  	return false
   482  }