diff --git a/tests/70-conformance.js b/tests/70-conformance.js new file mode 100644 index 00000000..90b33243 --- /dev/null +++ b/tests/70-conformance.js @@ -0,0 +1,36 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import {conformanceSuite} from './suites/conformance.js'; +import {endpoints} from 'vc-test-suite-implementations'; +import {getSuiteConfig} from './test-config.js'; + +const cryptosuites = [ + 'ecdsa-rdfc-2019', + 'ecdsa-sd-2023' +]; + +for(const suiteName of cryptosuites) { + const {tags, credentials, vectors} = getSuiteConfig(suiteName); + const {match: verifiers} = endpoints.filterByTag({ + tags: [...tags], + property: 'verifiers' + }); + for(const vcVersion of vectors.vcTypes) { + const { + document, + mandatoryPointers, + selectivePointers + } = credentials.create[vcVersion]; + conformanceSuite({ + verifiers, + suiteName, + keyTypes: vectors.keyTypes, + vcVersion, + credential: document, + mandatoryPointers, + selectivePointers + }); + } +} diff --git a/tests/80-data-model.js b/tests/80-data-model.js new file mode 100644 index 00000000..e67f2e05 --- /dev/null +++ b/tests/80-data-model.js @@ -0,0 +1,37 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import {dataModelSuite} from './suites/data-model.js'; +import {endpoints} from 'vc-test-suite-implementations'; +import {getSuiteConfig} from './test-config.js'; + +const cryptosuites = [ + 'ecdsa-rdfc-2019', + 'ecdsa-sd-2023' + //FIXME implement jcs 'ecdsa-jcs-2019' +]; + +for(const suiteName of cryptosuites) { + const {tags, credentials, vectors} = getSuiteConfig(suiteName); + const {match: issuers} = endpoints.filterByTag({ + tags: [...tags], + property: 'issuers' + }); + for(const vcVersion of vectors.vcTypes) { + const { + document, + mandatoryPointers, + selectivePointers + } = credentials.create[vcVersion]; + dataModelSuite({ + issuers, + suiteName, + keyTypes: vectors.keyTypes, + vcVersion, + credential: document, + mandatoryPointers, + selectivePointers + }); + } +} diff --git a/tests/suites/conformance.js b/tests/suites/conformance.js new file mode 100644 index 00000000..8df7bce0 --- /dev/null +++ b/tests/suites/conformance.js @@ -0,0 +1,157 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import { + assertions, + generators, + issueCloned +} from 'data-integrity-test-suite-assertion'; +import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +import {getMultiKey} from '../vc-generator/key-gen.js'; +import {getSuite} from '../vc-generator/cryptosuites.js'; + +export function conformanceSuite({ + verifiers, + suiteName, + keyTypes, + vcVersion, + credential, + mandatoryPointers, + selectivePointers, + setup = _setup +}) { + return describe(`${suiteName} - Conformance - VC ${vcVersion}`, function() { + this.matrix = true; + this.report = true; + this.implemented = []; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Implementation'; + const credentials = new Map(keyTypes.map(kt => [kt, null])); + before(async function() { + for(const keyType of keyTypes) { + credentials.set(keyType, await setup({ + credential, + mandatoryPointers, + selectivePointers, + suiteName, + keyType + })); + } + }); + for(const [name, {endpoints}] of verifiers) { + const [verifier] = endpoints; + for(const keyType of keyTypes) { + // add implementer name and keyType to test report + this.implemented.push(`${name}: ${keyType}`); + describe(`${name}: ${keyType}`, function() { + beforeEach(function() { + this.currentTest.cell = { + rowId: this.currentTest.title, + columnId: this.currentTest.parent.title + }; + }); + it('Specifically, all relevant normative statements in Sections 2. ' + + 'Data Model and 3. Algorithms of this document MUST be enforced.', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#:~:text=Specifically%2C%20all%20relevant%20normative%20statements%20in%20Sections%202.%20Data%20Model%20and%203.%20Algorithms%20of%20this%20document%20MUST%20be%20enforced.'; + for(const [key, credential] of credentials.get(keyType)) { + await assertions.verificationFail({ + verifier, + credential, + reason: `Should not verify VC with ${key}` + }); + } + }); + it('Conforming processors MUST produce errors when non-conforming ' + + 'documents are consumed.', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#:~:text=Conforming%20processors%20MUST%20produce%20errors%20when%20non%2Dconforming%20documents%20are%20consumed.'; + for(const [key, credential] of credentials.get(keyType)) { + await assertions.verificationFail({ + verifier, + credential, + reason: `Should not verify VC with ${key}` + }); + } + }); + }); + } + } + }); +} +async function _setup({ + credential, + suiteName, + keyType, + mandatoryPointers, + selectivePointers +}) { + const { + invalidProofType, + invalidVm, + invalidCryptosuite + } = generators?.mandatory; + const credentials = new Map(); + const keyPair = await getMultiKey({keyType}); + const signer = keyPair.signer(); + const _credential = structuredClone(credential); + _credential.issuer = keyPair.controller; + // not bs58 encoded verificationMethod via invalidVm + // type is not DataIntegrityProof invalidType + // invalid cryptosuite name invalidCryptosuite + credentials.set('invalid cryptosuite', await issueCloned(invalidCryptosuite({ + credential: structuredClone(_credential), + ..._getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }) + }))); + credentials.set('invalid VerificationMethod', await issueCloned(invalidVm({ + credential: structuredClone(_credential), + ..._getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }) + }))); + credentials.set('invalid Proof Type', await issueCloned(invalidProofType({ + credential: structuredClone(_credential), + ..._getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }) + }))); + return credentials; +} + +function _getSuites({ + signer, + suiteName, + mandatoryPointers, + selectivePointers +}) { + const suites = { + suite: new DataIntegrityProof({ + signer, + cryptosuite: getSuite({ + suite: suiteName, + mandatoryPointers + }) + }) + }; + if(selectivePointers) { + suites.selectiveSuite = new DataIntegrityProof({ + signer, + cryptosuite: getSuite({ + suite: suiteName, + selectivePointers + }) + }); + } + return suites; +} diff --git a/tests/suites/data-model.js b/tests/suites/data-model.js new file mode 100644 index 00000000..a8f486c1 --- /dev/null +++ b/tests/suites/data-model.js @@ -0,0 +1,130 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import {createInitialVc, endpointCheck} from '../helpers.js'; +import { + assertions, +} from 'data-integrity-test-suite-assertion'; +import {didResolver} from '../didResolver.js'; +import {expect} from 'chai'; + +export function dataModelSuite({ + issuers, + suiteName, + keyTypes, + vcVersion, + credential, + mandatoryPointers +}) { + return describe(`${suiteName} - Data Model - VC ${vcVersion}`, function() { + this.matrix = true; + this.report = true; + this.implemented = []; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Implementation'; + for(const [name, {endpoints}] of issuers) { + for(const keyType of keyTypes) { + for(const issuer of endpoints) { + // does the endpoint support this test? + if(!endpointCheck({endpoint: issuer, keyType, vcVersion})) { + continue; + } + // add implementer name and keyType to test report + this.implemented.push(`${name}: ${keyType}`); + describe(`${name}: ${keyType}`, function() { + let securedCredential = null; + let proofs = []; + before(async function() { + securedCredential = await createInitialVc({ + issuer, + vcVersion, + vc: credential, + mandatoryPointers + }); + if(securedCredential) { + proofs = Array.isArray(securedCredential.proof) ? + securedCredential?.proof : [securedCredential?.proof]; + // only test proofs that match the relevant cryptosuite + proofs = proofs.filter(p => p?.cryptosuite === suiteName); + } + }); + beforeEach(function() { + this.currentTest.cell = { + rowId: this.currentTest.title, + columnId: this.currentTest.parent.title + }; + }); + function assertBefore() { + expect( + securedCredential, + `Expected issuer ${name}: ${keyType} to issue a VC.` + ).to.exist; + expect( + securedCredential, + 'Expected VC to be an object.' + ).to.be.an('object'); + } + it('The publicKeyMultibase value of the verification method MUST ' + + 'start with the base-58-btc prefix (z), as defined in the ' + + 'Multibase section of Controller Documents 1.0. ', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#data-model:~:text=The%20publicKeyMultibase%20value%20of%20the%20verification%20method%20MUST%20start%20with%20the%20base%2D58%2Dbtc%20prefix%20(z)%2C%20as%20defined%20in%20the%20Multibase%20section%20of%20Controller%20Documents%201.0.'; + assertBefore(); + for(const proof of proofs) { + expect(proof.verificationMethod).to.exist; + expect(proof.verificationMethod).to.be.a('string'); + const didDoc = await didResolver({ + url: proof.verificationMethod}); + expect(didDoc).to.be.an('object'); + expect(didDoc.publicKeyMultibase).to.be.a('string'); + expect( + assertions.shouldBeBs58(didDoc.publicKeyMultibase), + 'Expected "publicKeyMultibase" to be Base58 encoded.' + ).to.be.true; + } + }); + it('Any other encoding MUST NOT be allowed. (verificationMethod)', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#multikey'; + assertBefore(); + for(const proof of proofs) { + expect(proof.verificationMethod).to.exist; + expect(proof.verificationMethod).to.be.a('string'); + const didDoc = await didResolver({ + url: proof.verificationMethod}); + expect(didDoc).to.be.an('object'); + expect(didDoc.publicKeyMultibase).to.be.a('string'); + expect( + assertions.shouldBeBs58(didDoc.publicKeyMultibase), + 'Expected "publicKeyMultibase" to be Base58 encoded.' + ).to.be.true; + } + }); + it('The type property MUST be DataIntegrityProof.', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#multikey:~:text=The%20type%20property%20MUST%20be%20DataIntegrityProof.'; + assertBefore(); + for(const proof of proofs) { + expect(proof.type).to.exist; + expect(proof.type).to.be.a('string'); + expect(proof.type).to.equal('DataIntegrityProof'); + } + }); + it('The cryptosuite property MUST be ecdsa-rdfc-2019, ' + + 'ecdsa-jcs-2019, or ecdsa-sd-2023.', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#multikey:~:text=The%20cryptosuite%20property%20MUST%20be%20ecdsa%2Drdfc%2D2019%2C%20ecdsa%2Djcs%2D2019%2C%20or%20ecdsa%2Dsd%2D2023.'; + assertBefore(); + for(const proof of proofs) { + expect(proof.cryptosuite).to.exist; + expect(proof.cryptosuite).to.be.a('string'); + expect(proof.cryptosuite).to.be.oneOf( + ['ecdsa-rdfc-2019', 'edcsa-jcs-2019', 'ecdsa-sd-2023']); + } + }); + }); + } + } + } + }); +}