diff --git a/tests/10-rdfc-create.js b/tests/10-rdfc-create.js deleted file mode 100644 index a12d0c2a..00000000 --- a/tests/10-rdfc-create.js +++ /dev/null @@ -1,219 +0,0 @@ -/*! - * Copyright 2023-2024 Digital Bazaar, Inc. - * SPDX-License-Identifier: BSD-3-Clause - */ -import {createInitialVc, endpointCheck} from './helpers.js'; -import { - shouldBeBs58, - shouldBeMulticodecEncoded, - shouldHaveByteLength, - verificationSuccess -} from './assertions.js'; -import {cryptosuite} from '@digitalbazaar/ecdsa-rdfc-2019-cryptosuite'; -import {documentLoader} from './documentLoader.js'; -import {endpoints} from 'vc-test-suite-implementations'; -import {expect} from 'chai'; -import {getSuiteConfig} from './test-config.js'; -import {localVerifier} from './vc-verifier/index.js'; - -const { - tags, - credentials, - vectors, - proofLengths -} = getSuiteConfig('ecdsa-rdfc-2019'); -const {match} = endpoints.filterByTag({ - tags: [...tags], - property: 'issuers' -}); - -const verifier = localVerifier({cryptosuite}); - -describe('ecdsa-rdfc-2019 (create)', function() { - for(const vcVersion of vectors.vcTypes) { - describe(`ecdsa-rdfc-2019 (issuers) VC ${vcVersion}`, function() { - this.matrix = true; - this.report = true; - this.implemented = []; - this.rowLabel = 'Test Name'; - this.columnLabel = 'Implementation'; - for(const [name, {endpoints: issuers}] of match) { - // test for each support key type - for(const keyType of vectors.keyTypes) { - // loop through each issuer in suite - for(const issuer of issuers) { - // 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 issuedVc; - let proofs; - const verificationMethodDocuments = []; - before(async function() { - issuedVc = await createInitialVc({ - issuer, - vc: credentials.create[vcVersion].document, - vcVersion - }); - // VCs can have multiple proofs so account for that - proofs = Array.isArray(issuedVc?.proof) ? issuedVc.proof : - [issuedVc?.proof]; - const verificationMethods = proofs.map( - proof => proof.verificationMethod); - for(const verificationMethod of verificationMethods) { - const verificationMethodDocument = await documentLoader({ - url: verificationMethod - }); - verificationMethodDocuments.push(verificationMethodDocument); - } - }); - /* - @link https://w3c.github.io/vc-di-ecdsa/ - #verify-derived-proof-ecdsa-sd-2023:~:text= - The%20type%20property%20MUST%20be%20DataIntegrityProof - */ - it('The (proof) type property MUST be DataIntegrityProof.', - function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - proofs.map(p => p?.type).should.contain( - 'DataIntegrityProof', - 'Expected at least one proof to have type ' + - 'DataIntegrityProof'); - }); - /* - @link https://w3c.github.io/vc-di-ecdsa/ - #verify-derived-proof-ecdsa-sd-2023:~:text= - The%20cryptosuite%20property%20MUST%20be%20ecdsa%2Drdfc%2D2019% - 2C%20ecdsa%2Djcs%2D2019%2C%20or%20ecdsa%2Dsd%2D2023. - */ - it('The cryptosuite property of the proof MUST be ' + - 'ecdsa-rdfc-2019 or ecdsa-jcs-2019.', function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - const cryptosuite = ['ecdsa-rdfc-2019', 'ecdsa-jcs-2019']; - expect( - proofs.map(proof => proof?.cryptosuite), - 'Expected at least one proof to have ' + - '"cryptosuite" property "ecdsa-rdfc-2019" or "ecdsa-jcs-2019".') - .to.contain.oneOf(cryptosuite); - }); - it('the signature value (proofValue) MUST be expressed ' + - 'according to section 7 of [RFC4754] (sometimes referred to ' + - 'as the IEEE P1363 format) and encoded according to the ' + - 'specific cryptosuite proof generation algorithm.', - async function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - const _proof = proofs.find(p => - p?.cryptosuite === 'ecdsa-rdfc-2019'); - expect( - _proof, - `Expected VC from issuer ${name} to have an ' + - '"ecdsa-rdfc-2019" proof`).to.exist; - expect( - _proof.proofValue, - `Expected VC from issuer ${name} to have a ' + - '"proof.proofValue"` - ).to.exist; - expect( - _proof.proofValue, - `Expected VC "proof.proofValue" from issuer ${name} to be ` + - 'a string.' - ).to.be.a.string; - // now test the encoding which is bs58 for this suite - expect( - shouldBeBs58(_proof.proofValue), - 'Expected "proof.proofValue" to be bs58 encoded.' - ).to.be.true; - // proofBytes will be exactly 64 bytes in size for a P-256 key, - // and 96 bytes in size for a P-384 key. - const expectedLength = proofLengths[keyType]; - await shouldHaveByteLength(_proof.proofValue, expectedLength); - }); - it('The "proof" MUST verify with a conformant verifier.', - async function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - await verificationSuccess({ - credential: issuedVc, - verifier - }); - }); - it('The "proof.proofPurpose" field MUST match the verification ' + - 'relationship expressed by the verification method controller.', - async function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - verificationMethodDocuments.should.not.eql([], 'Expected ' + - 'at least one "verificationMethodDocument".'); - verificationMethodDocuments.some( - verificationMethodDocument => - verificationMethodDocument?.type === 'Multikey' - ).should.equal(true, 'Expected at least one proof to have ' + - '"type" property value "Multikey".' - ); - const controllerDocuments = []; - for(const verificationMethodDocument of - verificationMethodDocuments) { - const controllerDocument = await documentLoader({ - url: verificationMethodDocument.controller - }); - controllerDocuments.push(controllerDocument); - } - proofs.some( - proof => controllerDocuments.some(controllerDocument => - controllerDocument.hasOwnProperty(proof.proofPurpose)) - ).should.equal(true, 'Expected "proof.proofPurpose" field ' + - 'to match the verification method controller.' - ); - }); - it('Dereferencing "verificationMethod" MUST result in an ' + - 'object containing a type property with "Multikey" value.', - async function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - verificationMethodDocuments.should.not.eql([], 'Expected ' + - 'at least one "verificationMethodDocument".'); - verificationMethodDocuments.some( - verificationMethodDocument => - verificationMethodDocument?.type === 'Multikey' - ).should.equal(true, 'Expected at least one proof to have ' + - '"type" property value "Multikey".' - ); - }); - it('The "publicKeyMultibase" property of the verification ' + - 'method MUST be public key encoded according to MULTICODEC ' + - 'and formatted according to MULTIBASE.', async function() { - this.test.cell = { - columnId: `${name}: ${keyType}`, rowId: this.test.title - }; - verificationMethodDocuments.should.not.eql([], 'Expected ' + - '"verificationMethodDocuments" to not be empty.'); - verificationMethodDocuments.some( - verificationMethodDocument => { - const multibase = 'z'; - const {publicKeyMultibase} = verificationMethodDocument; - return publicKeyMultibase.startsWith(multibase) && - shouldBeBs58(publicKeyMultibase) && - shouldBeMulticodecEncoded(publicKeyMultibase); - } - ).should.equal(true, 'Expected at "publicKeyMultibase" to ' + - 'be MULTIBASE formatted and MULTICODEC encoded.'); - }); - }); - } - } - } - }); - } -}); diff --git a/tests/20-rdfc-verify.js b/tests/20-rdfc-verify.js deleted file mode 100644 index 7f42f3d1..00000000 --- a/tests/20-rdfc-verify.js +++ /dev/null @@ -1,25 +0,0 @@ -import { - itMustVerifyValidVC, - itRejectsInvalidCryptosuite, -} from './assertions.js'; -import {defineSuiteConformanceTests} from './suiteTests.js'; -import {issueCredentials} from './vc-generator/index.js'; - -function buildTestVectors(suite, credentials, keyTypes) { - return issueCredentials({ - credentials: Object.entries(credentials.verify), - suite, - keyTypes - }); -} - -describe('ecdsa-rdfc-2019 (verify)', async function() { - await defineSuiteConformanceTests({ - suite: 'ecdsa-rdfc-2019', - testCategory: 'verifiers', - buildTestVectorsFn: buildTestVectors, - }, function(args) { - itMustVerifyValidVC(args); - itRejectsInvalidCryptosuite(['ecdsa-rdfc-2019', 'ecdsa-jcs-2019'], args); - }); -}); diff --git a/tests/90-algorithms-rdfc.js b/tests/90-algorithms-rdfc.js new file mode 100644 index 00000000..cab9baf2 --- /dev/null +++ b/tests/90-algorithms-rdfc.js @@ -0,0 +1,227 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import { + config, + createInitialVc, + createValidCredential, + getProofs, + isValidDatetime, + isValidUtf8, + setupReportableTestSuite, + setupRow +} from './helpers.js'; +import chai from 'chai'; +import {endpoints} from 'vc-test-suite-implementations'; + +const should = chai.should(); + +const cryptosuite = 'ecdsa-rdfc-2019'; +const {tags} = config.suites[ + cryptosuite +]; +const {match: issuers} = endpoints.filterByTag({ + tags: [...tags], + property: 'issuers' +}); + +describe('ecdsa-rdfc-2019 - Algorithms - Transformation', function() { + setupReportableTestSuite(this); + this.implemented = [...issuers.keys()]; + let validCredential; + before(async function() { + validCredential = await createValidCredential(); + }); + for(const [columnId, {endpoints}] of issuers) { + describe(columnId, function() { + const [issuer] = endpoints; + let issuedVc; + let proofs; + let rdfc2019Proofs = []; + before(async function() { + issuedVc = await createInitialVc({issuer, vc: validCredential}); + proofs = getProofs(issuedVc); + if(proofs?.length) { + rdfc2019Proofs = proofs.filter( + proof => proof?.cryptosuite === cryptosuite); + } + }); + beforeEach(setupRow); + const assertBefore = () => { + should.exist(issuedVc, 'Expected issuer to have issued a ' + + 'credential.'); + should.exist(proofs, 'Expected credential to have a proof.'); + rdfc2019Proofs.length.should.be.gte(1, 'Expected at least one ' + + 'ecdsa-rdfc-2019 cryptosuite.'); + }; + it('The transformation options MUST contain a type identifier ' + + 'for the cryptographic suite (type) and a cryptosuite identifier ' + + '(cryptosuite).', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof.type, 'Expected a type identifier on ' + + 'the proof.'); + should.exist(proof.cryptosuite, + 'Expected a cryptosuite identifier on the proof.'); + } + }); + it('Whenever this algorithm encodes strings, ' + + 'it MUST use UTF-8 encoding.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof?.proofValue, + 'Expected proofValue to exist.'); + isValidUtf8(proof.proofValue).should.equal( + true, + 'Expected proofValue value to be a valid UTF-8 encoded string.' + ); + } + }); + it('If options.type is not set to the string DataIntegrityProof or ' + + 'options.cryptosuite is not set to the string ecdsa-rdfc-2019, ' + + 'an error MUST be raised and SHOULD convey an error type ' + + 'of PROOF_TRANSFORMATION_ERROR.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof.type, + 'Expected a type identifier on the proof.'); + should.exist(proof.cryptosuite, + 'Expected a cryptosuite identifier on the proof.'); + proof.type.should.equal('DataIntegrityProof', + 'Expected DataIntegrityProof type.'); + proof.cryptosuite.should.equal('ecdsa-rdfc-2019', + 'Expected ecdsa-rdfc-2019 cryptosuite.'); + } + }); + }); + } +}); + +describe('ecdsa-rdfc-2019 - Algorithms - Proof Configuration', function() { + setupReportableTestSuite(this); + this.implemented = [...issuers.keys()]; + let validCredential; + before(async function() { + validCredential = await createValidCredential(); + }); + for(const [columnId, {endpoints}] of issuers) { + describe(columnId, function() { + const [issuer] = endpoints; + let issuedVc; + let proofs; + let rdfc2019Proofs = []; + before(async function() { + issuedVc = await createInitialVc({issuer, vc: validCredential}); + proofs = getProofs(issuedVc); + if(proofs?.length) { + rdfc2019Proofs = proofs.filter( + proof => proof?.cryptosuite === cryptosuite); + } + }); + beforeEach(setupRow); + const assertBefore = () => { + should.exist(issuedVc, 'Expected issuer to have issued a ' + + 'credential.'); + should.exist(proofs, 'Expected credential to have a proof.'); + rdfc2019Proofs.length.should.be.gte(1, 'Expected at least one ' + + 'ecdsa-rdfc-2019 cryptosuite.'); + }; + it('The proof options MUST contain a type identifier for the ' + + 'cryptographic suite (type) and MUST contain a cryptosuite ' + + 'identifier (cryptosuite).', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof.type, + 'Expected a type identifier on the proof.'); + should.exist(proof.cryptosuite, + 'Expected a cryptosuite identifier on the proof.'); + } + }); + it('If proofConfig.type is not set to DataIntegrityProof ' + + 'and/or proofConfig.cryptosuite is not set to ecdsa-rdfc-2019, ' + + 'an error MUST be raised and SHOULD convey an error type ' + + 'of PROOF_GENERATION_ERROR.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof.type, + 'Expected a type identifier on the proof.'); + should.exist(proof.cryptosuite, + 'Expected a cryptosuite identifier on the proof.'); + proof.type.should.equal('DataIntegrityProof', + 'Expected DataIntegrityProof type.'); + proof.cryptosuite.should.equal('ecdsa-rdfc-2019', + 'Expected ecdsa-rdfc-2019 cryptosuite.'); + } + }); + it('If proofConfig.created is set and if the value is not a ' + + 'valid [XMLSCHEMA11-2] datetime, an error MUST be raised and ' + + 'SHOULD convey an error type of PROOF_GENERATION_ERROR.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019'; + for(const proof of rdfc2019Proofs) { + if(proof?.created) { + isValidDatetime(proof.created).should.equal( + true, + 'Expected created value to be a valid datetime string.' + ); + } + } + }); + }); + } +}); + +describe('ecdsa-rdfc-2019 - Algorithms - Proof Serialization', function() { + setupReportableTestSuite(this); + this.implemented = [...issuers.keys()]; + let validCredential; + before(async function() { + validCredential = await createValidCredential(); + }); + for(const [columnId, {endpoints}] of issuers) { + describe(columnId, function() { + const [issuer] = endpoints; + let issuedVc; + let proofs; + let rdfc2019Proofs = []; + before(async function() { + issuedVc = await createInitialVc({issuer, vc: validCredential}); + proofs = getProofs(issuedVc); + if(proofs?.length) { + rdfc2019Proofs = proofs.filter( + proof => proof?.cryptosuite === cryptosuite); + } + }); + beforeEach(setupRow); + const assertBefore = () => { + should.exist(issuedVc, 'Expected issuer to have issued a ' + + 'credential.'); + should.exist(proofs, 'Expected credential to have a proof.'); + rdfc2019Proofs.length.should.be.gte(1, 'Expected at least one ' + + 'ecdsa-rdfc-2019 cryptosuite.'); + }; + it('The proof options MUST contain a type identifier for the ' + + 'cryptographic suite (type) and MAY contain a cryptosuite identifier ' + + '(cryptosuite).', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-di-ecdsa/#proof-serialization-ecdsa-rdfc-2019'; + assertBefore(); + for(const proof of rdfc2019Proofs) { + should.exist(proof.type, + 'Expected a type identifier on the proof.'); + } + }); + }); + } +}); diff --git a/tests/helpers.js b/tests/helpers.js index 5dc06626..3d1f9fcf 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -5,7 +5,9 @@ import * as bs58 from 'base58-universal'; import * as bs64 from 'base64url-universal'; import {createRequire} from 'node:module'; +import {isUtf8} from 'node:buffer'; import {klona} from 'klona'; +import {readFileSync} from 'fs'; import {v4 as uuidv4} from 'uuid'; export const require = createRequire(import.meta.url); @@ -34,6 +36,9 @@ export const ISOTimeStamp = ({date = new Date()} = {}) => { * * @returns {Promise} The resulting issuance result. */ + +export const config = JSON.parse(readFileSync('./config/runner.json')); + export const createInitialVc = async ({ issuer, vc, @@ -216,3 +221,64 @@ export function setupReportableTestSuite(runnerContext, name) { runnerContext.implemented = []; } + +export function isValidUtf8(string) { + const textEncoder = new TextEncoder(); + const uint8Array = textEncoder.encode(string); + if(!isUtf8(uint8Array)) { + return false; + } else { + return true; + } +} + +export function isValidDatetime(dateString) { + return !isNaN(Date.parse(dateString)); +} + +export function getProofs(issuedVc) { + // if the implementation failed to issue a VC or to sign the VC, + // return an empty array + if(!issuedVc?.proof) { + return []; + } + const proofs = Array.isArray(issuedVc?.proof) ? + issuedVc.proof : [issuedVc?.proof]; + return proofs; +} + +export function createValidCredential(version = 2) { + let credential = { + type: ['VerifiableCredential'], + id: `urn:uuid:${uuidv4()}`, + credentialSubject: {id: 'did:example:alice'} + }; + if(version === 1) { + // add v1.1 context and issuanceDate + credential = Object.assign({}, { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/security/data-integrity/v2' + ], + issuanceDate: ISOTimeStamp() + }, credential); + } else if(version === 2) { + // add v2 context + credential = Object.assign({}, { + '@context': [ + 'https://www.w3.org/ns/credentials/v2' + ] + }, credential); + } else { + return null; + } + return credential; +} + +export function setupRow() { + // append test meta data to the it/test this. + this.currentTest.cell = { + columnId: this.currentTest.parent.title, + rowId: this.currentTest.title + }; +}