diff --git a/.eslintrc.cjs b/.eslintrc.cjs index bccedc0..f4aef1d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,7 +3,14 @@ module.exports = { node: true }, extends: [ - 'eslint-config-digitalbazaar', - 'eslint-config-digitalbazaar/jsdoc' - ] + 'eslint-config-digitalbazaar/jsdoc', + 'digitalbazaar/module', + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + 'jsdoc/check-examples': 0 + } }; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10e8d07..99f47b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,10 +41,6 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install npm dependencies run: npm install --legacy-peer-deps - - name: Clean VCs - run: rm -f ./static-vcs/*.json - - name: Generate VCs - run: npm run generate-vcs - name: Run test with Node.js ${{ matrix.node-version }} env: CLIENT_SECRET_DB: ${{ secrets.CLIENT_SECRET_DB }} diff --git a/README.md b/README.md index 8baaddc..c8415b2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ npm run generate-vcs ## Implementation To add your implementation to this test suite see the [README here.](https://github.com/w3c-ccg/vc-api-test-suite-implementations) Add the tag `StatusList2021` to the issuers and verifiers you want tested. -To run the tests, some implementations require client secrets -that can be passed as env variables to the test script. To see which ones -require client secrets, you can check the [vc-api-test-suite-implementations](https://github.com/w3c-ccg/vc-api-test-suite-implementations) library. +To run the tests and to generate VCs, some implementations require client +secrets that can be passed as env variables to the test script. To see which +ones require client secrets, you can check the [vc-api-test-suite-implementations](https://github.com/w3c-ccg/vc-api-test-suite-implementations) +library. diff --git a/credentials/index.js b/credentials/index.js deleted file mode 100644 index b9a3a93..0000000 --- a/credentials/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. - */ -'use strict'; - -const requireDir = require('require-dir'); - -const dir = requireDir('.'); - -module.exports = dir; diff --git a/package.json b/package.json index fe75edb..8a0e8cb 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,11 @@ "name": "status-list-2021-test-suite", "version": "0.1.0", "description": "Interoperability Tests for StatusList2021", - "main": "index.js", + "main": "/tests", + "type": "module", "scripts": { - "test": "mocha tests --require esm --reporter @digitalbazaar/mocha-w3c-interop-reporter --reporter-options abstract=\"$PWD/abstract.hbs\",reportDir=\"$PWD/reports\",respec=\"$PWD/respecConfig.json\",title=\"Status List 2021 Interoperability Report 1.0\",suiteLog='./suite.log' --timeout 15000 --preserve-symlinks", - "lint": "eslint .", - "generate-vcs": "node vc-generator" + "test": "mocha tests --reporter @digitalbazaar/mocha-w3c-interop-reporter --reporter-options abstract=\"$PWD/abstract.hbs\",reportDir=\"$PWD/reports\",respec=\"$PWD/respecConfig.json\",title=\"Status List 2021 Interoperability Report 1.0\",suiteLog='./suite.log' --timeout 15000 --preserve-symlinks", + "lint": "eslint ." }, "repository": { "type": "git", @@ -25,32 +25,28 @@ "url": "https://digitalbazaar.com/" }, "engines": { - "node": "14.0.0" + "node": ">=16.0.0" }, "dependencies": { - "@digitalbazaar/did-method-key": "^2.0.0", - "@digitalbazaar/ed25519-signature-2020": "^3.0.0", - "@digitalbazaar/http-client": "^2.0.1", - "@digitalbazaar/mocha-w3c-interop-reporter": "^1.2.0", - "@digitalbazaar/vc": "^2.1.0", - "@digitalbazaar/vc-status-list": "^2.1.0", - "@digitalbazaar/vc-status-list-context": "^2.0.0", + "@digitalbazaar/http-client": "^3.2.0", + "@digitalbazaar/mocha-w3c-interop-reporter": "^1.2.1", + "@digitalbazaar/vc-status-list": "^5.0.0", + "@digitalbazaar/vc-status-list-context": "^3.0.1", "@digitalcredentials/did-context": "^1.0.0", - "bnid": "^2.1.0", "chai": "^4.3.4", "credentials-context": "^2.0.0", "ed25519-signature-2020-context": "^1.1.0", - "jsonld-document-loader": "^1.2.0", + "jsonld-document-loader": "^1.2.1", "klona": "^2.0.5", - "mocha": "^9.1.3", - "require-dir": "^1.2.0", + "mocha": "^10.0.0", "uuid": "^8.3.2", "vc-api-test-suite-implementations": "github:w3c-ccg/vc-api-test-suite-implementations" }, "devDependencies": { - "eslint": "^7.23.0", - "eslint-config-digitalbazaar": "^2.6.1", - "eslint-plugin-jsdoc": "^32.3.0" + "eslint": "^8.19.0", + "eslint-config-digitalbazaar": "^4.0.1", + "eslint-plugin-jsdoc": "^39.3.3", + "eslint-plugin-unicorn": "^43.0.0" }, "bugs": { "url": "https://github.com/w3c-ccg/status-list-2021-test-suite/issues" diff --git a/static-vcs/invalidCredentialStatusType.json b/static-vcs/invalidCredentialStatusType.json deleted file mode 100644 index a065f04..0000000 --- a/static-vcs/invalidCredentialStatusType.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/v1", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "id": "urn:uuid:a0418a78-7924-11ea-8a23-10bf48838a41", - "type": [ - "VerifiableCredential", - "example:TestCredential" - ], - "credentialSubject": { - "id": "urn:uuid:4886029a-7925-11ea-9274-10bf48838a41", - "example:test": "foo" - }, - "credentialStatus": { - "id": "https://localhost:9001/status/1#67342", - "type": "invalid-type", - "statusListIndex": "67342", - "statusListCredential": "https://localhost:9001/status/1" - }, - "issuer": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "issuanceDate": "2022-04-22T16:46:50Z", - "proof": { - "type": "Ed25519Signature2020", - "created": "2022-04-22T16:46:50Z", - "verificationMethod": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b#z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "proofPurpose": "assertionMethod", - "proofValue": "z2DycXg8zD13f5q2PveHRuQPhqfRtAoHxWDfXEkuBvbgp5v67rvu1B7qQroZSXVSvEPRkRS3uik4xWJDgDsCAMawH" - } -} \ No newline at end of file diff --git a/static-vcs/invalidStatusListCredentialId.json b/static-vcs/invalidStatusListCredentialId.json deleted file mode 100644 index 17ecab0..0000000 --- a/static-vcs/invalidStatusListCredentialId.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/v1", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "id": "urn:uuid:a0418a78-7924-11ea-8a23-10bf48838a41", - "type": [ - "VerifiableCredential", - "example:TestCredential" - ], - "credentialSubject": { - "id": "urn:uuid:4886029a-7925-11ea-9274-10bf48838a41", - "example:test": "foo" - }, - "credentialStatus": { - "id": "https://localhost:9001/status/1#67342", - "type": "RevocationList2021Status", - "statusListIndex": "67342", - "statusListCredential": "invalid-id" - }, - "issuer": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "issuanceDate": "2022-04-22T16:46:50Z", - "proof": { - "type": "Ed25519Signature2020", - "created": "2022-04-22T16:46:50Z", - "verificationMethod": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b#z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "proofPurpose": "assertionMethod", - "proofValue": "z2YNnjES5nSQLBgpNL2XaycVaPccpU1qBYBFQD3wRPRxCo27t7jCL1C6PzGY1AKonG7NRmjLVnBf2iq3xWb8okVzu" - } -} \ No newline at end of file diff --git a/static-vcs/validVc.json b/static-vcs/validVc.json deleted file mode 100644 index 292a542..0000000 --- a/static-vcs/validVc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/v1", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "id": "urn:uuid:a0418a78-7924-11ea-8a23-10bf48838a41", - "type": [ - "VerifiableCredential", - "example:TestCredential" - ], - "credentialSubject": { - "id": "urn:uuid:4886029a-7925-11ea-9274-10bf48838a41", - "example:test": "foo" - }, - "credentialStatus": { - "id": "https://localhost:9001/status/1#67342", - "type": "RevocationList2021Status", - "statusListIndex": "67342", - "statusListCredential": "https://localhost:9001/status/1" - }, - "issuer": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "issuanceDate": "2022-04-22T16:46:50Z", - "proof": { - "type": "Ed25519Signature2020", - "created": "2022-04-22T16:46:50Z", - "verificationMethod": "did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b#z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b", - "proofPurpose": "assertionMethod", - "proofValue": "z2DycXg8zD13f5q2PveHRuQPhqfRtAoHxWDfXEkuBvbgp5v67rvu1B7qQroZSXVSvEPRkRS3uik4xWJDgDsCAMawH" - } -} \ No newline at end of file diff --git a/tests/10-issue.js b/tests/10-issue.js index 8d17d25..1d615ad 100644 --- a/tests/10-issue.js +++ b/tests/10-issue.js @@ -1,100 +1,125 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -'use strict'; - -const chai = require('chai'); -const documentLoader = require('../vc-generator/documentLoader.js'); -const {filterByTag} = require('vc-api-test-suite-implementations'); -const {ISOTimeStamp} = require('./helpers.js'); -const rl = require('@digitalbazaar/vc-status-list'); -const {testCredential} = require('./assertions.js'); -const {v4: uuidv4} = require('uuid'); -const {validVc} = require('../credentials'); +import chai from 'chai'; +import {createValidVc, getSlc} from './helpers.js'; +import {filterByTag} from 'vc-api-test-suite-implementations'; +import * as sl from '@digitalbazaar/vc-status-list'; +import {testCredential, testSlCredential} from './assertions.js'; const should = chai.should(); // only use implementations with `StatusList2021` issuers. const {match, nonMatch} = filterByTag({ property: 'issuers', - tags: ['RevocationList2020'] + tags: ['StatusList2021'] }); - -describe('StatusList2021 Credentials (Issue)', function() { - // this will tell the report - // to make an interop matrix with this suite - this.matrix = true; - this.report = true; - this.implemented = [...match.keys()]; - this.rowLabel = 'Test Name'; - this.columnLabel = 'Issuer'; - this.notImplemented = [...nonMatch.keys()]; - for(const [issuerName, {issuers}] of match) { - describe(issuerName, function() { - let issuerResponse; - let err; - let issuedVc; - before(async function() { - const issuer = issuers.find( - issuer => issuer.tags.has('RevocationList2020')); - const expires = () => { - const date = new Date(); - date.setMonth(date.getMonth() + 2); - return ISOTimeStamp({date}); - }; - const {settings: {id: issuerId}} = issuer; - const body = { - credential: { - ...validVc, - id: `urn:uuid:${uuidv4()}`, - issuanceDate: ISOTimeStamp(), - expirationDate: expires(), - issuer: issuerId - } - }; - const {result, error} = await issuer.post({json: body}); - issuerResponse = result; - err = error; - if(issuerResponse) { - // FIXME: This might need to be changed to `result.data` instead - issuedVc = issuerResponse.data.verifiableCredential; - } +describe('StatusList2021 Credentials (Issue "statusPurpose: revocation")', + function() { + this.matrix = true; + this.report = true; + this.implemented = [...match.keys()]; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Issuer'; + this.notImplemented = [...nonMatch.keys()]; + for(const [issuerName, {issuers}] of match) { + describe(issuerName, function() { + let issuerResponse; + let err; + let issuedVc; + before(async function() { + const issuer = issuers.find(issuer => issuer.tags.has('Revocation')); + const credential = createValidVc({issuer}); + const {result, error, data} = await issuer.post({ + json: {credential}}); + err = error; + issuerResponse = result; + issuedVc = data; + }); + it('MUST issue a VC with SL 2021 "credentialStatus" and ' + + '"revocation" status purpose', async function() { + this.test.cell = {columnId: issuerName, rowId: this.test.title}; + should.exist(issuerResponse); + should.not.exist(err); + issuerResponse.status.should.equal(201); + should.exist(issuedVc, `Expected VC from ${issuerName} to exist.`); + testCredential({credential: issuedVc}); + }); + // ensure that issued StatusList Credential contain correct properties + it('MUST have correct properties when dereferencing ' + + '"credentialStatus.statusListCredential"', async function() { + this.test.cell = {columnId: issuerName, rowId: this.test.title}; + const {slc} = await getSlc({issuedVc}); + testSlCredential({slCredential: slc}); + }); + it('MUST be 16kb in size when "encodedList" is decoded', + async function() { + this.test.cell = {columnId: issuerName, rowId: this.test.title}; + const {slc: {credentialSubject}} = await getSlc({issuedVc}); + const {encodedList} = credentialSubject; + // Uncompress encodedList + const decoded = await sl.decodeList({encodedList}); + should.exist(decoded); + // decoded size should be 16kb. For more info see + // `credentialSubject.encoded` in + // https://w3c-ccg.github.io/vc-status-list-2021/#statuslist2021credential + const decodedSize = (decoded.length / 8) / 1024; + decodedSize.should.equal(16); + }); }); - it(`MUST issue a VC with a "credentialStatus" property`, - async function() { + } + }); + +describe('StatusList2021 Credentials (Issue "statusPurpose: suspension")', + function() { + this.matrix = true; + this.report = true; + this.implemented = [...match.keys()]; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Issuer'; + this.notImplemented = [...nonMatch.keys()]; + for(const [issuerName, {issuers}] of match) { + describe(issuerName, function() { + let issuerResponse; + let err; + let issuedVc; + before(async function() { + const issuer = issuers.find(issuer => issuer.tags.has('Suspension')); + const credential = createValidVc({issuer}); + const {result, error, data} = await issuer.post({ + json: {credential}}); + err = error; + issuerResponse = result; + issuedVc = data; + }); + it('MUST issue a VC with SL 2021 "credentialStatus" and ' + + '"suspension" status purpose', async function() { this.test.cell = {columnId: issuerName, rowId: this.test.title}; - should.not.exist(err, `Expected ${issuerName} to not error.`); should.exist(issuerResponse); + should.not.exist(err); issuerResponse.status.should.equal(201); - should.exist( - issuedVc, `Expected VC from ${issuerName} to exist.`); - testCredential(issuedVc); - issuedVc.credentialSubject.should.eql( - validVc.credentialSubject); + should.exist(issuedVc, `Expected VC from ${issuerName} to exist.`); + testCredential({credential: issuedVc}); }); - // ensure that issued VC contain correct properties. - it('MUST have correct properties when dereferencing' + - '"credentialStatus.statusListCredential"', async function() { - this.test.cell = {columnId: issuerName, rowId: this.test.title}; - // FIXME: Change revocationListCredential to statusListCredential - const {credentialStatus: {revocationListCredential}} = issuedVc; - const {document: rlc} = await documentLoader(revocationListCredential); - rlc.should.have.property('type'); - // FIXME: Change the type to `StatusList2021Credential`. - rlc.type.should.include('RevocationList2020Credential'); - rlc.should.have.property('credentialSubject'); - const {credentialSubject} = rlc; - credentialSubject.should.have.keys(['id', 'type', 'encodedList']); - // FIXME: Change type to equal 'RevocationList2021' - credentialSubject.type.should.equal('RevocationList2020'); - const {encodedList} = credentialSubject; - // Uncompress encodedList - const decoded = await rl.decodeList({encodedList}); - should.exist(decoded); - // decoded size should be 16kb - const decodedSize = (decoded.length / 8) / 1024; - decodedSize.should.equal(16); + // ensure that issued StatusList Credential contain correct properties + it('MUST have correct properties when dereferencing ' + + '"credentialStatus.statusListCredential"', async function() { + this.test.cell = {columnId: issuerName, rowId: this.test.title}; + const {slc} = await getSlc({issuedVc}); + testSlCredential({slCredential: slc}); + }); + it('MUST be 16kb in size when "encodedList" is decoded', + async function() { + this.test.cell = {columnId: issuerName, rowId: this.test.title}; + const {slc: {credentialSubject}} = await getSlc({issuedVc}); + const {encodedList} = credentialSubject; + // Uncompress encodedList + const decoded = await sl.decodeList({encodedList}); + should.exist(decoded); + // decoded size should be 16kb + const decodedSize = (decoded.length / 8) / 1024; + decodedSize.should.equal(16); + }); }); - }); - } -}); + } + }); diff --git a/tests/20-verify.js b/tests/20-verify.js index 50eb36b..849374e 100644 --- a/tests/20-verify.js +++ b/tests/20-verify.js @@ -1,17 +1,11 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -'use strict'; - -const chai = require('chai'); -const {filterByTag} = require('vc-api-test-suite-implementations'); -const invalidCredentialStatusType = - require('../static-vcs/invalidCredentialStatusType.json'); -const invalidStatusListCredentialId = - require('../static-vcs/invalidStatusListCredentialId.json'); -const validVc = require('../static-vcs/validVc.json'); - -const should = chai.should(); +import {createRequestBody, createValidVc} from './helpers.js'; +import {filterByTag, filterImplementations} from + 'vc-api-test-suite-implementations'; +import {shouldFailVerification, shouldPassVerification} from './assertions.js'; +import {klona} from 'klona'; // only use implementations with `StatusList2021` verifiers. const {match, nonMatch} = filterByTag({ @@ -20,8 +14,6 @@ const {match, nonMatch} = filterByTag({ }); describe('StatusList2021 Credentials (Verify)', function() { - // this will tell the report - // to make an interop matrix with this suite this.matrix = true; this.report = true; this.implemented = [...match.keys()]; @@ -32,61 +24,62 @@ describe('StatusList2021 Credentials (Verify)', function() { describe(verifierName, function() { const verifier = verifiers.find(verifier => verifier.tags.has('StatusList2021')); - it.skip('MUST verify a valid "StatusList2021Credential"', - async function() { - // this tells the test report which cell - // in the interop matrix the result goes in - this.test.cell = {columnId: verifierName, rowId: this.test.title}; - const body = { - verifiableCredential: validVc, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result, error} = await verifier.post({json: body}); - should.exist(result); - should.not.exist(error); + let validVcForRevocation; + let validVcForSuspension; + before(async function() { + // get a VC issued by DB + const {match} = filterImplementations({filter: ({value}) => { + // FIXME: Make issuer name configurable via env variable + return value.settings.name === 'Digital Bazaar'; + }}); + const {issuers} = match.get('Digital Bazaar'); + const issuer1 = issuers.find(issuer => issuer.tags.has('Revocation')); + const credential1 = createValidVc({issuer: issuer1}); + const {data: data1} = await issuer1.post( + {json: {credential: credential1}}); + validVcForRevocation = data1; + const issuer2 = issuers.find(issuer => issuer.tags.has('Suspension')); + const credential2 = createValidVc({issuer: issuer2}); + const {data: data2} = await issuer1.post( + {json: {credential: credential2}}); + validVcForSuspension = data2; + }); + it('MUST verify a valid "StatusList2021Credential" with "revocation" ' + + 'status purpose', async function() { + this.test.cell = {columnId: verifierName, rowId: this.test.title}; + const {result, error, statusCode} = await verifier.post({ + json: createRequestBody({vc: validVcForRevocation}) }); + shouldPassVerification({result, error, statusCode}); + }); + it('MUST verify a valid "StatusList2021Credential" with "suspension"' + + 'status purpose', async function() { + this.test.cell = {columnId: verifierName, rowId: this.test.title}; + const {result, error, statusCode} = await verifier.post({ + json: createRequestBody({vc: validVcForSuspension}) + }); + shouldPassVerification({result, error, statusCode}); + }); it('MUST fail to verify a VC with invalid ' + - '"credentialStatus.statusListCredential"', async function() { - // this tells the test report which cell - // in the interop matrix the result goes in + '"credentialStatus.statusListCredential"', async function() { this.test.cell = {columnId: verifierName, rowId: this.test.title}; - const body = { - verifiableCredential: invalidStatusListCredentialId, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result, error} = await verifier.post({json: body}); - should.not.exist(result); - should.exist(error); - should.exist(error.data); - // verifier returns 400 - error.status.should.equal(400); - error.data.verified.should.equal(false); - const {check} = error.data.checks[0]; - check.should.be.an('array'); - check.should.include.members(['credentialStatus', 'proof']); + const invalidStatusListCredential = klona(validVcForRevocation); + invalidStatusListCredential.credentialStatus.statusListCredential = + 'invalid-statusListCredential'; + const {result, error, statusCode} = await verifier.post({ + json: createRequestBody({vc: invalidStatusListCredential}) + }); + shouldFailVerification({result, error, statusCode}); }); it('MUST fail to verify a VC with invalid "credentialStatus.type"', async function() { - // this tells the test report which cell - // in the interop matrix the result goes in this.test.cell = {columnId: verifierName, rowId: this.test.title}; - const body = { - verifiableCredential: invalidCredentialStatusType, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result, error} = await verifier.post({json: body}); - should.not.exist(result); - should.exist(error); - should.exist(error.data); - // verifier returns 400 - error.status.should.equal(400); - error.data.verified.should.equal(false); + const invalidCredentialStatusType = klona(validVcForRevocation); + invalidCredentialStatusType.credentialStatus.type = 'invalid-type'; + const {result, error, statusCode} = await verifier.post({ + json: createRequestBody({vc: invalidCredentialStatusType}) + }); + shouldFailVerification({result, error, statusCode}); }); }); } diff --git a/tests/30-interop.js b/tests/30-interop.js index 35220fd..e9561df 100644 --- a/tests/30-interop.js +++ b/tests/30-interop.js @@ -1,93 +1,149 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -'use strict'; - -const chai = require('chai'); -const {filterByTag} = require('vc-api-test-suite-implementations'); -const {ISOTimeStamp, getCredentialStatus} = require('./helpers.js'); -const {klona} = require('klona'); -const {v4: uuidv4} = require('uuid'); -const {validVc} = require('../credentials'); +import chai from 'chai'; +import {createValidVc, createRequestBody, getCredentialStatus} from + './helpers.js'; +import {filterByTag} from 'vc-api-test-suite-implementations'; +import {klona} from 'klona'; +import {shouldPassVerification, shouldFailVerification} from './assertions.js'; const should = chai.should(); // only use implementations with `StatusList2021` tags. const {match, nonMatch} = filterByTag({ property: 'issuers', - tags: ['RevocationList2020'] + tags: ['StatusList2021'] }); -describe('StatusList2021 Credentials (Interop)', function() { - // this will tell the report - // to make an interop matrix with this suite - this.matrix = true; - this.report = true; - this.implemented = [...match.keys()]; - this.rowLabel = 'Test Name'; - this.columnLabel = 'Implementation'; - this.notImplemented = [...nonMatch.keys()]; - // the reportData will be displayed under the test title - for(const [issuerName, {issuers, statusLists, publishStatusLists}] of match) { - let issuedVc; - before(async function() { - const issuer = issuers.find(issuer => - issuer.tags.has('RevocationList2020')); - const expires = () => { - const date = new Date(); - date.setMonth(date.getMonth() + 2); - return ISOTimeStamp({date}); - }; - const {settings: {id: issuerId}} = issuer; - const body = { - credential: { - ...validVc, - id: `urn:uuid:${uuidv4()}`, - issuanceDate: ISOTimeStamp(), - expirationDate: expires(), - issuer: issuerId - } - }; - const {result} = await issuer.post({json: body}); - if(result) { - issuedVc = result.data.verifiableCredential; - } - }); - // this sends a credential issued by the implementation - // to each verifier - for(const [verifierName, {verifiers}] of match) { - const verifier = verifiers.find(verifier => - verifier.tags.has('StatusList2021')); - it(`MUST successfully verify VC issued by ${issuerName}`, - async function() { - // this tells the test report which cell in the interop matrix - // the result goes in +describe('StatusList2021 Credentials (Interop "statusPurpose: revocation")', + function() { + this.matrix = true; + this.report = true; + this.implemented = [...match.keys()]; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Implementation'; + this.notImplemented = [...nonMatch.keys()]; + for(const [ + issuerName, + {issuers, setStatusLists, publishStatusLists} + ] of match) { + let issuedVc; + before(async function() { + const issuer = issuers.find(issuer => issuer.tags.has('Revocation')); + const credential = createValidVc({issuer}); + const body = {credential}; + const {data} = await issuer.post({json: body}); + issuedVc = data; + }); + for(const [verifierName, {verifiers}] of match) { + const verifier = verifiers.find(verifier => + verifier.tags.has('StatusList2021')); + it(`MUST successfully verify VC issued by ${issuerName}`, + async function() { + this.test.cell = {columnId: verifierName, rowId: this.test.title}; + const body = createRequestBody({vc: issuedVc}); + const {result, error, statusCode} = await verifier.post( + {json: body}); + shouldPassVerification({result, error, statusCode}); + }); + it('MUST update StatusList2021 revocation credential status and ' + + 'fail to verify revoked credential', async function() { this.test.cell = {columnId: verifierName, rowId: this.test.title}; - const body = { - verifiableCredential: issuedVc, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result, error} = await verifier.post({json: body}); - should.exist(result); - should.not.exist(error); - // verifier returns 200 - result.status.should.equal(200); - should.exist(result.data); - // verifier responses vary but are all objects - result.data.should.be.an('object'); - result.data.verified.should.equal(true); - result.data.statusResult.verified.should.equal(true); - result.data.checks.should.eql(['proof', 'credentialStatus']); + // copy vc issued + const vc = klona(issuedVc); + // get the status of the VC + const statusInfo = await getCredentialStatus( + {verifiableCredential: vc}); + statusInfo.status.should.equal(false); + + // verification of the credential should pass + const { + result: result1, + error: err1, + statusCode: statusCode1 + } = await verifier.post({json: createRequestBody({vc})}); + shouldPassVerification( + {result: result1, error: err1, statusCode: statusCode1}); + const setStatusList = setStatusLists.find( + issuer => issuer.tags.has('Revocation')); + // Then revoke the VC + const body = createRequestBody({ + vc, setStatus: true, statusPurpose: 'revocation'}); + const { + result: result2, + error: err2, + statusCode: statusCode2 + } = await setStatusList.post({json: body}); + should.not.exist(err2); + should.exist(result2); + statusCode2.should.equal(200); + const publishSlcEndpoint = + `${statusInfo.statusListCredential}/publish`; + const publishStatusList = publishStatusLists.find( + issuer => issuer.tags.has('Revocation')); + // force publication of new SLC + const { + result: result3, + error: err3, + statusCode: statusCode3 + } = await publishStatusList.post({url: publishSlcEndpoint, json: {}}); + should.not.exist(err3); + should.exist(result3); + statusCode3.should.equal(204); + + // get the status of the VC + const {status} = await getCredentialStatus( + {verifiableCredential: vc}); + status.should.equal(true); + + // try to verify the credential again, should fail since it + // has now been revoked + const { + result: result4, + error: err4, + statusCode: statusCode4 + } = await verifier.post({json: createRequestBody({vc})}); + shouldFailVerification( + {result: result4, error: err4, statusCode: statusCode4}); }); - it(`MUST revoke a credential and fail to verify revoked credential`, - async function() { - // FIXME: Currently this test uses credential with 2020 status - // type. + } + } + }); - // this tells the test report which cell in the interop matrix - // the result goes in +describe('StatusList2021 Credentials (Interop "statusPurpose: suspension")', + function() { + this.matrix = true; + this.report = true; + this.implemented = [...match.keys()]; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Implementation'; + this.notImplemented = [...nonMatch.keys()]; + for(const [ + issuerName, + {issuers, setStatusLists, publishStatusLists} + ] of match) { + let issuedVc; + before(async function() { + const issuer = issuers.find(issuer => issuer.tags.has('Suspension')); + const credential = createValidVc({issuer}); + const body = {credential}; + const {data} = await issuer.post({json: body}); + issuedVc = data; + }); + for(const [verifierName, {verifiers}] of match) { + const verifier = verifiers.find(verifier => + verifier.tags.has('StatusList2021')); + it(`MUST successfully verify VC issued by ${issuerName}`, + async function() { + this.test.cell = {columnId: verifierName, rowId: this.test.title}; + const body = createRequestBody({vc: issuedVc}); + const {result, error, statusCode} = await verifier.post( + {json: body}); + shouldPassVerification({result, error, statusCode}); + }); + it('MUST update StatusList2021 suspension credential status and ' + + 'fail to verify suspended credential', async function() { this.test.cell = {columnId: verifierName, rowId: this.test.title}; // copy vc issued const vc = klona(issuedVc); @@ -97,47 +153,40 @@ describe('StatusList2021 Credentials (Interop)', function() { statusInfo.status.should.equal(false); // verification of the credential should pass - const body = { - verifiableCredential: vc, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result: result1, error: err1} = await verifier.post( - {json: body}); - should.exist(result1); - should.not.exist(err1); - result1.status.should.equal(200); - should.exist(result1.data); - // verifier responses vary but are all objects - result1.data.should.be.an('object'); - result1.data.verified.should.equal(true); - result1.data.statusResult.verified.should.equal(true); + const { + result: result1, + error: err1, + statusCode: statusCode1 + } = await verifier.post({json: createRequestBody({vc})}); + shouldPassVerification( + {result: result1, error: err1, statusCode: statusCode1}); - const statusList = statusLists.find(issuer => - issuer.tags.has('RevocationList2020')); - const publishList = publishStatusLists.find(issuer => - issuer.tags.has('RevocationList2020')); - const body2 = { - credentialId: vc.id, - credentialStatus: { - type: 'RevocationList2020Status' - } - }; - // Then revoke the VC - const {result: result2, error: err2} = await statusList.post( - {json: body2}); + const setStatusList = setStatusLists.find( + issuer => issuer.tags.has('Suspension')); + // Then suspend the VC + const body = createRequestBody({ + vc, setStatus: true, statusPurpose: 'suspension'}); + const { + result: result2, + error: err2, + statusCode: statusCode2 + } = await setStatusList.post({json: body}); should.not.exist(err2); should.exist(result2); - result2.status.should.equal(200); + statusCode2.should.equal(200); const publishSlcEndpoint = - `${statusInfo.statusListCredential}/publish`; - // force publication of new SLC - const {result: result3, error: err3} = await publishList.post( - {url: publishSlcEndpoint, json: {}}); + `${statusInfo.statusListCredential}/publish`; + const publishStatusList = publishStatusLists.find(issuer => + issuer.tags.has('Suspension')); + // force publication of new SLC + const { + result: result3, + error: err3, + statusCode: statusCode3 + } = await publishStatusList.post({url: publishSlcEndpoint, json: {}}); should.not.exist(err3); should.exist(result3); - result3.status.should.equal(204); + statusCode3.should.equal(204); // get the status of the VC const {status} = await getCredentialStatus( @@ -145,22 +194,15 @@ describe('StatusList2021 Credentials (Interop)', function() { status.should.equal(true); // try to verify the credential again, should fail since it - // has been revoked - const body3 = { - verifiableCredential: vc, - options: { - checks: ['proof', 'credentialStatus'] - } - }; - const {result: result4, error: err4} = await verifier.post( - {json: body3}); - should.not.exist(result4); - should.exist(err4); - should.exist(err4.data); - // verifier returns 400 - err4.status.should.equal(400); - err4.data.verified.should.equal(false); + // has now been revoked + const { + result: result4, + error: err4, + statusCode: statusCode4 + } = await verifier.post({json: createRequestBody({vc})}); + shouldFailVerification( + {result: result4, error: err4, statusCode: statusCode4}); }); + } } - } -}); + }); diff --git a/tests/assertions.js b/tests/assertions.js index 769eceb..32621d9 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -1,45 +1,50 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -'use strict'; - -const chai = require('chai'); +import chai from 'chai'; +import {createRequire} from 'node:module'; +const require = createRequire(import.meta.url); +const validVc = require('../credentials/validVc.json'); const should = chai.should(); /** * Tests the properties of a credential. * - * @param {object} credential - A VC issued from an issuer. + * @param {object} options - The options to use. + * @param {object} options.credential - A VC issued from an issuer. * * @returns {undefined} Just returns on success. */ -const testCredential = credential => { +export const testCredential = ({credential}) => { should.exist(credential, 'expected credential to exist'); credential.should.be.an('object'); credential.should.have.property('@context'); - // NOTE: some issuers add a revocation list context to the types credential['@context'].should.include.members([ 'https://www.w3.org/2018/credentials/v1', - // FIXME: Uncomment this once status-list is implemented in issuer - // and verifier - // 'https://w3id.org/vc/status-list/v1' - 'https://w3id.org/vc-revocation-list-2020/v1' + 'https://w3id.org/vc/status-list/2021/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1' ]); credential.should.have.property('type'); - credential.type.should.eql([ - 'VerifiableCredential', + credential.type.should.be.an('array'); + credential.type.should.include.members([ + 'VerifiableCredential' ]); credential.should.have.property('id'); credential.id.should.be.a('string'); credential.should.have.property('credentialSubject'); + credential.credentialSubject.should.eql(validVc.credentialSubject); credential.credentialSubject.should.be.an('object'); credential.should.have.property('issuanceDate'); credential.issuanceDate.should.be.a('string'); - credential.should.have.property('expirationDate'); - credential.expirationDate.should.be.a('string'); credential.should.have.property('issuer'); - credential.issuer.should.be.a('string'); + const issuerType = typeof(credential.issuer); + issuerType.should.be.oneOf(['string', 'object']); + if(issuerType === 'object') { + should.exist(credential.issuer.id, + 'Expected issuer object to have property id'); + credential.issuer.id.should.be.an('object'); + } credential.should.have.property('proof'); credential.proof.should.be.an('object'); credential.should.have.property('credentialStatus'); @@ -47,16 +52,67 @@ const testCredential = credential => { credential.credentialStatus.should.have.keys([ 'id', 'type', - // FIXME: once status list 2021 is implemented, change this to - // statusListCredential - 'revocationListCredential', - // FIXME: once status list 2021 is implemented, change this to - // statusListIndex - 'revocationListIndex' + 'statusListCredential', + 'statusListIndex', + 'statusPurpose' + ]); + credential.credentialStatus.statusPurpose.should.be.oneOf( + ['revocation', 'suspension']); + credential.credentialStatus.type.should.equal('StatusList2021Entry'); +}; + +export const testSlCredential = ({slCredential}) => { + should.exist(slCredential, 'expected credential to exist'); + slCredential.should.be.an('object'); + slCredential.should.have.property('@context'); + slCredential['@context'].should.include.members([ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/vc/status-list/2021/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1' ]); - // FIXME: Update this to either equal `StatusList2021Status` or - // `SuspensionList2021Status` - credential.credentialStatus.type.should.equal('RevocationList2020Status'); + slCredential.should.have.property('type'); + slCredential.type.should.be.an('array'); + slCredential.type.should.include.members( + ['VerifiableCredential', 'StatusList2021Credential']); + slCredential.should.have.property('id'); + slCredential.id.should.be.a('string'); + slCredential.should.have.property('credentialSubject'); + const {credentialSubject} = slCredential; + credentialSubject.should.have.keys( + ['id', 'type', 'encodedList', 'statusPurpose']); + credentialSubject.id.should.be.a('string'); + credentialSubject.encodedList.should.be.a('string'); + credentialSubject.type.should.be.a('string'); + credentialSubject.type.should.eql('StatusList2021'); + slCredential.should.have.property('issuer'); + const issuerType = typeof(slCredential.issuer); + issuerType.should.be.oneOf(['string', 'object']); + if(issuerType === 'object') { + should.exist(slCredential.issuer.id, + 'Expected issuer object to have property id'); + slCredential.issuer.id.should.be.an('object'); + } + slCredential.should.have.property('issuanceDate'); + slCredential.issuanceDate.should.be.a('string'); + slCredential.should.have.property('proof'); + slCredential.proof.should.be.an('object'); +}; + +export const shouldFailVerification = ({result, error, statusCode}) => { + should.not.exist(result, 'Expected no response from verifier'); + should.exist(error, 'Expected verifier to error'); + statusCode.should.equal(400, 'Expected status code 400'); + should.exist(error.data); + error.data.verified.should.equal(false); }; -module.exports = {testCredential}; +export const shouldPassVerification = ({result, error, statusCode}) => { + should.exist(result, 'Expected response from verifier'); + should.not.exist(error, 'Expected verifier to not error'); + // verifier returns 200 + statusCode.should.equal(200, 'Expected status code 200'); + should.exist(result.data); + // verifier responses vary but are all objects + result.data.should.be.an('object'); + result.data.verified.should.equal(true); +}; diff --git a/vc-generator/contexts.js b/tests/contexts.js similarity index 55% rename from vc-generator/contexts.js rename to tests/contexts.js index d63e778..4ae1873 100644 --- a/vc-generator/contexts.js +++ b/tests/contexts.js @@ -1,13 +1,10 @@ -/** - * - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. -*/ -'use strict'; - -const ed25519Ctx = require('ed25519-signature-2020-context'); -const didCtx = require('@digitalcredentials/did-context'); -const credentialsCtx = require('credentials-context'); -const statusListCtx = require('@digitalbazaar/vc-status-list-context'); +/*! + * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. + */ +import credentialsCtx from 'credentials-context'; +import didCtx from '@digitalcredentials/did-context'; +import ed25519Ctx from 'ed25519-signature-2020-context'; +import statusListCtx from '@digitalbazaar/vc-status-list-context'; const contextMap = new Map(); @@ -28,4 +25,4 @@ contextMap.set( statusListCtx.CONTEXT_V1 ); -module.exports = {contextMap}; +export {contextMap}; diff --git a/vc-generator/documentLoader.js b/tests/documentLoader.js similarity index 59% rename from vc-generator/documentLoader.js rename to tests/documentLoader.js index a2c91a9..7883666 100644 --- a/vc-generator/documentLoader.js +++ b/tests/documentLoader.js @@ -1,13 +1,10 @@ -/** - * - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. -*/ -'use strict'; - -const {httpClient} = require('@digitalbazaar/http-client'); -const https = require('https'); -const {JsonLdDocumentLoader} = require('jsonld-document-loader'); -const {contextMap} = require('./contexts.js'); +/*! + * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. + */ +import {contextMap} from './contexts.js'; +import {httpClient} from '@digitalbazaar/http-client'; +import https from 'https'; +import {JsonLdDocumentLoader} from 'jsonld-document-loader'; const agent = new https.Agent({rejectUnauthorized: false}); @@ -29,9 +26,10 @@ const handler = { const jdl = new JsonLdDocumentLoader(); jdl.setProtocolHandler({protocol: 'https', handler}); -// add contexts to documentLoad +// add contexts to documentLoader for(const [key, value] of contextMap) { jdl.addStatic(key, value); } -module.exports = jdl.build(); +const documentLoader = jdl.build(); +export {documentLoader}; diff --git a/tests/helpers.js b/tests/helpers.js index 9b3fb54..e9f41f7 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -1,39 +1,82 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -'use strict'; - -const {decodeList} = require('@digitalbazaar/vc-status-list'); -const {httpClient} = require('@digitalbazaar/http-client'); -const https = require('https'); - +import {decodeList} from '@digitalbazaar/vc-status-list'; +import {documentLoader} from './documentLoader.js'; +import {httpClient} from '@digitalbazaar/http-client'; +import https from 'https'; +import {v4 as uuidv4} from 'uuid'; +import {createRequire} from 'node:module'; +const require = createRequire(import.meta.url); +const validVc = require('../credentials/validVc.json'); const agent = new https.Agent({rejectUnauthorized: false}); -// Javascript's default ISO timestamp is contains milliseconds. +// Javascript's default ISO timestamp contains milliseconds. // This lops off the MS part of the UTC RFC3339 TimeStamp and replaces // it with a terminal Z. -const ISOTimeStamp = ({date = new Date()} = {}) => { +export const ISOTimeStamp = ({date = new Date()} = {}) => { return date.toISOString().replace(/\.\d+Z$/, 'Z'); }; -const getCredentialStatus = async ({verifiableCredential}) => { - // get SLC for the VC +export const getCredentialStatus = async ({verifiableCredential}) => { const {credentialStatus} = verifiableCredential; - // FIXME: support `statusListCredential` as well - const {revocationListCredential} = credentialStatus; + const {statusListCredential} = credentialStatus; + // get StatusList Credential for the VC const {data: slc} = await httpClient.get( - revocationListCredential, {agent}); - - const {encodedList} = slc.credentialSubject; + statusListCredential, {agent}); + const {credentialSubject: {encodedList}} = slc; const list = await decodeList({encodedList}); - // FIXME: support `statusListIndex` as well const statusListIndex = parseInt( - credentialStatus.revocationListIndex, 10); + credentialStatus.statusListIndex, 10); const status = list.getStatus(statusListIndex); - return {status, statusListCredential: revocationListCredential}; + return {status, statusListCredential}; +}; + +// copies a validVc and adds an id. +export const createValidVc = ({issuer}) => { + const {settings: {id: issuerId}} = issuer; + return { + ...validVc, + id: `urn:uuid:${uuidv4()}`, + issuanceDate: ISOTimeStamp(), + issuer: issuerId + }; +}; + +export const getSlc = async ({issuedVc}) => { + const {credentialStatus: {statusListCredential}} = issuedVc; + const {document} = await documentLoader(statusListCredential); + return {slc: document}; }; -module.exports = { - ISOTimeStamp, - getCredentialStatus +/** + * Creates a request body. + * + * @param {object} options - The options to use. + * @param {object} options.vc - The verifiable credential to send in request. + * @param {boolean} options.setStatus - The verifiable credential to send in + * request. + * @param {string} [options.statusPurpose] - The purpose of the status entry. + * "statusPurpose" must be set if "setStatus" is set to true. The values + * "revocation" or "suspension" must be used. + * + * @returns {object} - A request body. + */ +export const createRequestBody = ({vc, setStatus = false, statusPurpose}) => { + let body = { + verifiableCredential: vc, + options: { + checks: ['proof', 'credentialStatus'], + } + }; + if(setStatus) { + body = { + credentialId: vc.id, + credentialStatus: { + type: 'StatusList2021Entry', + statusPurpose + } + }; + } + return body; }; diff --git a/vc-generator/helpers.js b/vc-generator/helpers.js deleted file mode 100644 index 90f52e8..0000000 --- a/vc-generator/helpers.js +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. - */ -'use strict'; - -const didKeyDriver = require('@digitalbazaar/did-method-key').driver(); -const {writeFile} = require('fs'); -const {promisify} = require('util'); -const {decodeSecretKeySeed} = require('bnid'); - -const asyncWriteFile = promisify(writeFile); -const _seed = 'z1AYMku6XEB5KV3XJbYzz9VejGJYRuqzu5wmq4JDRyUCjr8'; - -/** - * Writes a json file to disc. - * - * @param {object} options - Options to use. - * @param {string} options.path - A path to write to. - * @param {object} options.data - A JSON Object. - * - * @returns {Promise} Resolves on write. - */ -const writeJSON = async ({path, data}) => { - return asyncWriteFile(path, JSON.stringify(data, null, 2)); -}; - -/** - * Takes in a bs58 mutlicodec multibase seed and returns a did key. - * - * @param {object} options - Options to use. - * @param {string} [options.keySeed=_seed] - A bs58 encoded string. - * - * @returns {Promise} - Returns the resulting did key driver result. - */ -const getDiDKey = async ({keySeed = _seed} = {}) => { - const seed = decodeSecretKeySeed({secretKeySeed: keySeed}); - return didKeyDriver.generate({seed}); -}; - -module.exports = { - getDiDKey, - writeJSON, -}; diff --git a/vc-generator/index.js b/vc-generator/index.js deleted file mode 100644 index a440a63..0000000 --- a/vc-generator/index.js +++ /dev/null @@ -1,124 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. - */ -'use strict'; - -const documentLoader = require('./documentLoader'); -const {Ed25519Signature2020} = require('@digitalbazaar/ed25519-signature-2020'); -const {getDiDKey, writeJSON} = require('./helpers'); -const {join} = require('path'); -const {klona} = require('klona'); -const statusListCtx = require('@digitalbazaar/vc-status-list-context'); -const vc = require('@digitalbazaar/vc'); - -const credentialsPath = join(process.cwd(), 'static-vcs'); -const VC_SL_CONTEXT_URL = statusListCtx.constants.CONTEXT_URL_V1; - -const encodedList100k = - 'H4sIAAAAAAAAA-3BMQEAAADCoPVPbQsvoAAAAAAAAAAAAAAAAP4GcwM92tQwAAA'; - -// this will generate the signed VCs for the test -const main = async () => { - console.log('generating vcs'); - const {methodFor} = await getDiDKey(); - const key = methodFor({purpose: 'capabilityInvocation'}); - const suite = new Ed25519Signature2020({key}); - const PORT = 9001; - const BASE_URL = `https://localhost:${PORT}`; - - let slCredential = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - VC_SL_CONTEXT_URL - ], - id: `${BASE_URL}/status/1`, - issuer: 'did:key:z6MktKwz7Ge1Yxzr4JHavN33wiwa8y81QdcMRLXQsrH9T53b', - issuanceDate: '2022-01-10T04:24:12.164Z', - type: ['VerifiableCredential', 'StatusList2021Credential'], - credentialSubject: { - id: `${BASE_URL}/status/1#list`, - type: 'RevocationList2021', - encodedList: encodedList100k - } - }; - // sign slCredential - slCredential = await vc.issue({ - credential: slCredential, - documentLoader, - suite - }); - - const unsignedCredential = { - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - VC_SL_CONTEXT_URL, - 'https://w3id.org/security/suites/ed25519-2020/v1' - ], - id: 'urn:uuid:a0418a78-7924-11ea-8a23-10bf48838a41', - type: ['VerifiableCredential', 'example:TestCredential'], - credentialSubject: { - id: 'urn:uuid:4886029a-7925-11ea-9274-10bf48838a41', - 'example:test': 'foo' - }, - credentialStatus: { - id: `${BASE_URL}/status/1#67342`, - type: 'RevocationList2021Status', - statusListIndex: '67342', - statusListCredential: slCredential.id - }, - issuer: slCredential.issuer, - }; - - const {path, signedVc: data} = await _validVc(unsignedCredential, suite); - const validVc = data; - const vcs = await Promise.all([ - _invalidCredentialStatusType(validVc), - _invalidStatusListCredentialId(unsignedCredential, suite), - ]); - console.log('writing vcs to /credentials'); - - // write them to disk - await Promise.all([ - ...vcs, - // add the valid vc to the list - {path, data} - ].map(writeJSON)); - console.log('vcs generated'); -}; - -async function _invalidStatusListCredentialId(unsignedCredential, suite) { - const copyUnsignedCredential = klona(unsignedCredential); - copyUnsignedCredential.credentialStatus.statusListCredential = 'invalid-id'; - const signedVc = await vc.issue({ - credential: copyUnsignedCredential, - documentLoader, - suite - }); - return { - path: `${credentialsPath}/invalidStatusListCredentialId.json`, - data: signedVc - }; -} - -async function _invalidCredentialStatusType(validVc) { - const vc = klona(validVc); - vc.credentialStatus.type = 'invalid-type'; - return { - path: `${credentialsPath}/invalidCredentialStatusType.json`, - data: vc, - }; -} - -async function _validVc(unsignedCredential, suite) { - const copyUnsignedCredential = klona(unsignedCredential); - const signedVc = await vc.issue({ - credential: copyUnsignedCredential, - documentLoader, - suite - }); - - return {path: `${credentialsPath}/validVc.json`, signedVc}; -} - -// run main by calling node ./vc-generator -main();