diff --git a/src/ScopeRequest.js b/src/ScopeRequest.js index beb79dc..d1ede5e 100644 --- a/src/ScopeRequest.js +++ b/src/ScopeRequest.js @@ -53,6 +53,11 @@ const isValidEvidenceChannelDetails = (channelDetails) => { return result; }; +const isValidCredentialMeta = (credentialItem, constraints) => { + const credentialMeta = VC.getCredentialMeta(credentialItem); + return VC.isMatchCredentialMeta(credentialMeta, constraints); +}; + /** * Class for generating Scope Requests */ @@ -61,9 +66,10 @@ class ScopeRequest { * * @param credentialItems - A list of credentialItems to check * @param request - Original ScopeRequest + * @param checkCredentialMeta - If true, check credential meta * @return {boolean} */ - static async credentialsMatchesRequest(credentialItems, request) { + static async credentialsMatchesRequest(credentialItems, request, checkCredentialMeta = false) { let result = true; const requestedItems = _.get(request, 'credentialItems'); @@ -88,7 +94,8 @@ class ScopeRequest { const constraints = _.get(requestedItem, 'constraints'); const match = verifiableCredential.isMatch(constraints); - if (!match) { + const matchCredentialMeta = (!checkCredentialMeta) || isValidCredentialMeta(credentialItem, constraints); + if (!match || !matchCredentialMeta) { // no need to continue breaking and returning false result = false; return false; diff --git a/test/fixtures/emailV3Cred.json b/test/fixtures/emailV3Cred.json new file mode 100644 index 0000000..57a6854 --- /dev/null +++ b/test/fixtures/emailV3Cred.json @@ -0,0 +1,153 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.identity.com/credentials/v3" + ], + "id": "852854bf-86aa-47a8-8570-6911b439342b", + "identifier": "credential-cvc:Email-v3", + "issuer": "did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn", + "issuanceDate": "2023-02-02T13:12:39.394Z", + "type": [ + "VerifiableCredential", + "IdentityCredential" + ], + "credentialSubject": { + "id": "did:sol:QuhsMqH2DTCUSYkCdvE9fcaq7pER7iYd8BRJmdBpwEC", + "contact": { + "email": { + "domain": { + "name": "civic", + "tld": "com" + }, + "username": "lucas" + } + } + }, + "proof": { + "type": "CvcMerkleProof2018", + "merkleRoot": "0152d816dc649aea82473cdb7467de9eecd6dafb5bc72059d7d92ea012dbf6dd", + "anchor": "TBD (Civic Blockchain Attestation)", + "leaves": [ + { + "identifier": "claim-cvc:Contact.email-v1", + "value": "urn:email.domain.name:56a8b5dc58caa0ed416a82eb48cbf7e1dee4449e6a289839037ccf7a79be0f61:civic|urn:email.domain.tld:92c4c27240f59ff2e1cf02e8d34c406f54f9ca3ebd802719354fb5081a9beca5:com|urn:email.username:85018d3234c84b7adc22cfc0682c9610b0ed2e7d2f9fd8d0bc73cc2d823d089a:lucas|", + "claimPath": "contact.email", + "targetHash": "5e9773b0433acea7e150f5d62a716789a1679d7fdc69910b55b0b462811da8ba", + "node": [ + { + "right": "2c19051202e3da553836b89f16646fbf718be0d84090484b3840589aeedab66b" + }, + { + "right": "e3ef4f46b4f635bac2fab6b779510e97a7d4f1b18374de4089edc9fc4c4a25ed" + }, + { + "right": "bd4f9f71c2b6b93e6641e11b781e5188c24641952f9690bba80fc16a66f224c9" + }, + { + "right": "7ddd7075dc704b653d1f24cec85114f743e593f88ab6bcb8217434b9bedab489" + }, + { + "right": "3c69f2bb8f4c402be9e2522bb1544b56d401b7ddb5fe263f71ee9f89d551ed83" + } + ] + }, + { + "identifier": "claim-cvc:Email.domain-v1", + "value": "urn:domain.name:56a8b5dc58caa0ed416a82eb48cbf7e1dee4449e6a289839037ccf7a79be0f61:civic|urn:domain.tld:92c4c27240f59ff2e1cf02e8d34c406f54f9ca3ebd802719354fb5081a9beca5:com|", + "claimPath": "contact.email.domain", + "targetHash": "2c19051202e3da553836b89f16646fbf718be0d84090484b3840589aeedab66b", + "node": [ + { + "left": "5e9773b0433acea7e150f5d62a716789a1679d7fdc69910b55b0b462811da8ba" + }, + { + "right": "e3ef4f46b4f635bac2fab6b779510e97a7d4f1b18374de4089edc9fc4c4a25ed" + }, + { + "right": "bd4f9f71c2b6b93e6641e11b781e5188c24641952f9690bba80fc16a66f224c9" + }, + { + "right": "7ddd7075dc704b653d1f24cec85114f743e593f88ab6bcb8217434b9bedab489" + }, + { + "right": "3c69f2bb8f4c402be9e2522bb1544b56d401b7ddb5fe263f71ee9f89d551ed83" + } + ] + }, + { + "identifier": "cvc:Meta:issuer", + "value": "urn:issuer:542609fea6749d154bec471a5140a004593f239417a082580e21485312637339:did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn|", + "claimPath": "meta.issuer", + "targetHash": "82c70faffba5e4507214549dd527c4a1f088ad65cf7cce6d53b83a38dbd6c64e", + "node": [ + { + "right": "8c00eea4b9e4ff41013ac92b4bd7e9dd6a21e2a76ed31b930fd9de17325438dd" + }, + { + "left": "a8dcbe1accb61ad409b19ff02129bef53df1c10e91b0f762885ccb433c6e7a9f" + }, + { + "right": "bd4f9f71c2b6b93e6641e11b781e5188c24641952f9690bba80fc16a66f224c9" + }, + { + "right": "7ddd7075dc704b653d1f24cec85114f743e593f88ab6bcb8217434b9bedab489" + }, + { + "right": "3c69f2bb8f4c402be9e2522bb1544b56d401b7ddb5fe263f71ee9f89d551ed83" + } + ] + }, + { + "identifier": "cvc:Meta:issuanceDate", + "value": "urn:issuanceDate:9e1f567c70f07536bcf604a5f03fb9e2fe241871b579fa9757085a00e61d698f:2023-02-02T13:12:39.394Z|", + "claimPath": "meta.issuanceDate", + "targetHash": "8c00eea4b9e4ff41013ac92b4bd7e9dd6a21e2a76ed31b930fd9de17325438dd", + "node": [ + { + "left": "82c70faffba5e4507214549dd527c4a1f088ad65cf7cce6d53b83a38dbd6c64e" + }, + { + "left": "a8dcbe1accb61ad409b19ff02129bef53df1c10e91b0f762885ccb433c6e7a9f" + }, + { + "right": "bd4f9f71c2b6b93e6641e11b781e5188c24641952f9690bba80fc16a66f224c9" + }, + { + "right": "7ddd7075dc704b653d1f24cec85114f743e593f88ab6bcb8217434b9bedab489" + }, + { + "right": "3c69f2bb8f4c402be9e2522bb1544b56d401b7ddb5fe263f71ee9f89d551ed83" + } + ] + }, + { + "identifier": "cvc:Meta:expirationDate", + "value": "urn:expirationDate:6b4f012ea5cfce84ca08c49941c64883a8d5955627b35c830a641b3e29ead7e0:null|", + "claimPath": "meta.expirationDate", + "targetHash": "978807d6506bc2748e7976c515df311e247be299b0d7237e3bea0e702b30fc7d", + "node": [ + { + "right": "91dfdf2b1a4072f6316c2241d0cbd3b9efc9159f061d23b7f60f0f38bf7b3217" + }, + { + "right": "17b28becc9cd28693064fb80120d8fd00e9150337db6d2f1b3b8767326c14274" + }, + { + "left": "a92335740e6ad78e0dcd0758459a5b21f9fa62e462803cc939166d9380a590b1" + }, + { + "right": "7ddd7075dc704b653d1f24cec85114f743e593f88ab6bcb8217434b9bedab489" + }, + { + "right": "3c69f2bb8f4c402be9e2522bb1544b56d401b7ddb5fe263f71ee9f89d551ed83" + } + ] + } + ], + "merkleRootSignature": { + "signature": "a67f362f6e8ff8c7c11e832d1587c4e0bc22c669f2f3bd69c37603628fbc3db0c40840ffec74a4ce4deafc93980e652dbe7e0c938911d917287cbc0568b64b08", + "verificationMethod": "did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn#default" + }, + "granted": null + } +} \ No newline at end of file diff --git a/test/unit/ScopeRequest.test.js b/test/unit/ScopeRequest.test.js index 9c0f2f9..bf8553b 100644 --- a/test/unit/ScopeRequest.test.js +++ b/test/unit/ScopeRequest.test.js @@ -53,6 +53,7 @@ const { ScopeRequest, buildSignedRequestBody, verifySignedRequestBody } = requir // -----Fixtures const idDoc = require('../fixtures/idDocCred'); +const emailV3Doc = require('../fixtures/emailV3Cred'); const filteredIdDoc = require('../fixtures/idDocV2Filtered.json'); describe('DSR Factory Tests', () => { @@ -631,7 +632,7 @@ describe('DSR Request Utils', () => { expect(dsr).toBeDefined(); }); - it('Should check is credentials matches the request constraints', async () => { + it('Should check if credentials matches the request constraints', async () => { const credentialItems = [idDoc]; // This is should be the CI on the scopeRequest response const dsr = await ScopeRequest.create('abcd', [{ @@ -651,7 +652,47 @@ describe('DSR Request Utils', () => { expect(match).toBeTruthy(); }); - it('Should check if partial credentials matches the request constraints ', async () => { + it('Should check if a v3 credential matches the request constraints', async () => { + const credentialItems = [emailV3Doc]; + const dsr = await ScopeRequest.create('abcd', + [{ + identifier: 'credential-cvc:Email-v3', + credential: 'credential-cvc:Email-v3', + constraints: { + meta: { + issuer: { is: { $eq: 'did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn' } }, + }, + claims: [ + { path: 'contact.email.domain.name', is: { $eq: 'civic' } }, + ], + }, + }]); + expect(dsr).toBeDefined(); + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr); + expect(match).toBeTruthy(); + }); + + it('Should check if a v3 credential matches the request constraints including the credential meta', async () => { + const credentialItems = [emailV3Doc]; + const dsr = await ScopeRequest.create('abcd', + [{ + identifier: 'credential-cvc:Email-v3', + credential: 'credential-cvc:Email-v3', + constraints: { + meta: { + issuer: { is: { $eq: 'did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn' } }, + }, + claims: [ + { path: 'contact.email.domain.name', is: { $eq: 'civic' } }, + ], + }, + }]); + expect(dsr).toBeDefined(); + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr, true); + expect(match).toBeTruthy(); + }); + + it('Should check if partial credentials matches the request constraints', async () => { const credentialItems = [filteredIdDoc]; // This is should be the CI on the scopeRequest response const dsr = await ScopeRequest.create('abcd', [{ @@ -667,11 +708,11 @@ describe('DSR Request Utils', () => { }, }]); expect(dsr).toBeDefined(); - const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr); + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr, true); expect(match).toBeTruthy(); }); - it('Should fail ckeck is credentials matches the request constraints', async () => { + it('Should fail ckeck if credentials dont match the request constraints', async () => { const credentialItems = [idDoc]; // This is should be the CI on the scopeRequest response const dsr = await ScopeRequest.create('abcd', [{ @@ -690,6 +731,68 @@ describe('DSR Request Utils', () => { expect(match).toBeFalsy(); }); + it('Should fail check if v3 credential doesnt match the request constraints', async () => { + const credentialItems = [emailV3Doc]; + const dsr = await ScopeRequest.create('abcd', + [{ + identifier: 'credential-cvc:Email-v3', + credential: 'credential-cvc:Email-v3', + constraints: { + meta: { + issuer: { is: { $eq: 'did:sol:tid652xmv91UHLW3HKnQSYMoNYko6FWd8sUEuYF5LPn' } }, + }, + claims: [ + { path: 'contact.email.domain.name', is: { $eq: 'gmail' } }, + ], + }, + }]); + expect(dsr).toBeDefined(); + // will fail check because the credential domain is civic (not gmail) + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr); + expect(match).toBeFalsy(); + }); + + it('Should fail ckeck if a credential fails the credential meta check', async () => { + const credentialItems = [idDoc]; + const dsr = await ScopeRequest.create('abcd', + [{ + identifier: 'credential-cvc:IdDocument-v1', + credential: 'credential-cvc:IdDocument-v1', + constraints: { + meta: { + issuer: { is: { $eq: 'did:ethr:different-issuer' } }, + }, + claims: [ + { path: 'document.dateOfBirth', is: { $lte: '-21y' } }, + ], + }, + }]); + expect(dsr).toBeDefined(); + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr, true); + expect(match).toBeFalsy(); + }); + + it('Should fail check if v3 credential fails the credential meta check', async () => { + const credentialItems = [emailV3Doc]; + const dsr = await ScopeRequest.create('abcd', + [{ + identifier: 'credential-cvc:Email-v3', + credential: 'credential-cvc:Email-v3', + constraints: { + meta: { + issuer: { is: { $eq: 'did:sol:different-issuer' } }, + }, + claims: [ + { path: 'contact.email.domain.name', is: { $eq: 'civic' } }, + ], + }, + }]); + expect(dsr).toBeDefined(); + // will fail check because the credential domain is civic (not gmail) + const match = await ScopeRequest.credentialsMatchesRequest(credentialItems, dsr, true); + expect(match).toBeFalsy(); + }); + it('Should throw with empty credentialItems', async () => { const credentialItems = []; // This is should be the CI on the scopeRequest response const dsr = await ScopeRequest.create('abcd',