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 }