diff --git a/pkg/restapi/issuer/operation/operations.go b/pkg/restapi/issuer/operation/operations.go index a96dcade..38a92445 100644 --- a/pkg/restapi/issuer/operation/operations.go +++ b/pkg/restapi/issuer/operation/operations.go @@ -1623,7 +1623,7 @@ func getAttachments(action service.DIDCommAction) ([]decorator.GenericAttachment return nil, fmt.Errorf("failed to decode didComm action message: %w", err) } - attachmentsRaw, ok := didCommMsgAsMap["requests~attach"] + attachmentsRaw, ok := didCommMsgAsMap["attachments"] if !ok { return nil, errors.New("missing attachments from DIDComm message map") } diff --git a/pkg/restapi/issuer/operation/operations_test.go b/pkg/restapi/issuer/operation/operations_test.go index be56bd72..3daf097e 100644 --- a/pkg/restapi/issuer/operation/operations_test.go +++ b/pkg/restapi/issuer/operation/operations_test.go @@ -3973,9 +3973,9 @@ func TestWACIIssuanceHandler(t *testing.T) { done := make(chan struct{}) - msg := service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV2{ + msg := service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV3{ Type: issuecredsvc.RequestCredentialMsgTypeV2, - RequestsAttach: []decorator.Attachment{ + Attachments: []decorator.AttachmentV2{ { ID: manifestID, Data: decorator.AttachmentData{ @@ -4059,9 +4059,9 @@ func TestWACIIssuanceHandler(t *testing.T) { // test failed to get credential manifestID from store testFailure(actionCh, msg, "failed to get credential manifestID from store") - msg = service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV2{ - Type: issuecredsvc.RequestCredentialMsgTypeV2, - RequestsAttach: []decorator.Attachment{ + msg = service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV3{ + Type: issuecredsvc.RequestCredentialMsgTypeV3, + Attachments: []decorator.AttachmentV2{ {ID: manifestID, Data: decorator.AttachmentData{ JSON: &verifiable.Presentation{}, @@ -4079,9 +4079,9 @@ func TestWACIIssuanceHandler(t *testing.T) { testFailure(actionCh, msg, "missing credential_application field") application := createCredentialApplication(t, c, manifestID, profile) - msg = service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV2{ - Type: issuecredsvc.RequestCredentialMsgTypeV2, - RequestsAttach: []decorator.Attachment{ + msg = service.NewDIDCommMsgMap(issuecredsvc.RequestCredentialV3{ + Type: issuecredsvc.RequestCredentialMsgTypeV3, + Attachments: []decorator.AttachmentV2{ {ID: manifestID, Data: decorator.AttachmentData{ JSON: application, diff --git a/test/bdd/features/waci_didcommv2.feature b/test/bdd/features/waci_didcommv2.feature index 480426d6..8b3e4a7c 100644 --- a/test/bdd/features/waci_didcommv2.feature +++ b/test/bdd/features/waci_didcommv2.feature @@ -9,19 +9,36 @@ Feature: WACI DIDComm V2 @issuer_adapter_waci_v2 Scenario Outline: Issuer adapter features - Given "WalletApp" agent is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082" - And Wallet "WalletApp" has profile created and unlocked - + Given "" agent is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082" + And Wallet "" has profile created and unlocked + #This scenario only has output descriptors configured in manifest-config file. Given Issuer Profile with id "", name "", issuerURL "", supportedVCContexts "", scopes "", issuer id "", linked wallet "" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support And Retrieved profile with id "" contains name "", issuerURL "", supportedVCContexts "", scopes "", issuer id "", linked wallet "" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support Then Issuer adapter shows the wallet connect UI when the issuer "" with scopes "" wants to connect to the wallet And Issuer adapter ("") creates DIDComm connection invitation for "" - And "" accepts invitation from issuer adapter "" and performs WACI credential issuance interaction + # While performing WACI interaction, validation of offer credential attachment(manifest and fulfillment) and + # request credential with credential application attachment presentation is sent via universal wallet + And "" accepts invitation from issuer adapter "" and performs WACI credential issuance interaction with manifest with PEx requirement "false" And "" received web redirect info from "" after successful completion of WACI credential issuance interaction Examples: - | profileID | profileName | issuerURL | supportedVCContexts | scopes | issuerID | linkedWallet | walletID | + | profileID | profileName | issuerURL | supportedVCContexts | scopes | issuerID | linkedWallet | walletID | | prCardWACI | PRCard Issuer | http://mock-issuer.com:9080/prCard | https://trustbloc.github.io/context/vc/examples/citizenship-v1.jsonld | prc | did:example:123?linked-domains=3 | https://example.wallet.com/waci | WalletApp | + @issuer_adapter_waci_v2_withPEx + Scenario Outline: Issuer adapter features + Given "" agent is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082" + And Wallet "" has profile created and unlocked + #This scenario has output descriptors and prc card input descriptor configured in manifest-config file. + Given Issuer Profile with id "", name "", issuerURL "", supportedVCContexts "", scopes "", issuer id "", linked wallet "" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support + And Retrieved profile with id "" contains name "", issuerURL "", supportedVCContexts "", scopes "", issuer id "", linked wallet "" and oidc provider "https://issuer-hydra.trustbloc.local:9044/" with DIDComm V2 and WACI support + Then Issuer adapter shows the wallet connect UI when the issuer "" with scopes "" wants to connect to the wallet + And Issuer adapter ("") creates DIDComm connection invitation for "" + And "" accepts invitation from issuer adapter "" and performs WACI credential issuance interaction with manifest with PEx requirement "true" + And "" received web redirect info from "" after successful completion of WACI credential issuance interaction + Examples: + | profileID | profileName | issuerURL | supportedVCContexts | scopes | issuerID | linkedWallet | walletID | + | mDLWACI | Driving License Issuer | http://mock-issuer.com:9080/driversLicense | https://trustbloc.github.io/context/vc/examples/driver-license-evidence-v1.jsonld | mDL | did:example:123?linked-domains=3 | https://example.wallet.com/waci | WalletMDLApp | + @verifier_adapter_waci_v2 Scenario: WACI flow between Verifier and Wallet using DIDComm V2 Given the "Mock Wallet" is running on "localhost" port "9081" with webhook "http://localhost:9083" and controller "http://localhost:9082" diff --git a/test/bdd/fixtures/testdata/manifest-config/cmdescriptors.json b/test/bdd/fixtures/testdata/manifest-config/cmdescriptors.json index ed4699d6..0d18c924 100644 --- a/test/bdd/fixtures/testdata/manifest-config/cmdescriptors.json +++ b/test/bdd/fixtures/testdata/manifest-config/cmdescriptors.json @@ -68,10 +68,116 @@ } } } + ] + }, + "mDL":{ + "output_descriptor":[ + { + "id":"driver_license_output", + "schema":"https://schema.org/EducationalOccupationalCredential", + "display":{ + "title":{ + "path":[ + "$.name", + "$.vc.name" + ], + "schema":{ + "type":"string" + }, + "fallback":"Washington State Driver License" + }, + "subtitle":{ + "path":[ + "$.class", + "$.vc.class" + ], + "schema":{ + "type":"string" + }, + "fallback":"Class A, Commercial" + }, + "description":{ + "text":"License to operate a vehicle with a gross combined weight rating (GCWR) of 26,001 or more pounds, as long as the GVWR of the vehicle(s) being towed is over 10,000 pounds." + }, + "properties":[ + { + "path":[ + "$.donor", + "$.vc.donor" + ], + "schema":{ + "type":"boolean" + }, + "fallback":"Unknown", + "label":"Organ Donor" + } + ] + }, + "styles":{ + "thumbnail":{ + "uri":"https://dol.wa.com/logo.png", + "alt":"Washington State Seal" + }, + "hero":{ + "uri":"https://dol.wa.com/happy-people-driving.png", + "alt":"Happy people driving" + }, + "background":{ + "color":"#ff0000" + }, + "text":{ + "color":"#d4d400" + } + } + } ], - "options":{ - "challenge":"508adef4-b8e0-4edf-a53d-a260371c1423", - "domain":"9rf25a28rs96" - } + "input_descriptor":[ + { + "id":"prc_input", + "name":"Permanent Resident Card", + "purpose":"We need PRC to verify your status.", + "schema":[ + { + "uri":"https://w3id.org/citizenship#PermanentResidentCard" + } + ], + "constraints":{ + "fields":[ + { + "path":[ + "$.credentialSubject.givenName" + ], + "filter":{ + "type":"string" + } + }, + { + "path":[ + "$.credentialSubject.familyName" + ], + "filter":{ + "type":"string" + } + }, + { + "path":[ + "$.credentialSubject.birthCountry" + ], + "filter":{ + "type":"string" + } + }, + { + "path":[ + "$.credentialSubject.birthDate" + ], + "filter":{ + "type":"string" + } + } + ] + } + } + ] } } \ No newline at end of file diff --git a/test/bdd/pkg/agent/testdata/vc_prc.json b/test/bdd/pkg/agent/testdata/vc_prc.json new file mode 100644 index 00000000..0b2237bd --- /dev/null +++ b/test/bdd/pkg/agent/testdata/vc_prc.json @@ -0,0 +1,33 @@ +{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1" + ], + "id":"urn:uvci:af5vshde843jf831j128fj", + "type":[ + "VerifiableCredential", + "VaccinationCertificate", + "PermanentResidentCard" + ], + "name":"Permanent Resident Card", + "description":"Permanent Resident Card of Mr.Louis Pasteu", + "expirationDate":"2029-12-03T12:19:52Z", + "issuanceDate":"2019-12-03T12:19:52Z", + "issuer":"did:example:456", + "credentialSubject":{ + "id":"did:example:ebfeb1f712ebc6f1c276e12ec21", + "givenName":"Louis", + "familyName":"Pasteur", + "birthCountry":"Bahamas", + "birthDate":"1958-07-17" + }, + "proof":{ + "type":"BbsBlsSignatureProof2020", + "created":"2021-02-18T23:04:28Z", + "nonce":"JNGovx4GGoi341v/YCTcZq7aLWtBtz8UhoxEeCxZFevEGzfh94WUSg8Ly/q+2jLqzzY=", + "proofPurpose":"assertionMethod", + "proofValue":"AB0GQA//jbDwMgaIIJeqP3fRyMYi6WDGhk0JlGJc/sk4ycuYGmyN7CbO4bA7yhIW/YQbHEkOgeMy0QM+usBgZad8x5FRePxfo4v1dSzAbJwWjx87G9F1lAIRgijlD4sYni1LhSo6svptDUmIrCAOwS2raV3G02mVejbwltMOo4+cyKcGlj9CzfjCgCuS1SqAxveDiMKGAAAAdJJF1pO6hBUGkebu/SMmiFafVdLvFgpMFUFEHTvElUQhwNSp6vxJp6Rs7pOVc9zHqAAAAAI7TJuDCf7ramzTo+syb7Njf6ExD11UKNcChaeblzegRBIkg3HoWgwR0hhd4z4D5/obSjGPKpGuD+1DoyTZhC/wqOjUZ03J1EtryZrC+y1DD14b4+khQVLgOBJ9+uvshrGDbu8+7anGezOa+qWT0FopAAAAEG6p07ghODpi8DVeDQyPwMY/iu2Lh7x3JShWniQrewY2GbsACBYOPlkNNm/qSExPRMe2X7UPpdsxpUDwqbObye4EXfAabgKd9gCmj2PNdvcOQAi5rIuJSGa4Vj7AtKoW/2vpmboPoOu4IEM1YviupomCKOzhjEuOof2/y5Adfb8JUVidWqf9Ye/HtxnzTu0HbaXL7jbwsMNn5wYfZuzpmVQgEXss2KePMSkHcfScAQNglnI90YgugHGuU+/DQcfMoA0+JviFcJy13yERAueVuzrDemzc+wJaEuNDn8UiTjAdVhLcgnHqUai+4F6ONbCfH2B3ohB3hSiGB6C7hDnEyXFOO9BijCTHrxPv3yKWNkks+3JfY28m+3NO0e2tlyH71yDX0+F6U388/bvWod/u5s3MpaCibTZEYoAc4sm4jW03HFYMmvYBuWOY6rGGOgIrXxQjx98D0macJJR7Hkh7KJhMkwvtyI4MaTPJsdJGfv8I+RFROxtRM7RcFpa4J5wF/wQnpyorqchwo6xAOKYFqCqKvI9B6Y7Da7/0iOiWsjs8a4zDiYynfYavnz6SdxCMpHLgplEQlnntqCb8C3qly2s5Ko3PGWu4M8Dlfcn4TT8YenkJDJicA91nlLaE8TJbBgsvgyT+zlTsRSXlFzQc+3KfWoODKZIZqTBaRZMft3S/", + "verificationMethod":"did:example:123#key-1" + } +} \ No newline at end of file diff --git a/test/bdd/pkg/agent/wallet.go b/test/bdd/pkg/agent/wallet.go index 13a3d01e..fd15fe7d 100644 --- a/test/bdd/pkg/agent/wallet.go +++ b/test/bdd/pkg/agent/wallet.go @@ -7,19 +7,24 @@ SPDX-License-Identifier: Apache-2.0 package agent import ( + _ "embed" //nolint // This is needed to use go:embed "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "net/url" + "strconv" "time" "github.com/cucumber/godog" "github.com/hyperledger/aries-framework-go/pkg/client/issuecredential" "github.com/hyperledger/aries-framework-go/pkg/controller/command/vcwallet" "github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service" + "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/decorator" issuecredsvc "github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/issuecredential" + "github.com/hyperledger/aries-framework-go/pkg/doc/cm" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/wallet" issuerop "github.com/trustbloc/edge-adapter/pkg/restapi/issuer/operation" @@ -27,6 +32,11 @@ import ( "github.com/trustbloc/edge-adapter/test/bdd/pkg/context" ) +var ( + //go:embed testdata/vc_prc.json + vcPRC []byte //nolint:gochecknoglobals +) + const ( // wallet controller URLs walletOperationID = "/vcwallet" @@ -40,6 +50,9 @@ const ( tokenExpiry = 20 * time.Minute ) +// nolint:gochecknoglobals +var expectedManifestIDs = []string{"mDL_mID", "prc_mID"} + // Steps contains steps for aries agent. type walletSteps struct { bddContext *context.BDDContext @@ -60,7 +73,8 @@ func newWalletSteps(ctx *context.BDDContext, controllers, webhooks map[string]st //nolint:lll func (a *walletSteps) RegisterSteps(s *godog.Suite) { s.Step(`^Wallet "([^"]*)" has profile created and unlocked$`, a.createProfileAndUnlock) - s.Step(`^"([^"]*)" accepts invitation from issuer adapter "([^"]*)" and performs WACI credential issuance interaction$`, a.performWACIIssuanceInteraction) + s.Step(`^"([^"]*)" accepts invitation from issuer adapter "([^"]*)" and performs WACI credential issuance interaction$`, a.performWACIIssuanceInteractionV1) + s.Step(`^"([^"]*)" accepts invitation from issuer adapter "([^"]*)" and performs WACI credential issuance interaction with manifest with PEx requirement "([^"]*)"$`, a.performWACIIssuanceInteractionV2) s.Step(`^"([^"]*)" received web redirect info from "([^"]*)" after successful completion of WACI credential issuance interaction$`, a.validateWebRedirect) } @@ -106,7 +120,7 @@ func (a *walletSteps) createProfileAndUnlock(walletID string) error { return nil } -func (a *walletSteps) performWACIIssuanceInteraction(walletID, issuerID string) error { +func (a *walletSteps) performWACIIssuanceInteractionV1(walletID, issuerID string) error { controller, ok := a.ControllerURLs[walletID] if !ok { return fmt.Errorf("unable to find controller URL registered for wallet agent [%s]", walletID) @@ -117,12 +131,12 @@ func (a *walletSteps) performWACIIssuanceInteraction(walletID, issuerID string) return fmt.Errorf("failed to find wallet auth for wallet['%s']", walletID) } - thID, err := a.initiateWACIIssuance(walletID, issuerID, auth, controller) + thID, err := a.initiateWACIIssuanceV1(walletID, issuerID, auth, controller) if err != nil { return fmt.Errorf("failed to initiate WACI issuance interaction for wallet[%s] : %w", walletID, err) } - err = a.concludeWACIIssuance(walletID, issuerID, auth, thID, controller) + err = a.concludeWACIIssuanceV1(walletID, issuerID, auth, thID, controller) if err != nil { return fmt.Errorf("failed to conclude WACI issuance interaction for wallet[%s] : %w", walletID, err) } @@ -130,16 +144,7 @@ func (a *walletSteps) performWACIIssuanceInteraction(walletID, issuerID string) return nil } -func (a *walletSteps) validateWebRedirect(walletID, agentID string) error { - redirectURL, ok := a.bddContext.GetString(walletRedirectKey(walletID, agentID)) - if !ok || redirectURL == "" { - return fmt.Errorf("redirect URL not found for wallet[%s] and agent[%s]", walletID, agentID) - } - - return nil -} - -func (a *walletSteps) initiateWACIIssuance(walletID, issuerID, auth, controller string) (string, error) { +func (a *walletSteps) initiateWACIIssuanceV1(walletID, issuerID, auth, controller string) (string, error) { oob, err := a.decodeOOBInvitation(walletID, issuerID) if err != nil { return "", fmt.Errorf("'%s' fails to read oob invitation from '%s': %w", walletID, issuerID, err) @@ -166,7 +171,7 @@ func (a *walletSteps) initiateWACIIssuance(walletID, issuerID, auth, controller logger.Debugf("wallet[%s] received propose credential response: %+v", walletID, response.OfferCredential) - err = validateOfferCredential(response.OfferCredential) + err = validateOfferCredentialV1(response.OfferCredential) if err != nil { return "", fmt.Errorf("offer credential response validation failed : %w", err) } @@ -189,7 +194,29 @@ func (a *walletSteps) initiateWACIIssuance(walletID, issuerID, auth, controller return thID, nil } -func (a *walletSteps) concludeWACIIssuance(walletID, issuerID, auth, thID, controller string) error { +func validateOfferCredentialV1(msg *service.DIDCommMsgMap) error { + var offer issuecredential.OfferCredential + + if err := msg.Decode(&offer); err != nil { + return fmt.Errorf("failed to decode offer credential from incoming msg: %w", err) + } + + if offer.Type == "" { + return fmt.Errorf("invalid offer credential message, empty type:\nmsg = %#v\noffer = %#v", msg, offer) + } + + if len(offer.Attachments) == 0 { + return errors.New("invalid offer credential message, expected attachments") + } + + if offer.Type == issuecredsvc.OfferCredentialMsgTypeV2 && len(offer.Formats) == 0 { + return errors.New("invalid offer credential message, expected valid attachment formats") + } + + return nil +} + +func (a *walletSteps) concludeWACIIssuanceV1(walletID, issuerID, auth, thID, controller string) error { // conclude WACI interaction by sending credential request requestCredential, err := json.Marshal(vcwallet.RequestCredentialRequest{ WalletAuth: vcwallet.WalletAuth{ @@ -237,6 +264,169 @@ func (a *walletSteps) concludeWACIIssuance(walletID, issuerID, auth, thID, contr return nil } +func (a *walletSteps) performWACIIssuanceInteractionV2(walletID, issuerID, hasPExRequirement string) error { + controller, ok := a.ControllerURLs[walletID] + if !ok { + return fmt.Errorf("unable to find controller URL registered for wallet agent [%s]", walletID) + } + + auth, ok := a.bddContext.GetString(walletAuthKey(walletID)) + if !ok { + return fmt.Errorf("failed to find wallet auth for wallet['%s']", walletID) + } + + manifest, thID, err := a.initiateWACIIssuance(walletID, issuerID, auth, controller, hasPExRequirement) + if err != nil { + return fmt.Errorf("failed to initiate WACI issuance interaction for wallet[%s] : %w", walletID, err) + } + + err = a.concludeWACIIssuance(walletID, issuerID, auth, thID, controller, manifest) + if err != nil { + return fmt.Errorf("failed to conclude WACI issuance interaction for wallet[%s] : %w", walletID, err) + } + + return nil +} + +func (a *walletSteps) validateWebRedirect(walletID, agentID string) error { + redirectURL, ok := a.bddContext.GetString(walletRedirectKey(walletID, agentID)) + if !ok || redirectURL == "" { + return fmt.Errorf("redirect URL not found for wallet[%s] and agent[%s]", walletID, agentID) + } + + return nil +} + +func (a *walletSteps) initiateWACIIssuance(walletID, issuerID, auth, controller, + hasPExRequirement string) (*cm.CredentialManifest, string, error) { + oob, err := a.decodeOOBInvitation(walletID, issuerID) + if err != nil { + return nil, "", fmt.Errorf("'%s' fails to read oob invitation from '%s': %w", walletID, issuerID, err) + } + + // initiate WACI issuance interaction from wallet by proposing credential + proposeRequest, err := json.Marshal(&vcwallet.ProposeCredentialRequest{ + WalletAuth: vcwallet.WalletAuth{ + UserID: walletID, + Auth: auth, + }, + Invitation: oob, + }) + if err != nil { + return nil, "", fmt.Errorf("failed to prepare wallet create profile request : %w", err) + } + + var response vcwallet.ProposeCredentialResponse + + err = bddutil.SendHTTP(http.MethodPost, controller+proposeCredentialPath, proposeRequest, &response) + if err != nil { + return nil, "", fmt.Errorf("failed to propose credential from wallet : %w", err) + } + + logger.Debugf("wallet[%s] received propose credential response: %+v", walletID, response.OfferCredential) + + manifest, err := validateOfferCredential(response.OfferCredential, hasPExRequirement) + if err != nil { + return nil, "", fmt.Errorf("offer credential response validation failed : %w", err) + } + + var thID string + + if oob.Version() == service.V2 { + thID = response.OfferCredential.ParentThreadID() + } else { + thID, err = response.OfferCredential.ThreadID() + if err != nil { + return nil, "", fmt.Errorf("failed to get thread ID from offer credential : %w", err) + } + } + + if thID == "" { + return nil, "", errors.New("no threadID found in offer credential message") + } + + return manifest, thID, nil +} + +func generatePresentationWithCredentialApplication(credentialManifest *cm.CredentialManifest) (*verifiable.Presentation, + error) { + presentationWithCA, err := cm.PresentCredentialApplication(credentialManifest) + if err != nil { + return nil, fmt.Errorf("failed to generate presentation with credential application : %w", err) + } + + cred := &verifiable.Credential{} + + err = json.Unmarshal(vcPRC, cred) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal credential : %w", err) + } + + presentationWithCA.AddCredentials(cred) + + return presentationWithCA, nil +} + +func (a *walletSteps) concludeWACIIssuance(walletID, issuerID, auth, thID, controller string, + manifest *cm.CredentialManifest) error { + presentation, err := generatePresentationWithCredentialApplication(manifest) + if err != nil { + return fmt.Errorf("failed to generate credential application attachment: %w", err) + } + + rawPresentation, err := presentation.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal credential application presentation : %w", err) + } + + // conclude WACI interaction by sending credential request with credential application + requestCredential, err := json.Marshal(vcwallet.RequestCredentialRequest{ + WalletAuth: vcwallet.WalletAuth{ + UserID: walletID, + Auth: auth, + }, + ThreadID: thID, + WaitForDone: true, + Presentation: rawPresentation, + }) + if err != nil { + return fmt.Errorf("failed to prepare request credential: %w", err) + } + + status := make(chan error) + + go func() { + status <- a.waitAndAcceptIncomingCredential(walletID) + }() + + var interactionResponse vcwallet.RequestCredentialResponse + + err = bddutil.SendHTTP(http.MethodPost, controller+requestCredentialPath, requestCredential, &interactionResponse) + if err != nil { + return fmt.Errorf("'%s', failed to request credential from wallet : %w", walletID, err) + } + + logger.Infof("wallet[%s] received credential fulfillment response: %+v", interactionResponse) + + select { + case e := <-status: + if e != nil { + return fmt.Errorf("incoming credential validation failed for wallet[%s]: cause: %w", walletID, e) + } + case <-time.After(waitForResponseTimeout): + return fmt.Errorf("timeout waiting for incoming credential wallet[%s]", walletID) + } + + if interactionResponse.Status != "OK" { + return fmt.Errorf("invalid credential interaction status received wallet[%s], expected[OK], got[%s]", + walletID, interactionResponse.Status) + } + + a.bddContext.Store[walletRedirectKey(walletID, issuerID)] = interactionResponse.RedirectURL + + return nil +} + func (a *walletSteps) waitAndAcceptIncomingCredential(walletID string) error { // nolint: gocyclo,cyclop webhookURL, ok := a.WebhookURLs[walletID] if !ok { @@ -283,6 +473,16 @@ func (a *walletSteps) waitAndAcceptIncomingCredential(walletID string) error { / return fmt.Errorf("wallet[%s] received invalid issue credential message: empty attachments", walletID) } + credentialFulfillment, err := getCredentialFulfillmentFromAttachment(&response.Attachments[0]) + if err != nil { + return fmt.Errorf("failed to credential fulfillment from attachment %w", err) + } + + if !stringsContains(credentialFulfillment.ManifestID, expectedManifestIDs) { + return fmt.Errorf("expected credential fulfillment's manifest ID to be %s, but got %s instead", + expectedManifestIDs, credentialFulfillment.ManifestID) + } + return nil } @@ -328,28 +528,168 @@ func (a *walletSteps) decodeOOBInvitation(walletID, issuerID string) (*wallet.Ge return inv, nil } -func validateOfferCredential(msg *service.DIDCommMsgMap) error { +func validateOfferCredential(msg *service.DIDCommMsgMap, hasPExRequirement string) (*cm.CredentialManifest, error) { var offer issuecredential.OfferCredential if err := msg.Decode(&offer); err != nil { - return fmt.Errorf("failed to decode offer credential from incoming msg: %w", err) + return nil, fmt.Errorf("failed to decode offer credential from incoming msg: %w", err) } if offer.Type == "" { - return fmt.Errorf("invalid offer credential message, empty type:\nmsg = %#v\noffer = %#v", msg, offer) + return nil, fmt.Errorf("invalid offer credential message, empty type:\nmsg = %#v\noffer = %#v", msg, offer) } if len(offer.Attachments) == 0 { - return errors.New("invalid offer credential message, expected attachments") + return nil, errors.New("invalid offer credential message, expected attachments") } if offer.Type == issuecredsvc.OfferCredentialMsgTypeV2 && len(offer.Formats) == 0 { - return errors.New("invalid offer credential message, expected valid attachment formats") + return nil, errors.New("invalid offer credential message, expected valid attachment formats") + } + + credentialManifest, err := validateOfferCredAttachments(&offer, hasPExRequirement) + if err != nil { + return nil, fmt.Errorf("failed to validate attachments %w", err) + } + + return credentialManifest, nil +} + +func validateOfferCredAttachments(offer *issuecredential.OfferCredential, + hasPExRequirement string) (*cm.CredentialManifest, error) { + credentialManifest, err := getCredentialManifestFromAttachment(&offer.Attachments[0]) + if err != nil { + return nil, fmt.Errorf("credential manifest not found: err= %w", err) + } + + if !stringsContains(credentialManifest.ID, expectedManifestIDs) { + return nil, fmt.Errorf("expected credential manifest ID to be %v"+ + " but got %s instead", expectedManifestIDs, credentialManifest.ID) + } + + hasPExSupport, err := strconv.ParseBool(hasPExRequirement) + if err != nil { + return nil, fmt.Errorf("failed to parse bool %v value for PEx support", hasPExSupport) + } + // if Presentation requirement is defined via input descriptors then + // presentation definition must be part of credential manifest + if hasPExSupport { + if credentialManifest.PresentationDefinition == nil { + return nil, fmt.Errorf("failed to find presentation definitation in the manifest with PEx "+ + "support %v", hasPExSupport) + } + } + + // The Credential Fulfillment we receive from the issuer acts as a preview for the credentials we eventually + // wish to receive. + credentialFulfillment, err := getCredentialFulfillmentFromAttachment(&offer.Attachments[1]) + if err != nil { + return nil, fmt.Errorf("failed to credential fulfillment from attachment %w", err) + } + + if !stringsContains(credentialManifest.ID, expectedManifestIDs) { + return nil, fmt.Errorf("expected credential fulfillment's manifest ID to be %v"+ + "but got %s instead", expectedManifestIDs, credentialFulfillment.ManifestID) + } + + err = resolveVCBasedOnCredentialFulfillment(credentialFulfillment, offer.Attachments[1].Data.JSON) + if err != nil { + return nil, fmt.Errorf("failed to resolve vc based on credential fulfillment %w", err) + } + + return credentialManifest, nil +} + +func resolveVCBasedOnCredentialFulfillment(credentialFulfillment *cm.CredentialFulfillment, + dataFromAttachment interface{}) error { + documentLoader, err := bddutil.DocumentLoader() + if err != nil { + return fmt.Errorf("failed to load document loader: %w", err) + } + + // These VCs are only previews - they lack proofs. + vcs, err := credentialFulfillment.ResolveDescriptorMaps(dataFromAttachment, + verifiable.WithJSONLDDocumentLoader(documentLoader)) + if err != nil { + return fmt.Errorf("failed to resolve DescriptorMaps %w", err) + } + + if len(vcs) != 1 { + return fmt.Errorf("received %d VCs, but expected only one", len(vcs)) } return nil } +//nolint:dupl +func getCredentialManifestFromAttachment(attachment *decorator.GenericAttachment) (*cm.CredentialManifest, error) { + attachmentAsMap, ok := attachment.Data.JSON.(map[string]interface{}) + if !ok { + return nil, errors.New("couldn't assert attachment as a map") + } + + credentialManifestRaw, ok := attachmentAsMap["credential_manifest"] + if !ok { + return nil, errors.New("credential_manifest object missing from attachment") + } + + credentialManifestBytes, err := json.Marshal(credentialManifestRaw) + if err != nil { + return nil, fmt.Errorf("failed to marshal credential manifest error = %w", err) + } + + var credentialManifest cm.CredentialManifest + + // This unmarshal call also triggers the credential manifest validation code, which ensures that the + // credential manifest is valid under the spec. + err = json.Unmarshal(credentialManifestBytes, &credentialManifest) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal credential manifest error = %w", err) + } + + return &credentialManifest, nil +} + +//nolint:dupl +func getCredentialFulfillmentFromAttachment(attachment *decorator.GenericAttachment) (*cm.CredentialFulfillment, + error) { + attachmentAsMap, ok := attachment.Data.JSON.(map[string]interface{}) + if !ok { + return nil, errors.New("couldn't assert attachment as a map") + } + + credentialFulfillmentRaw, ok := attachmentAsMap["credential_fulfillment"] + if !ok { + return nil, errors.New("credential_fulfillment object missing from attachment") + } + + credentialFulfillmentBytes, err := json.Marshal(credentialFulfillmentRaw) + if err != nil { + return nil, fmt.Errorf("failed to marshal credential fulfillment error = %w", err) + } + + var credentialFulfillment cm.CredentialFulfillment + + // This unmarshal call also triggers the credential fulfillment validation code, which ensures that the + // credential fulfillment object is valid under the spec. + err = json.Unmarshal(credentialFulfillmentBytes, &credentialFulfillment) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal credential fulfillment error = %w", err) + } + + return &credentialFulfillment, nil +} + +func stringsContains(val string, slice []string) bool { + for _, s := range slice { + if val == s { + return true + } + } + + return false +} + func walletAuthKey(walletID string) string { return fmt.Sprintf("walletauth_%s", walletID) }