diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f108f54..f96b535a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @digitalcredentials/vc ChangeLog +## 4.1.0 - 2022-07-06 + +### Added +- Add fine grained verification event `log` parameter to `verifyCredential()` + results. + ## 4.0.0 - 2022-07-06 ### Changed diff --git a/lib/CredentialIssuancePurpose.js b/lib/CredentialIssuancePurpose.js index a3437116..c7fb324f 100644 --- a/lib/CredentialIssuancePurpose.js +++ b/lib/CredentialIssuancePurpose.js @@ -3,7 +3,8 @@ */ 'use strict'; const jsonld = require('@digitalcredentials/jsonld'); -const {AssertionProofPurpose} = require('@digitalcredentials/jsonld-signatures').purposes; +const {AssertionProofPurpose} = + require('@digitalcredentials/jsonld-signatures').purposes; /** * Creates a proof purpose that will validate whether or not the verification diff --git a/lib/index.js b/lib/index.js index 5069932c..73f554f7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -101,6 +101,7 @@ module.exports = { * @property {object} statusResult * @property {Array} results * @property {object} error + * @property {object[]} log - Optional event log. */ /** @@ -251,11 +252,11 @@ async function verifyCredential(options = {}) { throw new TypeError( 'A "credential" property is required for verifying.'); } - return _verifyCredential(options); + return await _verifyCredential(options); } catch(error) { return { verified: false, - results: [{credential, verified: false, error}], + results: [{credential, verified: false, error, log: error.log}], error }; } @@ -285,8 +286,13 @@ async function verifyCredential(options = {}) { async function _verifyCredential(options = {}) { const {credential, checkStatus, now} = options; - // run common credential checks - _checkCredential({credential, now}); + // Fine grained result log containing checks performed. Example: + // [{id: 'valid_signature', valid: true}, + // {id: 'expiration', valid: true}] + const log = []; + + // run common credential checks (add check results to log) + _checkCredential({credential, log, now}); // if credential status is provided, a `checkStatus` function must be given if(credential.credentialStatus && typeof options.checkStatus !== 'function') { @@ -302,21 +308,40 @@ async function _verifyCredential(options = {}) { controller }); - const result = await jsigs.verify( - credential, {purpose, documentLoader, ...options}); + let result; + try { + result = await jsigs.verify( + credential, {purpose, documentLoader, ...options}); + } catch(error) { + log.push({id: 'valid_signature', valid: false}); + error.log = log; + throw error; + } // if verification has already failed, skip status check if(!result.verified) { return result; } - if(credential.credentialStatus) { + log.push({id: 'valid_signature', valid: true}); + log.push({id: 'issuer_did_resolves', valid: true}); + + if(checkStatus || credential.credentialStatus) { result.statusResult = await checkStatus(options); + if(!result.statusResult.verified) { result.verified = false; + log.push({id: 'revocation_status', valid: false}); + } else { + log.push({id: 'revocation_status', valid: true}); } + } else { + log.push({id: 'revocation_status', valid: true}); + } + result.log = log; + if(result.results) { + result.results[0].log = log; } - return result; } @@ -537,12 +562,14 @@ function _checkPresentation(presentation) { * @param {object} options - Options hashmap. * @param {object} options.credential - An object that could be a * VerifiableCredential. + * @param {object[]} [options.log] - Optional events log, for fine-grained + * verification result reporting. * @param {string|Date} [options.now] - A string representing date time in * ISO 8601 format or an instance of Date. Defaults to current date time. * @throws {Error} * @private */ -function _checkCredential({credential, now = new Date()}) { +function _checkCredential({credential, log = [], now = new Date()}) { if(typeof now === 'string') { now = new Date(now); } @@ -636,14 +663,21 @@ function _checkCredential({credential, now = new Date()}) { const {expirationDate} = credential; // check if `expirationDate` property is a date if(!dateRegex.test(expirationDate)) { - throw new Error( + log.push({id: 'expiration', valid: false}); + const error = new Error( `"expirationDate" must be a valid date: ${expirationDate}`); + error.log = log; + throw error; } // check if `now` is after `expirationDate` if(now > new Date(expirationDate)) { - throw new Error('Credential has expired.'); + log.push({id: 'expiration', valid: false}); + const error = new Error('Credential has expired.'); + error.log = log; + throw error; } } + log.push({id: 'expiration', valid: true}); } function _validateUriId({id, propertyName}) { diff --git a/package.json b/package.json index 69416169..8b692482 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "Credential" ], "scripts": { - "test": "npm run test-node && npm run test-karma", + "test": "npm run test-node", "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 10000 test/*.spec.js", "test-karma": "karma start karma.conf.js", "lint": "eslint lib test/*.spec.js", diff --git a/test/10-verify.spec.js b/test/10-verify.spec.js index 634f28db..35e8ab91 100644 --- a/test/10-verify.spec.js +++ b/test/10-verify.spec.js @@ -173,6 +173,13 @@ describe('verify API (credentials)', () => { throw result.error; } result.verified.should.be.true; + + result.results[0].log.should.eql([ + {id: 'expiration', valid: true}, + {id: 'valid_signature', valid: true}, + {id: 'issuer_did_resolves', valid: true}, + {id: 'revocation_status', valid: true} + ]); }); it('should verify a vc with a positive status check', async () => { @@ -192,6 +199,13 @@ describe('verify API (credentials)', () => { throw result.error; } result.verified.should.be.true; + + result.results[0].log.should.eql([ + {id: 'expiration', valid: true}, + {id: 'valid_signature', valid: true}, + {id: 'issuer_did_resolves', valid: true}, + {id: 'revocation_status', valid: true} + ]); }); describe('negative test', async () => { @@ -288,7 +302,14 @@ describe('verify API (credentials)', () => { if(result.error) { throw result.error; } - result.verified.should.be.true; + result.verified.should.be.false; + + result.results[0].log.should.eql([ + {id: 'expiration', valid: true}, + {id: 'valid_signature', valid: true}, + {id: 'issuer_did_resolves', valid: true}, + {id: 'revocation_status', valid: false} + ]); }); }); });