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

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package didexchange
     8  
     9  import (
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  
    14  	"github.com/google/uuid"
    15  
    16  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
    17  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange"
    18  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator"
    19  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/transport"
    20  	"github.com/hyperledger/aries-framework-go/pkg/doc/did"
    21  	"github.com/hyperledger/aries-framework-go/pkg/doc/util/kmsdidkey"
    22  	"github.com/hyperledger/aries-framework-go/pkg/kms"
    23  	"github.com/hyperledger/aries-framework-go/pkg/store/connection"
    24  	"github.com/hyperledger/aries-framework-go/pkg/vdr/fingerprint"
    25  	"github.com/hyperledger/aries-framework-go/spi/storage"
    26  )
    27  
    28  const (
    29  	// InvitationMsgType defines the did-exchange invite message type.
    30  	InvitationMsgType = didexchange.InvitationMsgType
    31  	// RequestMsgType defines the did-exchange request message type.
    32  	RequestMsgType = didexchange.RequestMsgType
    33  	// ProtocolName is the framework's friendly name for the did exchange protocol.
    34  	ProtocolName = didexchange.DIDExchange
    35  )
    36  
    37  // ErrConnectionNotFound is returned when connection not found.
    38  var ErrConnectionNotFound = errors.New("connection not found")
    39  
    40  type options struct {
    41  	routerConnections  []string
    42  	routerConnectionID string
    43  	keyType            kms.KeyType
    44  }
    45  
    46  func applyOptions(args ...Opt) *options {
    47  	opts := &options{}
    48  
    49  	for i := range args {
    50  		args[i](opts)
    51  	}
    52  
    53  	return opts
    54  }
    55  
    56  // Opt represents option function.
    57  type Opt func(*options)
    58  
    59  // InvOpt represents option for the CreateInvitation function.
    60  type InvOpt Opt
    61  
    62  // WithRouterConnectionID allows you to specify the router connection ID.
    63  func WithRouterConnectionID(conn string) InvOpt {
    64  	return func(opts *options) {
    65  		opts.routerConnectionID = conn
    66  	}
    67  }
    68  
    69  // WithRouterConnections allows you to specify the router connections.
    70  func WithRouterConnections(conns ...string) Opt {
    71  	return func(opts *options) {
    72  		for _, conn := range conns {
    73  			// filters out empty connections
    74  			if conn != "" {
    75  				opts.routerConnections = append(opts.routerConnections, conn)
    76  			}
    77  		}
    78  	}
    79  }
    80  
    81  // WithKeyType sets the key type to use in the didexchange invitation. DIDcomm v1 requires ED25519 key type, while
    82  // DIDComm V2 requires either NISTP(256/384/521)ECDHKW key type or X25519ECDHKW key type.
    83  func WithKeyType(keyType kms.KeyType) InvOpt {
    84  	return func(opts *options) {
    85  		opts.keyType = keyType
    86  	}
    87  }
    88  
    89  // provider contains dependencies for the DID exchange protocol and is typically created by using aries.Context().
    90  type provider interface {
    91  	Service(id string) (interface{}, error)
    92  	KMS() kms.KeyManager
    93  	ServiceEndpoint() string
    94  	StorageProvider() storage.Provider
    95  	ProtocolStateStorageProvider() storage.Provider
    96  	KeyType() kms.KeyType
    97  	KeyAgreementType() kms.KeyType
    98  	MediaTypeProfiles() []string
    99  }
   100  
   101  // Client enable access to didexchange api.
   102  type Client struct {
   103  	service.Event
   104  	didexchangeSvc    protocolService
   105  	routeSvc          mediator.ProtocolService
   106  	kms               kms.KeyManager
   107  	serviceEndpoint   string
   108  	connectionStore   *connection.Recorder
   109  	keyType           kms.KeyType
   110  	keyAgreementType  kms.KeyType
   111  	mediaTypeProfiles []string
   112  }
   113  
   114  // protocolService defines DID Exchange service.
   115  type protocolService interface {
   116  	// DIDComm service
   117  	service.DIDComm
   118  
   119  	// Accepts/Approves exchange request
   120  	AcceptExchangeRequest(connectionID, publicDID, label string, routerConnections []string) error
   121  
   122  	// Accepts/Approves exchange invitation
   123  	AcceptInvitation(connectionID, publicDID, label string, routerConnections []string) error
   124  
   125  	// CreateImplicitInvitation creates implicit invitation. Inviter DID is required, invitee DID is optional.
   126  	// If invitee DID is not provided new peer DID will be created for implicit invitation exchange request.
   127  	CreateImplicitInvitation(inviterLabel, inviterDID, inviteeLabel,
   128  		inviteeDID string, routerConnections []string) (string, error)
   129  
   130  	// CreateConnection saves the connection record.
   131  	CreateConnection(*connection.Record, *did.Doc) error
   132  }
   133  
   134  // New return new instance of didexchange client.
   135  func New(ctx provider) (*Client, error) {
   136  	svc, err := ctx.Service(didexchange.DIDExchange)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	didexchangeSvc, ok := svc.(protocolService)
   142  	if !ok {
   143  		return nil, errors.New("cast service to DIDExchange Service failed")
   144  	}
   145  
   146  	s, err := ctx.Service(mediator.Coordination)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	routeSvc, ok := s.(mediator.ProtocolService)
   152  	if !ok {
   153  		return nil, errors.New("cast service to Route Service failed")
   154  	}
   155  
   156  	connectionStore, err := connection.NewRecorder(ctx)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	keyType := ctx.KeyType()
   162  	if keyType == "" {
   163  		keyType = kms.ED25519Type
   164  	}
   165  
   166  	keyAgreementType := ctx.KeyAgreementType()
   167  	if keyAgreementType == "" {
   168  		keyAgreementType = kms.X25519ECDHKWType
   169  	}
   170  
   171  	mtp := ctx.MediaTypeProfiles()
   172  	if len(mtp) == 0 {
   173  		mtp = []string{transport.MediaTypeRFC0019EncryptedEnvelope}
   174  	}
   175  
   176  	return &Client{
   177  		Event:             didexchangeSvc,
   178  		didexchangeSvc:    didexchangeSvc,
   179  		routeSvc:          routeSvc,
   180  		kms:               ctx.KMS(),
   181  		serviceEndpoint:   ctx.ServiceEndpoint(),
   182  		connectionStore:   connectionStore,
   183  		keyType:           keyType,
   184  		keyAgreementType:  keyAgreementType,
   185  		mediaTypeProfiles: mtp,
   186  	}, nil
   187  }
   188  
   189  // CreateInvitation creates an invitation. New key pair will be generated and did:key encoded public key will be
   190  // used as basis for invitation. This invitation will be stored so client can cross-reference this invitation during
   191  // did exchange protocol.
   192  //nolint:funlen,gocyclo
   193  func (c *Client) CreateInvitation(label string, args ...InvOpt) (*Invitation, error) {
   194  	opts := &options{}
   195  
   196  	for i := range args {
   197  		args[i](opts)
   198  	}
   199  
   200  	keyType := c.keyType
   201  
   202  	if opts.keyType != "" {
   203  		keyType = opts.keyType
   204  	} else {
   205  		for _, mediaType := range c.mediaTypeProfiles {
   206  			if mediaType == transport.MediaTypeDIDCommV2Profile || mediaType == transport.MediaTypeAIP2RFC0587Profile {
   207  				keyType = c.keyAgreementType
   208  
   209  				break
   210  			}
   211  		}
   212  	}
   213  
   214  	// TODO https://github.com/hyperledger/aries-framework-go/issues/623 'alias' should be passed as arg and persisted
   215  	//  with connection record
   216  	_, pubKey, err := c.kms.CreateAndExportPubKeyBytes(keyType)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("createInvitation: failed to extract public SigningKey bytes from handle: %w", err)
   219  	}
   220  
   221  	var didKey string
   222  
   223  	switch keyType {
   224  	case kms.ED25519Type:
   225  		didKey, _ = fingerprint.CreateDIDKey(pubKey)
   226  	default:
   227  		didKey, err = kmsdidkey.BuildDIDKeyByKeyType(pubKey, keyType)
   228  		if err != nil {
   229  			return nil, fmt.Errorf("createInvitation: failed to build did:key by key type: %w", err)
   230  		}
   231  	}
   232  
   233  	var (
   234  		serviceEndpoint = c.serviceEndpoint
   235  		routingKeys     []string
   236  	)
   237  
   238  	if opts.routerConnectionID != "" {
   239  		// get the route configs
   240  		serviceEndpoint, routingKeys, err = mediator.GetRouterConfig(c.routeSvc,
   241  			opts.routerConnectionID, c.serviceEndpoint)
   242  		if err != nil {
   243  			return nil, fmt.Errorf("createInvitation: getRouterConfig: %w", err)
   244  		}
   245  
   246  		if err = mediator.AddKeyToRouter(c.routeSvc, opts.routerConnectionID, didKey); err != nil {
   247  			return nil, fmt.Errorf("createInvitation: AddKeyToRouter: %w", err)
   248  		}
   249  	}
   250  
   251  	invitation := &didexchange.Invitation{
   252  		ID:              uuid.New().String(),
   253  		Label:           label,
   254  		RecipientKeys:   []string{didKey},
   255  		ServiceEndpoint: serviceEndpoint,
   256  		Type:            didexchange.InvitationMsgType,
   257  		RoutingKeys:     routingKeys,
   258  	}
   259  
   260  	err = c.connectionStore.SaveInvitation(invitation.ID, invitation)
   261  	if err != nil {
   262  		return nil, fmt.Errorf("createInvitation: failed to save invitation: %w", err)
   263  	}
   264  
   265  	return &Invitation{invitation}, nil
   266  }
   267  
   268  // CreateInvitationWithDID creates an invitation with specified public DID. This invitation will be stored
   269  // so client can cross reference this invitation during did exchange protocol.
   270  func (c *Client) CreateInvitationWithDID(label, publicDID string) (*Invitation, error) {
   271  	invitation := &didexchange.Invitation{
   272  		ID:    uuid.New().String(),
   273  		Label: label,
   274  		DID:   publicDID,
   275  		Type:  didexchange.InvitationMsgType,
   276  	}
   277  
   278  	err := c.connectionStore.SaveInvitation(invitation.ID, invitation)
   279  	if err != nil {
   280  		return nil, fmt.Errorf("createInvitationWithDID: failed to save invitation with DID: %w", err)
   281  	}
   282  
   283  	return &Invitation{invitation}, nil
   284  }
   285  
   286  // HandleInvitation handle incoming invitation and returns the connectionID that can be used to query the state
   287  // of did exchange protocol. Upon successful completion of did exchange protocol connection details will be used
   288  // for securing communication between agents.
   289  func (c *Client) HandleInvitation(invitation *Invitation) (string, error) {
   290  	payload, err := json.Marshal(invitation)
   291  	if err != nil {
   292  		return "", fmt.Errorf("handleInvitation: failed marshal invitation: %w", err)
   293  	}
   294  
   295  	msg, err := service.ParseDIDCommMsgMap(payload)
   296  	if err != nil {
   297  		return "", fmt.Errorf("handleInvitation: failed to create DIDCommMsg: %w", err)
   298  	}
   299  
   300  	connectionID, err := c.didexchangeSvc.HandleInbound(msg, service.EmptyDIDCommContext())
   301  	if err != nil {
   302  		return "", fmt.Errorf("handleInvitation: failed from didexchange service handle: %w", err)
   303  	}
   304  
   305  	return connectionID, nil
   306  }
   307  
   308  // TODO https://github.com/hyperledger/aries-framework-go/issues/754 - e.Continue v Explicit API call for action events
   309  
   310  // AcceptInvitation accepts/approves exchange invitation. This call is not used if auto execute is setup
   311  // for this client (see package example for more details about how to setup auto execute).
   312  func (c *Client) AcceptInvitation(connectionID, publicDID, label string, args ...Opt) error {
   313  	opts := applyOptions(args...)
   314  
   315  	if err := c.didexchangeSvc.AcceptInvitation(connectionID, publicDID, label, opts.routerConnections); err != nil {
   316  		return fmt.Errorf("did exchange client - accept exchange invitation: %w", err)
   317  	}
   318  
   319  	return nil
   320  }
   321  
   322  // AcceptExchangeRequest accepts/approves exchange request. This call is not used if auto execute is setup
   323  // for this client (see package example for more details about how to setup auto execute).
   324  func (c *Client) AcceptExchangeRequest(connectionID, publicDID, label string, args ...Opt) error {
   325  	err := c.didexchangeSvc.AcceptExchangeRequest(connectionID, publicDID, label,
   326  		applyOptions(args...).routerConnections)
   327  	if err != nil {
   328  		return fmt.Errorf("did exchange client - accept exchange request: %w", err)
   329  	}
   330  
   331  	return nil
   332  }
   333  
   334  // CreateImplicitInvitation enables invitee to create and send an exchange request using inviter public DID.
   335  func (c *Client) CreateImplicitInvitation(inviterLabel, inviterDID string, args ...Opt) (string, error) {
   336  	return c.didexchangeSvc.CreateImplicitInvitation(inviterLabel, inviterDID,
   337  		"", "", applyOptions(args...).routerConnections)
   338  }
   339  
   340  // CreateImplicitInvitationWithDID enables invitee to create implicit invitation using inviter and invitee public DID.
   341  func (c *Client) CreateImplicitInvitationWithDID(inviter, invitee *DIDInfo) (string, error) {
   342  	if inviter == nil || invitee == nil {
   343  		return "", errors.New("missing inviter and/or invitee public DID(s)")
   344  	}
   345  
   346  	return c.didexchangeSvc.CreateImplicitInvitation(inviter.Label, inviter.DID, invitee.Label, invitee.DID, nil)
   347  }
   348  
   349  // QueryConnections queries connections matching given criteria(parameters).
   350  func (c *Client) QueryConnections(request *QueryConnectionsParams) ([]*Connection, error) { //nolint: gocyclo
   351  	// TODO https://github.com/hyperledger/aries-framework-go/issues/655 - query all connections from all criteria and
   352  	//  also results needs to be paged.
   353  	records, err := c.connectionStore.QueryConnectionRecords()
   354  	if err != nil {
   355  		return nil, fmt.Errorf("failed query connections: %w", err)
   356  	}
   357  
   358  	var result []*Connection
   359  
   360  	for _, record := range records {
   361  		if request.State != "" && request.State != record.State {
   362  			continue
   363  		}
   364  
   365  		if request.InvitationID != "" && request.InvitationID != record.InvitationID {
   366  			continue
   367  		}
   368  
   369  		if request.ParentThreadID != "" && request.ParentThreadID != record.ParentThreadID {
   370  			continue
   371  		}
   372  
   373  		if request.MyDID != "" && request.MyDID != record.MyDID {
   374  			continue
   375  		}
   376  
   377  		if request.TheirDID != "" && request.TheirDID != record.TheirDID {
   378  			continue
   379  		}
   380  
   381  		result = append(result, &Connection{Record: record})
   382  	}
   383  
   384  	return result, nil
   385  }
   386  
   387  // GetConnection fetches single connection record for given id.
   388  func (c *Client) GetConnection(connectionID string) (*Connection, error) {
   389  	conn, err := c.connectionStore.GetConnectionRecord(connectionID)
   390  	if err != nil {
   391  		if errors.Is(err, storage.ErrDataNotFound) {
   392  			return nil, ErrConnectionNotFound
   393  		}
   394  
   395  		return nil, fmt.Errorf("cannot fetch state from store: connectionid=%s err=%w", connectionID, err)
   396  	}
   397  
   398  	return &Connection{
   399  		conn,
   400  	}, nil
   401  }
   402  
   403  // GetConnectionAtState fetches connection record for connection id at particular state.
   404  func (c *Client) GetConnectionAtState(connectionID, stateID string) (*Connection, error) {
   405  	conn, err := c.connectionStore.GetConnectionRecordAtState(connectionID, stateID)
   406  	if err != nil {
   407  		if errors.Is(err, storage.ErrDataNotFound) {
   408  			return nil, ErrConnectionNotFound
   409  		}
   410  
   411  		return nil, fmt.Errorf("cannot fetch state from store: connectionid=%s err=%w", connectionID, err)
   412  	}
   413  
   414  	return &Connection{
   415  		conn,
   416  	}, nil
   417  }
   418  
   419  // CreateConnection creates a new connection between myDID and theirDID and returns the connectionID.
   420  func (c *Client) CreateConnection(myDID string, theirDID *did.Doc, options ...ConnectionOption) (string, error) {
   421  	conn := &Connection{&connection.Record{
   422  		ConnectionID: uuid.New().String(),
   423  		State:        connection.StateNameCompleted,
   424  		TheirDID:     theirDID.ID,
   425  		MyDID:        myDID,
   426  		Namespace:    connection.MyNSPrefix,
   427  	}}
   428  
   429  	for i := range options {
   430  		options[i](conn)
   431  	}
   432  
   433  	destination, err := service.CreateDestination(theirDID)
   434  	if err != nil {
   435  		return "", fmt.Errorf("createConnection: failed to create destination: %w", err)
   436  	}
   437  
   438  	conn.ServiceEndPoint = destination.ServiceEndpoint
   439  	conn.RecipientKeys = destination.RecipientKeys
   440  	conn.RoutingKeys = destination.RoutingKeys
   441  	conn.MediaTypeProfiles = destination.MediaTypeProfiles
   442  
   443  	err = c.didexchangeSvc.CreateConnection(conn.Record, theirDID)
   444  	if err != nil {
   445  		return "", fmt.Errorf("createConnection: err: %w", err)
   446  	}
   447  
   448  	return conn.ConnectionID, nil
   449  }
   450  
   451  // RemoveConnection removes connection record for given id.
   452  func (c *Client) RemoveConnection(connectionID string) error {
   453  	err := c.connectionStore.RemoveConnection(connectionID)
   454  	if err != nil {
   455  		return fmt.Errorf("cannot remove connection from the store: err=%w", err)
   456  	}
   457  
   458  	return nil
   459  }
   460  
   461  // ConnectionOption allows you to customize details of the connection record.
   462  type ConnectionOption func(*Connection)
   463  
   464  // WithTheirLabel sets TheirLabel on the connection record.
   465  func WithTheirLabel(l string) ConnectionOption {
   466  	return func(c *Connection) {
   467  		c.TheirLabel = l
   468  	}
   469  }
   470  
   471  // WithThreadID sets ThreadID on the connection record.
   472  func WithThreadID(thid string) ConnectionOption {
   473  	return func(c *Connection) {
   474  		c.ThreadID = thid
   475  	}
   476  }
   477  
   478  // WithParentThreadID sets ParentThreadID on the connection record.
   479  func WithParentThreadID(pthid string) ConnectionOption {
   480  	return func(c *Connection) {
   481  		c.ParentThreadID = pthid
   482  	}
   483  }
   484  
   485  // WithInvitationID sets InvitationID on the connection record.
   486  func WithInvitationID(id string) ConnectionOption {
   487  	return func(c *Connection) {
   488  		c.InvitationID = id
   489  	}
   490  }
   491  
   492  // WithInvitationDID sets InvitationDID on the connection record.
   493  func WithInvitationDID(didID string) ConnectionOption {
   494  	return func(c *Connection) {
   495  		c.InvitationDID = didID
   496  	}
   497  }
   498  
   499  // WithImplicit sets Implicit on the connection record.
   500  func WithImplicit(i bool) ConnectionOption {
   501  	return func(c *Connection) {
   502  		c.Implicit = i
   503  	}
   504  }