diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddd90fc..039f2f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # @digitalcredentials/vc ChangeLog +## 6.0.0 - +### Changed +- **BREAKING**: Add a fallback/override for legacy OBv3 VCs. + ## 5.0.0 - 2022-11-03 ### Changed diff --git a/lib/index.js b/lib/index.js index 11503df4..f7e844d3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,6 +40,8 @@ const jsonld = require('@digitalcredentials/jsonld'); const jsigs = require('@digitalcredentials/jsonld-signatures'); const {AuthenticationProofPurpose} = jsigs.purposes; const CredentialIssuancePurpose = require('./CredentialIssuancePurpose'); +const wrapWithLegacyLoader = require('./legacyDocumentLoader'); + const defaultDocumentLoader = jsigs.extendContextLoader( require('./documentLoader')); const {constants: {CREDENTIALS_CONTEXT_V1_URL}} = @@ -251,12 +253,15 @@ async function verify(options = {}) { */ async function verifyCredential(options = {}) { const {credential} = options; + let result; try { if(!credential) { throw new TypeError( 'A "credential" property is required for verifying.'); } - return await _verifyCredential(options); + result = await _verifyCredential(options); + + return result; } catch(error) { if(error instanceof TypeError) { throw error; @@ -315,9 +320,20 @@ async function _verifyCredential(options = {}) { throw error; } - // if verification has already failed, skip status check if(!result.verified) { - return result; + const contexts = credential['@context']; + // Custom processing to handle legacy OBv3 BETA VCs + if(Array.isArray(contexts) && contexts + .includes('https://purl.imsglobal.org/spec/ob/v3p0/context.json')) { + + result = await _verifyOBv3LegacySignature(credential, + {purpose, documentLoader, ...options}); + } + + // if verification has already failed, skip status check + if(!result.verified) { + return result; + } } // run common credential checks (add check results to log) @@ -352,6 +368,23 @@ async function _verifyCredential(options = {}) { return result; } +async function _verifyOBv3LegacySignature(credential, + {purpose, documentLoader, ...options}) { + let result; + + const legacyLoader = wrapWithLegacyLoader(documentLoader); + try { + result = await jsigs.verify( + credential, {purpose, documentLoader: legacyLoader, ...options}); + } catch(error) { + error.log = error.log && + error.log.push({id: 'valid_signature', valid: false}); + throw error; + } + + return result; +} + /** * Creates an unsigned presentation from a given verifiable credential. * diff --git a/lib/legacyDocumentLoader.js b/lib/legacyDocumentLoader.js new file mode 100644 index 00000000..14504a58 --- /dev/null +++ b/lib/legacyDocumentLoader.js @@ -0,0 +1,20 @@ +/*! + * Copyright (c) 2023 Digital Credentials Consortium. All rights reserved. + */ +'use strict'; + +const obCtx = require('@digitalcredentials/open-badges-context'); + +module.exports = function wrapWithLegacyLoader(existingLoader) { + return async function documentLoader(url) { + if(url === 'https://purl.imsglobal.org/spec/ob/v3p0/context.json') { + return { + contextUrl: null, + documentUrl: url, + document: obCtx.contexts.get(obCtx.CONTEXT_URL_V3_BETA) + }; + } + + return existingLoader(url); + }; +}; diff --git a/package.json b/package.json index ff271d7a..35aa910a 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "dependencies": { "@digitalbazaar/vc-status-list": "^7.0.0", "@digitalcredentials/ed25519-signature-2020": "^3.0.2", - "@digitalcredentials/jsonld": "^5.2.2", - "@digitalcredentials/jsonld-signatures": "^9.3.1", - "@digitalcredentials/security-document-loader": "^2.0.0", + "@digitalcredentials/jsonld": "^6.0.0", + "@digitalcredentials/jsonld-signatures": "^9.3.2", + "@digitalcredentials/open-badges-context": "^2.0.1", "@digitalcredentials/vc-status-list": "^5.0.2", "credentials-context": "^2.0.0", "fix-esm": "^1.0.1" @@ -30,6 +30,7 @@ "@babel/runtime": "^7.13.9", "@digitalbazaar/ed25519-signature-2018": "^2.0.1", "@digitalbazaar/ed25519-verification-key-2018": "^3.0.0", + "@digitalcredentials/security-document-loader": "^3.1.0", "babel-loader": "^8.2.2", "chai": "^4.3.3", "cross-env": "^7.0.3", diff --git a/test/10-verify.spec.js b/test/10-verify.spec.js index e294fa48..ae0c2ff7 100644 --- a/test/10-verify.spec.js +++ b/test/10-verify.spec.js @@ -9,11 +9,11 @@ const {Ed25519VerificationKey2018} = const jsigs = require('@digitalcredentials/jsonld-signatures'); const jsonld = require('@digitalcredentials/jsonld'); const {Ed25519Signature2018} = require('@digitalbazaar/ed25519-signature-2018'); -const {Ed25519Signature2020} = require('@digitalcredentials/ed25519-signature-2020'); +const {Ed25519Signature2020} = + require('@digitalcredentials/ed25519-signature-2020'); const CredentialIssuancePurpose = require('../lib/CredentialIssuancePurpose'); -const { checkStatus } = require("fix-esm").require('@digitalcredentials/vc-status-list'); - -const { securityLoader } = require('@digitalcredentials/security-document-loader'); +const {securityLoader} = + require('@digitalcredentials/security-document-loader'); const mockData = require('./mocks/mock.data'); const {v4: uuid} = require('uuid'); @@ -22,6 +22,7 @@ const MultiLoader = require('./MultiLoader'); const realContexts = require('../lib/contexts'); const invalidContexts = require('./contexts'); const mockCredential = require('./mocks/credential'); +const legacyOBv3Credential = require('./mocks/credential-legacy-obv3'); const assertionController = require('./mocks/assertionController'); const mockDidDoc = require('./mocks/didDocument'); const mockDidKeys = require('./mocks/didKeys'); @@ -161,58 +162,16 @@ describe('vc.signPresentation()', () => { }); describe('verify API (credentials)', () => { - it.skip('should verify a vc', async () => { - // verifiableCredential = await vc.issue({ - // credential: mockCredential, - // suite - // }); - const documentLoader = securityLoader({ fetchRemoteContexts: true }).build() - const verifiableCredential = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/ed25519-2020/v1", - "https://w3id.org/vc/status-list/2021/v1" - ], - "id": "https://credentials-dcc-credentials-dev.raccoongang.com/verifiable_credentials/api/v1/status-list/2021/v1/did:key:z6MkkePoGJV8CQJJULSHHUEv71okD9PsrqXnZpNQuoUfb3id/", - "type": [ - "VerifiableCredential", - "StatusList2021Credential" - ], - "credentialSubject": { - "id": "https://credentials-dcc-credentials-dev.raccoongang.com/verifiable_credentials/api/v1/status-list/2021/v1/did:key:z6MkkePoGJV8CQJJULSHHUEv71okD9PsrqXnZpNQuoUfb3id/#list", - "encodedList": "H4sIAHo1bmQC/+3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAODfAC7KO00QJwAA", - "statusPurpose": "revocation", - "type": "StatusList2021" - }, - "issuer": { - "id": "did:key:z6MkkePoGJV8CQJJULSHHUEv71okD9PsrqXnZpNQuoUfb3id" - }, - "issuanceDate": "2023-05-24T16:02:32Z", - "proof": { - "type": "Ed25519Signature2020", - "proofPurpose": "assertionMethod", - "proofValue": "zyvtkuyYKhCgJ62j2oRvzmqADcpDzRhFvdcdbeVtEYnTeZ8zjhXNoyPvvUnmNUMEb13Ua7kSr1p5Lxp5EFup9Q5z", - "verificationMethod": "did:key:z6MkkePoGJV8CQJJULSHHUEv71okD9PsrqXnZpNQuoUfb3id#z6MkkePoGJV8CQJJULSHHUEv71okD9PsrqXnZpNQuoUfb3id", - "created": "2023-05-24T16:04:10.179Z" - }, - "validFrom": "2023-05-24T16:02:32Z", - "issued": "2023-05-24T16:02:32Z" - } - const suite = new Ed25519Signature2020() - + it('should verify an OBv3 vc', async () => { const result = await vc.verifyCredential({ - credential: verifiableCredential, - checkStatus, - suite, - documentLoader + credential: legacyOBv3Credential, + suite: new Ed25519Signature2020(), + documentLoader: securityLoader().build() }); if(result.error) { throw result.error; } - - console.log(result) - result.verified.should.be.true; result.results[0].log.should.eql([ @@ -381,8 +340,8 @@ describe('verify API (presentations)', () => { const {presentation, suite: vcSuite, documentLoader} = await _generatePresentation({unsigned: true}); - console.log(JSON.stringify(presentation, null, 2)) - console.log(vcSuite) + console.log(JSON.stringify(presentation, null, 2)); + console.log(vcSuite); const result = await vc.verify({ documentLoader, diff --git a/test/20-checkStatus.spec.js b/test/20-checkStatus.spec.js index a88f92d9..1cf606f3 100644 --- a/test/20-checkStatus.spec.js +++ b/test/20-checkStatus.spec.js @@ -1,55 +1,56 @@ -const { checkStatus } = require("fix-esm").require('@digitalcredentials/vc-status-list'); +/* eslint-disable */ +const {checkStatus} = require('fix-esm').require('@digitalcredentials/vc-status-list'); const {Ed25519Signature2020} = require('@digitalcredentials/ed25519-signature-2020'); -const { securityLoader } = require('@digitalcredentials/security-document-loader'); +const {securityLoader} = require('@digitalcredentials/security-document-loader'); const vc = require('..'); const mockCredential = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/ed25519-2020/v1", - "https://w3id.org/dcc/v1", - "https://w3id.org/vc/status-list/2021/v1" + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + 'https://w3id.org/dcc/v1', + 'https://w3id.org/vc/status-list/2021/v1' ], - "type": [ - "VerifiableCredential", - "Assertion" + type: [ + 'VerifiableCredential', + 'Assertion' ], - "issuer": { - "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", - "name": "Example University", - "url": "https://cs.example.edu", - "image": "https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png" + issuer: { + id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', + name: 'Example University', + url: 'https://cs.example.edu', + image: 'https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png' }, - "issuanceDate": "2020-08-16T12:00:00.000+00:00", - "credentialSubject": { - "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", - "name": "Kayode Ezike", - "hasCredential": { - "type": [ - "EducationalOccupationalCredential" + issuanceDate: '2020-08-16T12:00:00.000+00:00', + credentialSubject: { + id: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', + name: 'Kayode Ezike', + hasCredential: { + type: [ + 'EducationalOccupationalCredential' ], - "name": "GT Guide", - "description": "The holder of this credential is qualified to lead new student orientations." + name: 'GT Guide', + description: 'The holder of this credential is qualified to lead new student orientations.' } }, - "expirationDate": "2025-08-16T12:00:00.000+00:00", - "credentialStatus": { - "id": "https://digitalcredentials.github.io/credential-status-playground/JWZM3H8WKU#3", - "type": "StatusList2021Entry", - "statusPurpose": "revocation", - "statusListIndex": 3, - "statusListCredential": "https://digitalcredentials.github.io/credential-status-playground/JWZM3H8WKU" + expirationDate: '2025-08-16T12:00:00.000+00:00', + credentialStatus: { + id: 'https://digitalcredentials.github.io/credential-status-playground/JWZM3H8WKU#3', + type: 'StatusList2021Entry', + statusPurpose: 'revocation', + statusListIndex: 3, + statusListCredential: 'https://digitalcredentials.github.io/credential-status-playground/JWZM3H8WKU' }, - "proof": { - "type": "Ed25519Signature2020", - "created": "2022-08-19T06:58:29Z", - "verificationMethod": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC#z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", - "proofPurpose": "assertionMethod", - "proofValue": "z33Wy3kvx8UEoPHdQWYHVCXAjW19AZpA88NnikwfJqcH9oNmHyqSkt6wiVS31ewytAX7m2vneVEm8Awo4xzqKHYUp" + proof: { + type: 'Ed25519Signature2020', + created: '2022-08-19T06:58:29Z', + verificationMethod: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC#z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC', + proofPurpose: 'assertionMethod', + proofValue: 'z33Wy3kvx8UEoPHdQWYHVCXAjW19AZpA88NnikwfJqcH9oNmHyqSkt6wiVS31ewytAX7m2vneVEm8Awo4xzqKHYUp' } -} +}; -const documentLoader = securityLoader().build() +const documentLoader = securityLoader().build(); describe('checkStatus', () => { it.skip('should verify', async () => { @@ -61,6 +62,6 @@ describe('checkStatus', () => { checkStatus }); - console.log(JSON.stringify(result, null, 2)) - }) -}) + console.log(JSON.stringify(result, null, 2)); + }); +}); diff --git a/test/mocks/credential-legacy-obv3.js b/test/mocks/credential-legacy-obv3.js new file mode 100644 index 00000000..09375d6e --- /dev/null +++ b/test/mocks/credential-legacy-obv3.js @@ -0,0 +1,42 @@ +/* eslint-disable */ +const credential = { + "@context": ["https://www.w3.org/2018/credentials/v1", "https://purl.imsglobal.org/spec/ob/v3p0/context.json", "https://w3id.org/security/suites/ed25519-2020/v1"], + "id": "urn:uuid:e7af51df-d51f-4ac3-bb57-c229c0e61679", + "type": ["VerifiableCredential", "OpenBadgeCredential"], + "name": "Digital Credentials Consortium Demo", + "issuer": { + "type": ["Profile"], + "id": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", + "name": "Digital Credentials Consortium", + "url": "https://dcconsortium.org/", + "image": "https://user-images.githubusercontent.com/752326/230469660-8f80d264-eccf-4edd-8e50-ea634d407778.png" + }, + "issuanceDate": "2023-04-13T21:00:48.141Z", + "credentialSubject": { + "type": ["AchievementSubject"], + "achievement": { + "id": "urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922", + "type": ["Achievement"], + "achievementType": "Badge", + "name": "Digital Credentials Consortium Demo", + "description": "Digital Credentials Consortium demo credential.", + "criteria": { + "type": "Criteria", + "narrative": "The recipient successfully installed Learner Credential Wallet (https://lcw.app/) and added a credential." + }, + "image": { + "id": "https://user-images.githubusercontent.com/752326/214947713-15826a3a-b5ac-4fba-8d4a-884b60cb7157.png", + "type": "Image" + } + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-04-13T21:00:48Z", + "verificationMethod": "did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC#z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC", + "proofPurpose": "assertionMethod", + "proofValue": "z5pBsZaMcEv76AvDtsWpNrCB2ZXp3ZVXSxdQovH8AVV5E8k8jUTpnZ8fFSDHHEdewq544Cdi2shH8gJdj6xidcxCz" + } +} + +module.exports = credential