diff --git a/config/runner.json b/config/runner.json index d3dc716e..52f24956 100644 --- a/config/runner.json +++ b/config/runner.json @@ -8,7 +8,17 @@ "P-384": 96 }, "interop": { - "vcVersion": "1.1" + "vcVersion": "2.0" + } + }, + "ecdsa-jcs-2019": { + "tags": ["ecdsa-jcs-2019"], + "proofLengths": { + "P-256": 64, + "P-384": 96 + }, + "interop": { + "vcVersion": "2.0" } }, "ecdsa-sd-2023": { diff --git a/tests/90-algorithms-jcs.js b/tests/90-algorithms-jcs.js new file mode 100644 index 00000000..6fe2c455 --- /dev/null +++ b/tests/90-algorithms-jcs.js @@ -0,0 +1,7 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import {ecdsaJcs2019Algorithms} from './suites/algorithms-jcs.js'; + +ecdsaJcs2019Algorithms(); diff --git a/tests/helpers.js b/tests/helpers.js index 5dc06626..04d907dd 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); @@ -89,8 +91,8 @@ export const createDisclosedVc = async ({ export const endpointCheck = ({endpoint, vcVersion, keyType}) => { const { supportedEcdsaKeyTypes, - // assume support for vc 1.1 - supports = {vc: ['1.1']} + // assume support for vc 2.0 + supports = {vc: ['2.0']} } = endpoint.settings; // if an issuer does not support the current keyType skip it const keyTypes = supportedEcdsaKeyTypes || supports?.keyTypes; @@ -208,11 +210,76 @@ export function getColumnNameForTestCategory(testCategory) { } } -export function setupReportableTestSuite(runnerContext, name) { +export function setupReportableTestSuite( + runnerContext, + name = 'Implementation' +) { runnerContext.matrix = true; runnerContext.report = true; runnerContext.rowLabel = 'Test Name'; runnerContext.columnLabel = 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 const config = JSON.parse(readFileSync('./config/runner.json')); + +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 + }; +} + +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; +} diff --git a/tests/suites/algorithms-jcs.js b/tests/suites/algorithms-jcs.js new file mode 100644 index 00000000..5c55a95a --- /dev/null +++ b/tests/suites/algorithms-jcs.js @@ -0,0 +1,196 @@ +/*! + * 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(); + +export function ecdsaJcs2019Algorithms() { + const cryptosuite = 'ecdsa-jcs-2019'; + const {tags} = config.suites[ + cryptosuite + ]; + const {match: issuers} = endpoints.filterByTag({ + tags: [...tags], + property: 'issuers' + }); + + describe('ecdsa-jcs-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 jcs2019Proofs = []; + before(async function() { + issuedVc = await createInitialVc({issuer, vc: validCredential}); + proofs = getProofs(issuedVc); + if(proofs?.length) { + jcs2019Proofs = 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.'); + jcs2019Proofs.length.should.be.gte(1, 'Expected at least one ' + + 'ecdsa-jcs-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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + should.exist(proof.type, + 'Expected a type identifier on the proof.'); + } + }); + 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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + 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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + 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-jcs-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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + 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-jcs-2019', + 'Expected ecdsa-jcs-2019 cryptosuite.'); + } + }); + }); + } + }); + + describe('ecdsa-jcs-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 jcs2019Proofs = []; + before(async function() { + issuedVc = await createInitialVc({issuer, vc: validCredential}); + proofs = getProofs(issuedVc); + if(proofs?.length) { + jcs2019Proofs = 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.'); + jcs2019Proofs.length.should.be.gte(1, 'Expected at least one ' + + 'ecdsa-jcs-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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + 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-jcs-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-jcs-2019'; + assertBefore(); + for(const proof of jcs2019Proofs) { + 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-jcs-2019', + 'Expected ecdsa-jcs-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-jcs-2019'; + for(const proof of jcs2019Proofs) { + if(proof?.created) { + isValidDatetime(proof.created).should.equal( + true, + 'Expected created value to be a valid datetime string.' + ); + } + } + }); + }); + } + }); +}