diff --git a/auth/services/selfsigned/validator.go b/auth/services/selfsigned/validator.go index 5e4dff912f..0ec3166863 100644 --- a/auth/services/selfsigned/validator.go +++ b/auth/services/selfsigned/validator.go @@ -19,13 +19,16 @@ package selfsigned import ( + "context" "encoding/json" "errors" + "fmt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/contract" "github.com/nuts-foundation/nuts-node/auth/services" "github.com/nuts-foundation/nuts-node/auth/services/selfsigned/types" + "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/verifier" "time" @@ -92,7 +95,8 @@ func (v validator) VerifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (c } func (v validator) verifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (credentialSubject types.EmployeeIdentityCredentialSubject, proof vc.JSONWebSignature2020Proof, resultErr error) { - vcs, err := v.vcr.Verifier().VerifyVP(vp, true, validAt) + // #2428: NutsEmployeeCredential should be valid (signature), but does not need to be trusted. + vcs, err := v.vcr.Verifier().VerifyVP(vp, true, true, validAt) if err != nil { if errors.As(err, &verifier.VerificationError{}) { resultErr = newVerificationError(err.Error()) @@ -135,6 +139,22 @@ func (v validator) verifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (c return } + // #2428: NutsEmployeeCredential trust is derived from the fact that the issuer has a trusted NutsOrganizationCredential + searchTerms := []vcr.SearchTerm{ + {IRIPath: jsonld.CredentialSubjectPath, Value: credentialSubject.ID}, + {IRIPath: jsonld.OrganizationNamePath, Type: vcr.NotNil}, + {IRIPath: jsonld.OrganizationCityPath, Type: vcr.NotNil}, + } + nutsOrgCreds, err := v.vcr.Search(context.TODO(), searchTerms, false, validAt) + if err != nil { + resultErr = fmt.Errorf("unable to check NutsEmployeeCredential trust status using NutsOrganizationCredential: %w", err) + return + } + if len(nutsOrgCreds) == 0 { + resultErr = newVerificationError("NutsEmployeeCredential rejected, issuer does not have a trusted NutsOrganizationCredential") + return + } + return credentialSubject, proof, nil } diff --git a/auth/services/selfsigned/validator_test.go b/auth/services/selfsigned/validator_test.go index 29f9ca8660..e3ef335c4f 100644 --- a/auth/services/selfsigned/validator_test.go +++ b/auth/services/selfsigned/validator_test.go @@ -22,11 +22,13 @@ import ( "context" "encoding/json" "errors" + "github.com/golang/mock/gomock" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/auth/services" "github.com/nuts-foundation/nuts-node/auth/services/selfsigned/types" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/util" + "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/verifier" "os" @@ -74,10 +76,11 @@ func TestSigner_Validator_Roundtrip(t *testing.T) { } signerService := NewSigner(vcrContext.VCR, "http://localhost").(*signer) roleName := "Administrator" + issuerDID := "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm" createdVP, err := signerService.createVP(audit.TestContext(), types.Session{ ExpiresAt: issuanceDate.Add(time.Hour * 24), Contract: testContract, - Employer: "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm", + Employer: issuerDID, Employee: types.Employee{ Identifier: "user@examle.com", RoleName: &roleName, @@ -86,6 +89,15 @@ func TestSigner_Validator_Roundtrip(t *testing.T) { }}, issuanceDate) require.NoError(t, err) + // #2428: NutsEmployeeCredential does not need to be trusted, but the issuer needs to have a trusted NutsOrganizationCredential (chain of trust). + // Issue() automatically trusts the issuer, so untrust it for asserting trust chain behavior + nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(issuerDID), false, false) + require.NoError(t, err) + err = vcrContext.VCR.StoreCredential(*nutsOrgCred, nil) // Need to explicitly store, since we didn't publish it. + require.NoError(t, err) + err = vcrContext.VCR.Untrust(ssi.MustParseURI(credentialType), did.MustParseDID(issuerDID).URI()) + require.NoError(t, err) + // Validate VP validatorService := NewValidator(vcrContext.VCR, contract.StandardContractTemplates) checkTime := issuanceDate.Add(time.Minute) @@ -107,7 +119,8 @@ func TestValidator_VerifyVP(t *testing.T) { t.Run("ok using mocks", func(t *testing.T) { mockContext := newMockContext(t) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, &vpValidTime).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, &vpValidTime) @@ -128,7 +141,8 @@ func TestValidator_VerifyVP(t *testing.T) { credentialWithoutRole := vc.VerifiableCredential{} data, _ := os.ReadFile("./test/vc-without-role.json") _ = json.Unmarshal(data, &credentialWithoutRole) - mockContext.verifier.EXPECT().VerifyVP(vp, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil) + mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, &vpValidTime) @@ -139,7 +153,7 @@ func TestValidator_VerifyVP(t *testing.T) { t.Run("technical error on verify", func(t *testing.T) { mockContext := newMockContext(t) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, errors.New("error")) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return(nil, errors.New("error")) _, err := ss.VerifyVP(vp, nil) @@ -149,7 +163,7 @@ func TestValidator_VerifyVP(t *testing.T) { t.Run("verification error on verify", func(t *testing.T) { mockContext := newMockContext(t) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, verifier.VerificationError{}) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return(nil, verifier.VerificationError{}) result, err := ss.VerifyVP(vp, nil) @@ -159,18 +173,40 @@ func TestValidator_VerifyVP(t *testing.T) { }) t.Run("ok using in-memory DBs", func(t *testing.T) { - vcrContext := vcr.NewTestVCRContext(t, crypto.NewMemoryCryptoInstance()) + keyStore := crypto.NewMemoryStorage() + vcrContext := vcr.NewTestVCRContext(t, crypto.NewTestCryptoInstance(keyStore)) + var didDocument did.Document + { + ddBytes, _ := os.ReadFile("./test/diddocument.json") + err := json.Unmarshal(ddBytes, &didDocument) + require.NoError(t, err) + } + { + // Load private key so we can sign + privateKeyData, _ := os.ReadFile("./test/private.pem") + privateKey, err := util.PemToPrivateKey(privateKeyData) + require.NoError(t, err) + err = keyStore.SavePrivateKey(context.Background(), didDocument.VerificationMethod[0].ID.String(), privateKey) + require.NoError(t, err) + } + ss := NewValidator(vcrContext.VCR, contract.StandardContractTemplates) - didDocument := did.Document{} - ddBytes, _ := os.ReadFile("./test/diddocument.json") - _ = json.Unmarshal(ddBytes, &didDocument) // test transaction for DIDStore ordering tx := didstore.TestTransaction(didDocument) tx.SigningTime = docTXTime err := vcrContext.DIDStore.Add(didDocument, tx) require.NoError(t, err) - // Trust issuer, only needed for test - vcrContext.VCR.Trust(ssi.MustParseURI(credentialType), didDocument.ID.URI()) + // #2428: NutsEmployeeCredential issuer needs a trusted NutsOrganizationCredential + issuer.TimeFunc = func() time.Time { + // Issued credentials get the current date/time as issuance date, + // need to set it to a fixed value that corresponds with vpValidTime for testing. + // Otherwise, the NutsOrganizationCredential is not yet valid or might be expired. + return vpValidTime.Add(-1 * time.Hour) + } + nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(didDocument.ID.String()), false, false) + require.NoError(t, err) + err = vcrContext.VCR.StoreCredential(*nutsOrgCred, &vpValidTime) // Need to explicitly store, since we didn't publish it. + require.NoError(t, err) result, err := ss.VerifyVP(vp, &vpValidTime) @@ -185,7 +221,8 @@ func TestValidator_VerifyVP(t *testing.T) { vpData, _ := os.ReadFile("./test/vp_invalid_contract.json") _ = json.Unmarshal(vpData, &vp) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, nil) @@ -198,7 +235,8 @@ func TestValidator_VerifyVP(t *testing.T) { mockContext := newMockContext(t) now := time.Now() ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, &now).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &now).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, &now) @@ -210,7 +248,7 @@ func TestValidator_VerifyVP(t *testing.T) { t.Run("error - missing credential", func(t *testing.T) { mockContext := newMockContext(t) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{}, nil) result, err := ss.VerifyVP(vp, nil) @@ -225,7 +263,7 @@ func TestValidator_VerifyVP(t *testing.T) { vpData, _ := os.ReadFile("./test/vp_missing_proof.json") _ = json.Unmarshal(vpData, &vp) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, nil) @@ -240,7 +278,7 @@ func TestValidator_VerifyVP(t *testing.T) { vpData, _ := os.ReadFile("./test/vp_incorrect_proof_type.json") _ = json.Unmarshal(vpData, &vp) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, nil) @@ -255,7 +293,7 @@ func TestValidator_VerifyVP(t *testing.T) { vpData, _ := os.ReadFile("./test/vp_incorrect_signer.json") _ = json.Unmarshal(vpData, &vp) ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil) result, err := ss.VerifyVP(vp, nil) @@ -272,7 +310,7 @@ func TestValidator_VerifyVP(t *testing.T) { _ = json.Unmarshal(vpData, &vp) credential.Issuer = did.MustParseDID("did:nuts:a").URI() ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) - mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{credential}, nil) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{credential}, nil) result, err := ss.VerifyVP(vp, nil) @@ -280,6 +318,21 @@ func TestValidator_VerifyVP(t *testing.T) { assert.Equal(t, contract.Invalid, result.Validity()) assert.Equal(t, "signer must be credentialSubject", result.Reason()) }) + + t.Run("error - issuer does not have trusted NutsOrganizationCredential", func(t *testing.T) { + mockContext := newMockContext(t) + ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates) + credentialWithoutRole := vc.VerifiableCredential{} + data, _ := os.ReadFile("./test/vc-without-role.json") + _ = json.Unmarshal(data, &credentialWithoutRole) + mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil) + mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{}, nil) + + result, err := ss.VerifyVP(vp, &vpValidTime) + + require.NoError(t, err) + assert.Empty(t, result.DisclosedAttribute(services.UserRoleClaim)) + }) } func Test_validateRequiredAttributes(t *testing.T) { @@ -378,3 +431,21 @@ func Test_selfsignedVerificationResult(t *testing.T) { assert.Equal(t, "test2", vr.DisclosedAttribute("dAttr1")) }) } + +func createOrganizationCredential(issuerDID string) vc.VerifiableCredential { + orgCred := vc.VerifiableCredential{ + Context: []ssi.URI{credential.NutsV1ContextURI}, + Type: []ssi.URI{ssi.MustParseURI("NutsOrganizationCredential")}, + Issuer: did.MustParseDID(issuerDID).URI(), + CredentialSubject: []interface{}{ + credential.NutsOrganizationCredentialSubject{ + ID: issuerDID, + Organization: map[string]string{ + "name": "CareBears", + "city": "CareTown", + }, + }, + }, + } + return orgCred +} diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index bbc8b3b564..2bf0e311d7 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -3,6 +3,16 @@ Release notes ############# +************************ +Hazelnut update (v5.3.2) +************************ + +Release date: 2023-08-20 + +- Fixed issue where NutsEmployeeCredentials needed to be explicitly trusted when issued by another node + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v5.3.1...v5.3.2 + ************************ Hazelnut update (v5.3.1) ************************ diff --git a/e2e-tests/auth/selfsigned/apps/selfsigned.go b/e2e-tests/auth/selfsigned/apps/selfsigned.go index 26f8ca7f6a..00c79bc9f8 100644 --- a/e2e-tests/auth/selfsigned/apps/selfsigned.go +++ b/e2e-tests/auth/selfsigned/apps/selfsigned.go @@ -25,6 +25,7 @@ import ( authAPI "github.com/nuts-foundation/nuts-node/auth/api/auth/v1/client" "github.com/nuts-foundation/nuts-node/core" "github.com/rs/zerolog/log" + vcrAPI "github.com/nuts-foundation/nuts-node/vcr/api/vcr/v2" ) var NodeClientConfig = core.ClientConfig{Address: "http://localhost:1323"} @@ -91,12 +92,28 @@ func (s SelfSigned) GetSessionStatus(sessionID string) (string, *authAPI.Verifia return response.JSON200.Status, response.JSON200.VerifiablePresentation, nil } -func (s SelfSigned) RequestAccessToken(organizationDID string, purposeOfUse string, presentation *authAPI.VerifiablePresentation) (*authAPI.TokenIntrospectionResponse, error) { +func (s SelfSigned) UntrustEmployeeCredential(issuerDID string) error { + vcrClient, _ := vcrAPI.NewClient(NodeClientConfig.Address) + resp, err := vcrClient.UntrustIssuer(s.Context, vcrAPI.UntrustIssuerJSONRequestBody{ + CredentialType: "NutsEmployeeCredential", + Issuer: issuerDID, + }) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 204 { + return fmt.Errorf("could not untrust issuer: %s", resp.Status) + } + return nil +} + +func (s SelfSigned) RequestAccessToken(requestorDID string, authorizerDID, purposeOfUse string, presentation *authAPI.VerifiablePresentation) (*authAPI.TokenIntrospectionResponse, error) { authClient, _ := authAPI.NewClient(s.URL) accessTokenResponse, err := authClient.RequestAccessToken(s.Context, authAPI.RequestAccessTokenJSONRequestBody{ - Authorizer: organizationDID, + Authorizer: authorizerDID, Identity: presentation, - Requester: organizationDID, + Requester: requestorDID, Service: purposeOfUse, }) if err != nil { diff --git a/e2e-tests/auth/selfsigned/main_test.go b/e2e-tests/auth/selfsigned/main_test.go index 85512c8da3..7bf5f4e8d8 100644 --- a/e2e-tests/auth/selfsigned/main_test.go +++ b/e2e-tests/auth/selfsigned/main_test.go @@ -51,11 +51,18 @@ func Test_LoginWithSelfSignedMeans(t *testing.T) { cancel() }() - organization, err := createDID() + verifyingOrganization, err := createDID() require.NoError(t, err) - err = registerCompoundService(organization.ID, purposeOfUse) + err = registerCompoundService(verifyingOrganization.ID, purposeOfUse) require.NoError(t, err) - err = issueOrganizationCredential(organization) + err = issueOrganizationCredential(verifyingOrganization, "Verifying Organization", "Testland") + require.NoError(t, err) + + issuingOrganization, err := createDID() + require.NoError(t, err) + err = registerCompoundService(issuingOrganization.ID, purposeOfUse) + require.NoError(t, err) + err = issueOrganizationCredential(issuingOrganization, "Issuing Organization", "Testland") require.NoError(t, err) selfSigned := apps.SelfSigned{ @@ -70,7 +77,7 @@ func Test_LoginWithSelfSignedMeans(t *testing.T) { RoleName: &roleName, } // Start a self-signed session - session, err := selfSigned.Start(organization.ID.String(), employeeInfo) + session, err := selfSigned.Start(issuingOrganization.ID.String(), employeeInfo) require.NoError(t, err) require.Equal(t, employeeInfo.Identifier, session.EmployeeIdentifier) require.Equal(t, employeeInfo.Initials+" "+employeeInfo.FamilyName, session.EmployeeName) @@ -86,10 +93,15 @@ func Test_LoginWithSelfSignedMeans(t *testing.T) { require.NoError(t, err) require.Equal(t, "completed", status) require.Equal(t, "NutsSelfSignedPresentation", presentation.Type[1].String()) - require.Equal(t, organization.ID.String(), presentation.VerifiableCredential[0].Issuer.String()) + require.Equal(t, issuingOrganization.ID.String(), presentation.VerifiableCredential[0].Issuer.String()) + + // Issue #2428: Issuer of NutsEmployeeCredential should not need to be trusted + // It's automatically trusted on issuance, and we've got a single node, so we're untrusting it. + err = selfSigned.UntrustEmployeeCredential(issuingOrganization.ID.String()) + require.NoError(t, err) // Now request an access token - accessToken, err := selfSigned.RequestAccessToken(organization.ID.String(), purposeOfUse, presentation) + accessToken, err := selfSigned.RequestAccessToken(issuingOrganization.ID.String(), verifyingOrganization.ID.String(), purposeOfUse, presentation) require.NoError(t, err) assert.Equal(t, "zorgtoepassing", *accessToken.Service) assert.Equal(t, "J", *accessToken.Initials) @@ -105,7 +117,7 @@ func Test_LoginWithSelfSignedMeans(t *testing.T) { } } -func issueOrganizationCredential(organization *did.Document) error { +func issueOrganizationCredential(organization *did.Document, name, city string) error { vcrClient := vcrAPI.HTTPClient{ClientConfig: apps.NodeClientConfig} visibility := vcrAPI.Public _, err := vcrClient.IssueVC(vcrAPI.IssueVCRequest{ @@ -114,8 +126,8 @@ func issueOrganizationCredential(organization *did.Document) error { CredentialSubject: map[string]interface{}{ "id": organization.ID.String(), "organization": map[string]interface{}{ - "name": "Test organization", - "city": "Testland", + "name": name, + "city": city, }, }, Visibility: &visibility, diff --git a/e2e-tests/storage/vault/docker-compose.yml b/e2e-tests/storage/vault/docker-compose.yml index f4565df188..4ee7c38761 100644 --- a/e2e-tests/storage/vault/docker-compose.yml +++ b/e2e-tests/storage/vault/docker-compose.yml @@ -18,7 +18,7 @@ services: VAULT_ADDR: http://vault:8200 VAULT_TOKEN: root vault: - image: vault + image: hashicorp/vault cap_add: - IPC_LOCK environment: diff --git a/vcr/api/vcr/v2/api.go b/vcr/api/vcr/v2/api.go index 90cc45f7bb..f6d4ed03ed 100644 --- a/vcr/api/vcr/v2/api.go +++ b/vcr/api/vcr/v2/api.go @@ -297,7 +297,7 @@ func (w *Wrapper) VerifyVP(ctx context.Context, request VerifyVPRequestObject) ( validAt = &parsedTime } - verifiedCredentials, err := w.VCR.Verifier().VerifyVP(request.Body.VerifiablePresentation, verifyCredentials, validAt) + verifiedCredentials, err := w.VCR.Verifier().VerifyVP(request.Body.VerifiablePresentation, verifyCredentials, false, validAt) if err != nil { if errors.Is(err, verifier.VerificationError{}) { msg := err.Error() diff --git a/vcr/api/vcr/v2/api_test.go b/vcr/api/vcr/v2/api_test.go index c97aeac59b..fd79da4e10 100644 --- a/vcr/api/vcr/v2/api_test.go +++ b/vcr/api/vcr/v2/api_test.go @@ -658,7 +658,7 @@ func TestWrapper_VerifyVP(t *testing.T) { VerifiablePresentation: vp, ValidAt: &validAtStr, } - testContext.mockVerifier.EXPECT().VerifyVP(vp, true, &validAt).Return(vp.VerifiableCredential, nil) + testContext.mockVerifier.EXPECT().VerifyVP(vp, true, false, &validAt).Return(vp.VerifiableCredential, nil) expectedResponse := VerifyVP200JSONResponse(VPVerificationResult{ Credentials: &expectedVCs, Validity: true, @@ -673,7 +673,7 @@ func TestWrapper_VerifyVP(t *testing.T) { testContext := newMockContext(t) verifyCredentials := false request := VPVerificationRequest{VerifiablePresentation: vp, VerifyCredentials: &verifyCredentials} - testContext.mockVerifier.EXPECT().VerifyVP(vp, false, nil).Return(vp.VerifiableCredential, nil) + testContext.mockVerifier.EXPECT().VerifyVP(vp, false, false, nil).Return(vp.VerifiableCredential, nil) expectedResponse := VerifyVP200JSONResponse(VPVerificationResult{ Credentials: &expectedVCs, Validity: true, @@ -687,7 +687,7 @@ func TestWrapper_VerifyVP(t *testing.T) { t.Run("error - verification failed (other error)", func(t *testing.T) { testContext := newMockContext(t) request := VPVerificationRequest{VerifiablePresentation: vp} - testContext.mockVerifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, errors.New("failed")) + testContext.mockVerifier.EXPECT().VerifyVP(vp, true, false, nil).Return(nil, errors.New("failed")) response, err := testContext.client.VerifyVP(testContext.requestCtx, VerifyVPRequestObject{Body: &request}) @@ -710,7 +710,7 @@ func TestWrapper_VerifyVP(t *testing.T) { t.Run("error - verification failed (verification error)", func(t *testing.T) { testContext := newMockContext(t) request := VPVerificationRequest{VerifiablePresentation: vp} - testContext.mockVerifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, verifier.VerificationError{}) + testContext.mockVerifier.EXPECT().VerifyVP(vp, true, false, nil).Return(nil, verifier.VerificationError{}) errMsg := "verification error: " expectedRepsonse := VerifyVP200JSONResponse(VPVerificationResult{ Message: &errMsg, diff --git a/vcr/verifier/interface.go b/vcr/verifier/interface.go index c0994027f3..793d92acf1 100644 --- a/vcr/verifier/interface.go +++ b/vcr/verifier/interface.go @@ -48,8 +48,8 @@ type Verifier interface { RegisterRevocation(revocation credential.Revocation) error // VerifyVP verifies the given Verifiable Presentation. If successful, it returns the credentials within the presentation. - // If verifyVCs is true, it will also verify the credentials inside the VP, checking their correctness, signature and trust status. - VerifyVP(presentation vc.VerifiablePresentation, verifyVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) + // If verifyVCs is true, it will also verify the credentials inside the VP, checking their correctness, signature and trust status (unless allowUntrustedVCs is true). + VerifyVP(presentation vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) } // ErrNotFound is returned when a credential or revocation can not be found based on its ID. diff --git a/vcr/verifier/mock.go b/vcr/verifier/mock.go index dc6f164ce6..1496df43db 100644 --- a/vcr/verifier/mock.go +++ b/vcr/verifier/mock.go @@ -111,18 +111,18 @@ func (mr *MockVerifierMockRecorder) Verify(credential, allowUntrusted, checkSign } // VerifyVP mocks base method. -func (m *MockVerifier) VerifyVP(presentation vc.VerifiablePresentation, verifyVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { +func (m *MockVerifier) VerifyVP(presentation vc.VerifiablePresentation, verifyVCs, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VerifyVP", presentation, verifyVCs, validAt) + ret := m.ctrl.Call(m, "VerifyVP", presentation, verifyVCs, allowUntrustedVCs, validAt) ret0, _ := ret[0].([]vc.VerifiableCredential) ret1, _ := ret[1].(error) return ret0, ret1 } // VerifyVP indicates an expected call of VerifyVP. -func (mr *MockVerifierMockRecorder) VerifyVP(presentation, verifyVCs, validAt interface{}) *gomock.Call { +func (mr *MockVerifierMockRecorder) VerifyVP(presentation, verifyVCs, allowUntrustedVCs, validAt interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyVP", reflect.TypeOf((*MockVerifier)(nil).VerifyVP), presentation, verifyVCs, validAt) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyVP", reflect.TypeOf((*MockVerifier)(nil).VerifyVP), presentation, verifyVCs, allowUntrustedVCs, validAt) } // MockStore is a mock of Store interface. diff --git a/vcr/verifier/verifier.go b/vcr/verifier/verifier.go index 8dce02d337..9ae018bc79 100644 --- a/vcr/verifier/verifier.go +++ b/vcr/verifier/verifier.go @@ -259,12 +259,12 @@ func (v *verifier) RegisterRevocation(revocation credential.Revocation) error { return nil } -func (v verifier) VerifyVP(vp vc.VerifiablePresentation, verifyVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { - return v.doVerifyVP(&v, vp, verifyVCs, validAt) +func (v verifier) VerifyVP(vp vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { + return v.doVerifyVP(&v, vp, verifyVCs, allowUntrustedVCs, validAt) } // doVerifyVP delegates VC verification to the supplied Verifier, to aid unit testing. -func (v verifier) doVerifyVP(vcVerifier Verifier, vp vc.VerifiablePresentation, verifyVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { +func (v verifier) doVerifyVP(vcVerifier Verifier, vp vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { // Multiple proofs might be supported in the future, when there's an actual use case. if len(vp.Proof) != 1 { return nil, newVerificationError("exactly 1 proof is expected") @@ -298,7 +298,7 @@ func (v verifier) doVerifyVP(vcVerifier Verifier, vp vc.VerifiablePresentation, if verifyVCs { for _, current := range vp.VerifiableCredential { - err := vcVerifier.Verify(current, false, true, validAt) + err := vcVerifier.Verify(current, allowUntrustedVCs, true, validAt) if err != nil { return nil, newVerificationError("invalid VC (id=%s): %w", current.ID, err) } diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go index d348d535f1..9bdeb95e7a 100644 --- a/vcr/verifier/verifier_test.go +++ b/vcr/verifier/verifier_test.go @@ -537,12 +537,12 @@ func TestVerifier_VerifyVP(t *testing.T) { ctx := newMockContext(t) ctx.keyResolver.EXPECT().ResolveSigningKey(vpSignerKeyID.String(), validAt).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) - vcs, err := ctx.verifier.VerifyVP(vp, false, validAt) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) assert.NoError(t, err) assert.Len(t, vcs, 1) }) - t.Run("ok - verify VCs", func(t *testing.T) { + t.Run("ok - verify VCs (and verify trusted)", func(t *testing.T) { _ = json.Unmarshal([]byte(rawVP), &vp) var validAt *time.Time @@ -553,7 +553,23 @@ func TestVerifier_VerifyVP(t *testing.T) { mockVerifier := NewMockVerifier(ctx.ctrl) mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, validAt) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) + + assert.NoError(t, err) + assert.Len(t, vcs, 1) + }) + t.Run("ok - verify VCs (do not need to be trusted)", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) + + var validAt *time.Time + + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveSigningKey(vpSignerKeyID.String(), validAt).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) + + mockVerifier := NewMockVerifier(ctx.ctrl) + mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], true, true, validAt) + + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, validAt) assert.NoError(t, err) assert.Len(t, vcs, 1) @@ -567,7 +583,7 @@ func TestVerifier_VerifyVP(t *testing.T) { mockVerifier := NewMockVerifier(ctx.ctrl) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, &validAt) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, &validAt) assert.EqualError(t, err, "verification error: presentation not valid at given time") assert.Empty(t, vcs) @@ -583,7 +599,7 @@ func TestVerifier_VerifyVP(t *testing.T) { mockVerifier := NewMockVerifier(ctx.ctrl) mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt).Return(errors.New("invalid")) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, validAt) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) assert.Error(t, err) assert.Empty(t, vcs) @@ -597,7 +613,7 @@ func TestVerifier_VerifyVP(t *testing.T) { // Return incorrect key, causing signature verification failure ctx.keyResolver.EXPECT().ResolveSigningKey(vpSignerKeyID.String(), validAt).Return(vdr.TestMethodDIDBPrivateKey().Public(), nil) - vcs, err := ctx.verifier.VerifyVP(vp, false, validAt) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) assert.EqualError(t, err, "verification error: invalid signature: invalid proof signature: failed to verify signature using ecdsa") assert.Empty(t, vcs) @@ -611,7 +627,7 @@ func TestVerifier_VerifyVP(t *testing.T) { // Return incorrect key, causing signature verification failure ctx.keyResolver.EXPECT().ResolveSigningKey(vpSignerKeyID.String(), validAt).Return(nil, vdrTypes.ErrKeyNotFound) - vcs, err := ctx.verifier.VerifyVP(vp, false, validAt) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) assert.ErrorIs(t, err, vdrTypes.ErrKeyNotFound) assert.Empty(t, vcs) @@ -625,7 +641,7 @@ func TestVerifier_VerifyVP(t *testing.T) { ctx := newMockContext(t) - vcs, err := ctx.verifier.VerifyVP(vp, false, validAt) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) assert.EqualError(t, err, "verification error: unsupported proof type: json: cannot unmarshal string into Go value of type proof.LDProof") assert.Empty(t, vcs) @@ -639,7 +655,7 @@ func TestVerifier_VerifyVP(t *testing.T) { ctx := newMockContext(t) - vcs, err := ctx.verifier.VerifyVP(vp, false, validAt) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) assert.EqualError(t, err, "verification error: exactly 1 proof is expected") assert.Empty(t, vcs)