github.com/hyperledger/aries-framework-go@v0.3.2/pkg/wallet/didcomm.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 Copyright Avast Software. All Rights Reserved. 4 5 SPDX-License-Identifier: Apache-2.0 6 */ 7 8 package wallet 9 10 import ( 11 "context" 12 "fmt" 13 "time" 14 15 "github.com/google/uuid" 16 17 "github.com/hyperledger/aries-framework-go/pkg/client/didexchange" 18 "github.com/hyperledger/aries-framework-go/pkg/client/issuecredential" 19 "github.com/hyperledger/aries-framework-go/pkg/client/outofband" 20 "github.com/hyperledger/aries-framework-go/pkg/client/outofbandv2" 21 "github.com/hyperledger/aries-framework-go/pkg/client/presentproof" 22 "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/model" 23 "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" 24 "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" 25 didexchangeSvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/didexchange" 26 issuecredentialsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" 27 outofbandv2svc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/outofbandv2" 28 "github.com/hyperledger/aries-framework-go/pkg/kms" 29 "github.com/hyperledger/aries-framework-go/pkg/store/connection" 30 "github.com/hyperledger/aries-framework-go/spi/storage" 31 ) 32 33 // miscellaneous constants. 34 const ( 35 msgEventBufferSize = 10 36 ldJSONMimeType = "application/ld+json" 37 38 // protocol states. 39 stateNameAbandoned = "abandoned" 40 stateNameAbandoning = "abandoning" 41 stateNameDone = "done" 42 43 // timeout constants. 44 defaultDIDExchangeTimeOut = 120 * time.Second 45 defaultWaitForRequestPresentationTimeOut = 120 * time.Second 46 defaultWaitForPresentProofDone = 120 * time.Second 47 retryDelay = 500 * time.Millisecond 48 ) 49 50 // didCommProvider to be used only if wallet needs to be participated in DIDComm operation. 51 // TODO: using wallet KMS instead of provider KMS. 52 // TODO: reconcile Protocol storage with wallet store. 53 type didCommProvider interface { 54 KMS() kms.KeyManager 55 ServiceEndpoint() string 56 ProtocolStateStorageProvider() storage.Provider 57 Service(id string) (interface{}, error) 58 KeyType() kms.KeyType 59 KeyAgreementType() kms.KeyType 60 } 61 62 type combinedDidCommWalletProvider interface { 63 provider 64 didCommProvider 65 } 66 67 // DidComm enables access to verifiable credential wallet features. 68 type DidComm struct { 69 // wallet implementation 70 wallet *Wallet 71 72 // present proof client 73 presentProofClient *presentproof.Client 74 75 // issue credential client 76 issueCredentialClient *issuecredential.Client 77 78 // out of band client 79 oobClient *outofband.Client 80 81 // out of band v2 client 82 oobV2Client *outofbandv2.Client 83 84 // did-exchange client 85 didexchangeClient *didexchange.Client 86 87 // connection lookup 88 connectionLookup *connection.Lookup 89 } 90 91 // NewDidComm returns new verifiable credential wallet for given user. 92 // returns error if wallet profile is not found. 93 // To create a new wallet profile, use `CreateProfile()`. 94 // To update an existing profile, use `UpdateProfile()`. 95 func NewDidComm(wallet *Wallet, ctx combinedDidCommWalletProvider) (*DidComm, error) { 96 presentProofClient, err := presentproof.New(ctx) 97 if err != nil { 98 return nil, fmt.Errorf("failed to initialize present proof client: %w", err) 99 } 100 101 issueCredentialClient, err := issuecredential.New(ctx) 102 if err != nil { 103 return nil, fmt.Errorf("failed to initialize issue credential client: %w", err) 104 } 105 106 oobClient, err := outofband.New(ctx) 107 if err != nil { 108 return nil, fmt.Errorf("failed to initialize out-of-band client: %w", err) 109 } 110 111 oobV2Client, err := outofbandv2.New(ctx) 112 if err != nil { 113 return nil, fmt.Errorf("failed to initialize out-of-band v2 client: %w", err) 114 } 115 116 connectionLookup, err := connection.NewLookup(ctx) 117 if err != nil { 118 return nil, fmt.Errorf("failed to initialize connection lookup: %w", err) 119 } 120 121 didexchangeClient, err := didexchange.New(ctx) 122 if err != nil { 123 return nil, fmt.Errorf("failed to initialize didexchange client: %w", err) 124 } 125 126 return &DidComm{ 127 wallet: wallet, 128 presentProofClient: presentProofClient, 129 issueCredentialClient: issueCredentialClient, 130 oobClient: oobClient, 131 oobV2Client: oobV2Client, 132 didexchangeClient: didexchangeClient, 133 connectionLookup: connectionLookup, 134 }, nil 135 } 136 137 // Connect accepts out-of-band invitations and performs DID exchange. 138 // 139 // Args: 140 // - authToken: authorization for performing create key pair operation. 141 // - invitation: out-of-band invitation. 142 // - options: connection options. 143 // 144 // Returns: 145 // - connection ID if DID exchange is successful. 146 // - error if operation false. 147 // 148 func (c *DidComm) Connect(authToken string, invitation *outofband.Invitation, options ...ConnectOptions) (string, error) { //nolint: lll 149 statusCh := make(chan service.StateMsg, msgEventBufferSize) 150 151 err := c.didexchangeClient.RegisterMsgEvent(statusCh) 152 if err != nil { 153 return "", fmt.Errorf("failed to register msg event : %w", err) 154 } 155 156 defer func() { 157 e := c.didexchangeClient.UnregisterMsgEvent(statusCh) 158 if e != nil { 159 logger.Warnf("Failed to unregister msg event for connect: %w", e) 160 } 161 }() 162 163 opts := &connectOpts{} 164 for _, opt := range options { 165 opt(opts) 166 } 167 168 connID, err := c.oobClient.AcceptInvitation(invitation, opts.Label, getOobMessageOptions(opts)...) 169 if err != nil { 170 return "", fmt.Errorf("failed to accept invitation : %w", err) 171 } 172 173 if opts.timeout == 0 { 174 opts.timeout = defaultDIDExchangeTimeOut 175 } 176 177 ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) 178 defer cancel() 179 180 err = waitForConnect(ctx, statusCh, connID) 181 if err != nil { 182 return "", fmt.Errorf("wallet connect failed : %w", err) 183 } 184 185 return connID, nil 186 } 187 188 // ProposePresentation accepts out-of-band invitation and sends message proposing presentation 189 // from wallet to relying party. 190 // https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposepresentation 191 // 192 // Currently Supporting 193 // [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) 194 // 195 // Args: 196 // - authToken: authorization for performing operation. 197 // - invitation: out-of-band invitation from relying party. 198 // - options: options for accepting invitation and send propose presentation message. 199 // 200 // Returns: 201 // - DIDCommMsgMap containing request presentation message if operation is successful. 202 // - error if operation fails. 203 // 204 func (c *DidComm) ProposePresentation(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll 205 opts := &initiateInteractionOpts{} 206 for _, opt := range options { 207 opt(opts) 208 } 209 210 var ( 211 connID string 212 err error 213 ) 214 215 switch invitation.Version() { 216 default: 217 fallthrough 218 case service.V1: 219 connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) 220 if err != nil { 221 return nil, fmt.Errorf("failed to perform did connection : %w", err) 222 } 223 case service.V2: 224 connOpts := &connectOpts{} 225 226 for _, opt := range opts.connectOpts { 227 opt(connOpts) 228 } 229 230 connID, err = c.oobV2Client.AcceptInvitation( 231 invitation.AsV2(), 232 outofbandv2svc.WithRouterConnections(connOpts.Connections), 233 ) 234 if err != nil { 235 return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) 236 } 237 } 238 239 connRecord, err := c.connectionLookup.GetConnectionRecord(connID) 240 if err != nil { 241 return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) 242 } 243 244 opts = prepareInteractionOpts(connRecord, opts) 245 246 _, err = c.presentProofClient.SendProposePresentation(&presentproof.ProposePresentation{}, connRecord) 247 if err != nil { 248 return nil, fmt.Errorf("failed to propose presentation from wallet: %w", err) 249 } 250 251 ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) 252 defer cancel() 253 254 return c.waitForRequestPresentation(ctx, connRecord) 255 } 256 257 // PresentProof sends message present proof message from wallet to relying party. 258 // https://w3c-ccg.github.io/universal-wallet-interop-spec/#presentproof 259 // 260 // Currently Supporting 261 // [0454-present-proof-v2](https://github.com/hyperledger/aries-rfcs/tree/master/features/0454-present-proof-v2) 262 // 263 // Args: 264 // - authToken: authorization for performing operation. 265 // - thID: thread ID (action ID) of request presentation. 266 // - presentProofFrom: presentation to be sent. 267 // 268 // Returns: 269 // - Credential interaction status containing status, redirectURL. 270 // - error if operation fails. 271 // 272 func (c *DidComm) PresentProof(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll 273 opts := &concludeInteractionOpts{} 274 275 for _, option := range options { 276 option(opts) 277 } 278 279 var presentation interface{} 280 if opts.presentation != nil { 281 presentation = opts.presentation 282 } else { 283 presentation = opts.rawPresentation 284 } 285 286 err := c.presentProofClient.AcceptRequestPresentation(thID, &presentproof.Presentation{ 287 Attachments: []decorator.GenericAttachment{{ 288 ID: uuid.New().String(), 289 Data: decorator.AttachmentData{ 290 JSON: presentation, 291 }, 292 }}, 293 }, nil) 294 if err != nil { 295 return nil, err 296 } 297 298 // wait for ack or problem-report. 299 if opts.waitForDone { 300 statusCh := make(chan service.StateMsg, msgEventBufferSize) 301 302 err = c.presentProofClient.RegisterMsgEvent(statusCh) 303 if err != nil { 304 return nil, fmt.Errorf("failed to register present proof msg event : %w", err) 305 } 306 307 defer func() { 308 e := c.presentProofClient.UnregisterMsgEvent(statusCh) 309 if e != nil { 310 logger.Warnf("Failed to unregister msg event for present proof: %w", e) 311 } 312 }() 313 314 ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) 315 defer cancel() 316 317 return waitCredInteractionCompletion(ctx, statusCh, thID) 318 } 319 320 return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil 321 } 322 323 // ProposeCredential sends propose credential message from wallet to issuer. 324 // https://w3c-ccg.github.io/universal-wallet-interop-spec/#proposecredential 325 // 326 // Currently Supporting : 0453-issueCredentialV2 327 // https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md 328 // 329 // Args: 330 // - authToken: authorization for performing operation. 331 // - invitation: out-of-band invitation from issuer. 332 // - options: options for accepting invitation and send propose credential message. 333 // 334 // Returns: 335 // - DIDCommMsgMap containing offer credential message if operation is successful. 336 // - error if operation fails. 337 // 338 func (c *DidComm) ProposeCredential(authToken string, invitation *GenericInvitation, options ...InitiateInteractionOption) (*service.DIDCommMsgMap, error) { //nolint: lll 339 opts := &initiateInteractionOpts{} 340 for _, opt := range options { 341 opt(opts) 342 } 343 344 var ( 345 connID string 346 err error 347 ) 348 349 switch invitation.Version() { 350 default: 351 fallthrough 352 case service.V1: 353 connID, err = c.Connect(authToken, (*outofband.Invitation)(invitation.AsV1()), opts.connectOpts...) 354 if err != nil { 355 return nil, fmt.Errorf("failed to perform did connection : %w", err) 356 } 357 case service.V2: 358 connOpts := &connectOpts{} 359 360 for _, opt := range opts.connectOpts { 361 opt(connOpts) 362 } 363 364 connID, err = c.oobV2Client.AcceptInvitation( 365 invitation.AsV2(), 366 outofbandv2svc.WithRouterConnections(connOpts.Connections), 367 ) 368 if err != nil { 369 return nil, fmt.Errorf("failed to accept OOB v2 invitation : %w", err) 370 } 371 } 372 373 connRecord, err := c.connectionLookup.GetConnectionRecord(connID) 374 if err != nil { 375 return nil, fmt.Errorf("failed to lookup connection for propose presentation : %w", err) 376 } 377 378 opts = prepareInteractionOpts(connRecord, opts) 379 380 _, err = c.issueCredentialClient.SendProposal( 381 &issuecredential.ProposeCredential{InvitationID: invitation.ID}, 382 connRecord, 383 ) 384 if err != nil { 385 return nil, fmt.Errorf("failed to propose credential from wallet: %w", err) 386 } 387 388 ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) 389 defer cancel() 390 391 return c.waitForOfferCredential(ctx, connRecord) 392 } 393 394 // RequestCredential sends request credential message from wallet to issuer and 395 // optionally waits for credential response. 396 // https://w3c-ccg.github.io/universal-wallet-interop-spec/#requestcredential 397 // 398 // Currently Supporting : 0453-issueCredentialV2 399 // https://github.com/hyperledger/aries-rfcs/blob/main/features/0453-issue-credential-v2/README.md 400 // 401 // Args: 402 // - authToken: authorization for performing operation. 403 // - thID: thread ID (action ID) of offer credential message previously received. 404 // - concludeInteractionOptions: options to conclude interaction like presentation to be shared etc. 405 // 406 // Returns: 407 // - Credential interaction status containing status, redirectURL. 408 // - error if operation fails. 409 // 410 func (c *DidComm) RequestCredential(authToken, thID string, options ...ConcludeInteractionOptions) (*CredentialInteractionStatus, error) { //nolint: lll 411 opts := &concludeInteractionOpts{} 412 413 for _, option := range options { 414 option(opts) 415 } 416 417 var presentation interface{} 418 if opts.presentation != nil { 419 presentation = opts.presentation 420 } else { 421 presentation = opts.rawPresentation 422 } 423 424 attachmentID := uuid.New().String() 425 426 err := c.issueCredentialClient.AcceptOffer(thID, &issuecredential.RequestCredential{ 427 Type: issuecredentialsvc.RequestCredentialMsgTypeV2, 428 Formats: []issuecredentialsvc.Format{{ 429 AttachID: attachmentID, 430 Format: ldJSONMimeType, 431 }}, 432 Attachments: []decorator.GenericAttachment{{ 433 ID: attachmentID, 434 Data: decorator.AttachmentData{ 435 JSON: presentation, 436 }, 437 }}, 438 }) 439 if err != nil { 440 return nil, err 441 } 442 443 // wait for credential response. 444 if opts.waitForDone { 445 statusCh := make(chan service.StateMsg, msgEventBufferSize) 446 447 err = c.issueCredentialClient.RegisterMsgEvent(statusCh) 448 if err != nil { 449 return nil, fmt.Errorf("failed to register issue credential action event : %w", err) 450 } 451 452 defer func() { 453 e := c.issueCredentialClient.UnregisterMsgEvent(statusCh) 454 if e != nil { 455 logger.Warnf("Failed to unregister action event for issue credential: %w", e) 456 } 457 }() 458 459 ctx, cancel := context.WithTimeout(context.Background(), opts.timeout) 460 defer cancel() 461 462 return waitCredInteractionCompletion(ctx, statusCh, thID) 463 } 464 465 return &CredentialInteractionStatus{Status: model.AckStatusPENDING}, nil 466 } 467 468 // currently correlating response action by connection due to limitation in current present proof V1 implementation. 469 func (c *DidComm) waitForRequestPresentation(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll 470 done := make(chan *service.DIDCommMsgMap) 471 472 go func() { 473 for { 474 actions, err := c.presentProofClient.Actions() 475 if err != nil { 476 continue 477 } 478 479 if len(actions) > 0 { 480 for _, action := range actions { 481 if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { 482 done <- &action.Msg 483 return 484 } 485 } 486 } 487 488 select { 489 default: 490 time.Sleep(retryDelay) 491 case <-ctx.Done(): 492 return 493 } 494 } 495 }() 496 497 select { 498 case msg := <-done: 499 return msg, nil 500 case <-ctx.Done(): 501 return nil, fmt.Errorf("timeout waiting for request presentation message") 502 } 503 } 504 505 // currently correlating response action by connection due to limitation in current issue credential V1 implementation. 506 func (c *DidComm) waitForOfferCredential(ctx context.Context, record *connection.Record) (*service.DIDCommMsgMap, error) { //nolint: lll 507 done := make(chan *service.DIDCommMsgMap) 508 509 go func() { 510 for { 511 actions, err := c.issueCredentialClient.Actions() 512 if err != nil { 513 continue 514 } 515 516 if len(actions) > 0 { 517 for _, action := range actions { 518 if action.MyDID == record.MyDID && action.TheirDID == record.TheirDID { 519 done <- &action.Msg 520 return 521 } 522 } 523 } 524 525 select { 526 default: 527 time.Sleep(retryDelay) 528 case <-ctx.Done(): 529 return 530 } 531 } 532 }() 533 534 select { 535 case msg := <-done: 536 return msg, nil 537 case <-ctx.Done(): 538 return nil, fmt.Errorf("timeout waiting for offer credential message") 539 } 540 } 541 542 func waitForConnect(ctx context.Context, didStateMsgs chan service.StateMsg, connID string) error { 543 done := make(chan struct{}) 544 545 go func() { 546 for msg := range didStateMsgs { 547 if msg.Type != service.PostState || msg.StateID != didexchangeSvc.StateIDCompleted { 548 continue 549 } 550 551 var event model.Event 552 553 switch p := msg.Properties.(type) { 554 case model.Event: 555 event = p 556 default: 557 logger.Warnf("failed to cast didexchange event properties") 558 559 continue 560 } 561 562 if event.ConnectionID() == connID { 563 logger.Debugf( 564 "Received connection complete event for invitationID=%s connectionID=%s", 565 event.InvitationID(), event.ConnectionID()) 566 567 close(done) 568 569 break 570 } 571 } 572 }() 573 574 select { 575 case <-done: 576 return nil 577 case <-ctx.Done(): 578 return fmt.Errorf("time out waiting for did exchange state 'completed'") 579 } 580 } 581 582 // wait for credential interaction to be completed (done or abandoned protocol state). 583 func waitCredInteractionCompletion(ctx context.Context, didStateMsgs chan service.StateMsg, thID string) (*CredentialInteractionStatus, error) { // nolint:gocognit,gocyclo,lll 584 done := make(chan *CredentialInteractionStatus) 585 586 go func() { 587 for msg := range didStateMsgs { 588 // match post state. 589 if msg.Type != service.PostState { 590 continue 591 } 592 593 // invalid state msg. 594 if msg.Msg == nil { 595 continue 596 } 597 598 msgThID, err := msg.Msg.ThreadID() 599 if err != nil { 600 continue 601 } 602 603 // match parent thread ID. 604 if msg.Msg.ParentThreadID() != thID && msgThID != thID { 605 continue 606 } 607 608 // match protocol state. 609 if msg.StateID != stateNameDone && msg.StateID != stateNameAbandoned && msg.StateID != stateNameAbandoning { 610 continue 611 } 612 613 properties := msg.Properties.All() 614 615 response := &CredentialInteractionStatus{} 616 response.RedirectURL, response.Status = getWebRedirectInfo(properties) 617 618 // if redirect status missing, then use protocol state, done -> OK, abandoned -> FAIL. 619 if response.Status == "" { 620 if msg.StateID == stateNameAbandoned || msg.StateID == stateNameAbandoning { 621 response.Status = model.AckStatusFAIL 622 } else { 623 response.Status = model.AckStatusOK 624 } 625 } 626 627 done <- response 628 629 return 630 } 631 }() 632 633 select { 634 case status := <-done: 635 return status, nil 636 case <-ctx.Done(): 637 return nil, fmt.Errorf("time out waiting for credential interaction to get completed") 638 } 639 } 640 641 func prepareInteractionOpts(connRecord *connection.Record, opts *initiateInteractionOpts) *initiateInteractionOpts { 642 if opts.from == "" { 643 opts.from = connRecord.TheirDID 644 } 645 646 if opts.timeout == 0 { 647 opts.timeout = defaultWaitForRequestPresentationTimeOut 648 } 649 650 return opts 651 } 652 653 // getWebRedirectInfo reads web redirect info from properties. 654 func getWebRedirectInfo(properties map[string]interface{}) (string, string) { 655 var redirect, status string 656 657 if redirectURL, ok := properties[webRedirectURLKey]; ok { 658 redirect = redirectURL.(string) //nolint: errcheck, forcetypeassert 659 } 660 661 if redirectStatus, ok := properties[webRedirectStatusKey]; ok { 662 status = redirectStatus.(string) //nolint: errcheck, forcetypeassert 663 } 664 665 return redirect, status 666 }