Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a fallback/override for legacy OBv3 VCs. #13

Merged
merged 2 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
67 changes: 50 additions & 17 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}} =
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -469,22 +502,10 @@ async function signPresentation(options = {}) {
async function _verifyPresentation(options = {}) {
const {presentation, unsignedPresentation} = options;

const documentLoader = options.documentLoader || defaultDocumentLoader;

const {controller, domain, challenge} = options;
if(!options.presentationPurpose && !challenge) {
throw new Error(
'A "challenge" param is required for AuthenticationProofPurpose.');
}

const purpose = options.presentationPurpose ||
new AuthenticationProofPurpose({controller, domain, challenge});

const presentationResult = await jsigs.verify(
presentation, {purpose, documentLoader, ...options});

_checkPresentation(presentation);

const documentLoader = options.documentLoader || defaultDocumentLoader;

// if verifiableCredentials are present, verify them, individually
let credentialResults;
let verified = true;
Expand All @@ -510,6 +531,18 @@ async function _verifyPresentation(options = {}) {
return {verified, results: [presentation], credentialResults};
}

const {controller, domain, challenge} = options;
if(!options.presentationPurpose && !challenge) {
throw new Error(
'A "challenge" param is required for AuthenticationProofPurpose.');
}

const purpose = options.presentationPurpose ||
new AuthenticationProofPurpose({controller, domain, challenge});

const presentationResult = await jsigs.verify(
presentation, {purpose, documentLoader, ...options});

return {
presentationResult,
verified: verified && presentationResult.verified,
Expand Down
20 changes: 20 additions & 0 deletions lib/legacyDocumentLoader.js
Original file line number Diff line number Diff line change
@@ -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);
};
};
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
"lib/**/*.js"
],
"dependencies": {
"@digitalcredentials/jsonld": "^5.2.1",
"@digitalcredentials/jsonld-signatures": "^9.3.1",
"credentials-context": "^2.0.0"
"@digitalbazaar/vc-status-list": "^7.0.0",
"@digitalcredentials/ed25519-signature-2020": "^3.0.2",
"@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"
},
"devDependencies": {
"@babel/core": "^7.13.8",
Expand All @@ -25,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",
Expand Down Expand Up @@ -76,7 +82,7 @@
],
"scripts": {
"test": "npm run test-node",
"test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 10000 test/*.spec.js",
"test-node": "cross-env NODE_ENV=test mocha -b --preserve-symlinks -t 10000 test/*.spec.js",
"test-karma": "karma start karma.conf.js",
"lint": "eslint lib test/*.spec.js",
"coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm run test-node",
Expand Down
21 changes: 12 additions & 9 deletions test/10-verify.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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 CredentialIssuancePurpose = require('../lib/CredentialIssuancePurpose');
const {securityLoader} =
require('@digitalcredentials/security-document-loader');

const mockData = require('./mocks/mock.data');
const {v4: uuid} = require('uuid');
Expand All @@ -18,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');
Expand Down Expand Up @@ -157,16 +162,11 @@ describe('vc.signPresentation()', () => {
});

describe('verify API (credentials)', () => {
it('should verify a vc', async () => {
verifiableCredential = await vc.issue({
credential: mockCredential,
suite
});
it('should verify an OBv3 vc', async () => {
const result = await vc.verifyCredential({
credential: verifiableCredential,
controller: assertionController,
suite,
documentLoader
credential: legacyOBv3Credential,
suite: new Ed25519Signature2020(),
documentLoader: securityLoader().build()
});

if(result.error) {
Expand Down Expand Up @@ -340,6 +340,9 @@ describe('verify API (presentations)', () => {
const {presentation, suite: vcSuite, documentLoader} =
await _generatePresentation({unsigned: true});

console.log(JSON.stringify(presentation, null, 2));
console.log(vcSuite);

const result = await vc.verify({
documentLoader,
presentation,
Expand Down
67 changes: 67 additions & 0 deletions test/20-checkStatus.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* 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 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'
],
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'
},
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.'
}
},
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'
}
};

const documentLoader = securityLoader().build();

describe('checkStatus', () => {
it.skip('should verify', async () => {
const suite = new Ed25519Signature2020();
const result = await vc.verifyCredential({
credential: mockCredential,
suite,
documentLoader,
checkStatus
});

console.log(JSON.stringify(result, null, 2));
});
});
42 changes: 42 additions & 0 deletions test/mocks/credential-legacy-obv3.js
Original file line number Diff line number Diff line change
@@ -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