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/90-algorithms.js b/tests/90-algorithms.js new file mode 100644 index 00000000..e14901ba --- /dev/null +++ b/tests/90-algorithms.js @@ -0,0 +1,47 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import { + commonAlgorithms, + ecdsaRdfc2019Algorithms +} from './suites/algorithms.js'; +import {endpoints} from 'vc-test-suite-implementations'; +import {getSuiteConfig} from './test-config.js'; + +const cryptosuites = [ + 'ecdsa-rdfc-2019', +]; + +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]; + ecdsaRdfc2019Algorithms({ + verifiers, + suiteName, + keyTypes: vectors.keyTypes, + vcVersion, + credential: document, + mandatoryPointers, + selectivePointers + }); + commonAlgorithms({ + verifiers, + suiteName, + keyTypes: vectors.keyTypes, + vcVersion, + credential: document, + mandatoryPointers, + selectivePointers + }); + } +} 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.' + ); + } + } + }); + }); + } + }); +} diff --git a/tests/suites/algorithms.js b/tests/suites/algorithms.js new file mode 100644 index 00000000..f26df9a5 --- /dev/null +++ b/tests/suites/algorithms.js @@ -0,0 +1,413 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import { + assertions, + generators, + issueCloned +} from 'data-integrity-test-suite-assertion'; +import crypto from 'node:crypto'; +import {getMultiKey} from '../vc-generator/key-gen.js'; +import {getSuites} from './helpers.js'; + +export function commonAlgorithms({ + credential, + verifiers, + mandatoryPointers, + selectivePointers, + keyTypes, + suiteName, + vcVersion, + setup = _commonSetup +}) { + const title = `${suiteName} - Algorithms Common - VC ${vcVersion}`; + return describe(title, function() { + this.matrix = true; + this.report = true; + this.implemented = []; + this.rowLabel = 'Test Name'; + this.columnLabel = 'Implementation'; + const credentials = new Map(keyTypes.map(keyType => [keyType, null])); + before(async function() { + for(const keyType of keyTypes) { + credentials.set(keyType, await setup({ + credential, + verifiers, + mandatoryPointers, + selectivePointers, + keyType, + suiteName, + vcVersion + })); + } + }); + for(const [name, {endpoints}] of verifiers) { + const [verifier] = endpoints; + this.implemented.push(`${name}`); + describe(`${name}`, function() { + beforeEach(function() { + this.currentTest.cell = { + rowId: this.currentTest.title, + columnId: this.currentTest.parent.title + }; + }); + it('When generating ECDSA signatures, the signature value 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.link = 'https://w3c.github.io/vc-di-ecdsa/#algorithms:~:text=When%20generating%20ECDSA%20signatures%2C%20the%20signature%20value%20MUST%20be%20expressed%20according%20to%20section%207%20of%20%5BRFC4754%5D%20(sometimes%20referred%20to%20as%20the%20IEEE%20P1363%20format)%20and%20encoded%20according%20to%20the%20specific%20cryptosuite%20proof%20generation%20algorithm'; + for(const [keyType, fixtures] of credentials) { + await assertions.verificationFail({ + credential: fixtures.get('invalidHash'), + verifier, + reason: `Should not verify VC signed w/ ${keyType} & invalidHash.` + }); + } + }); + if(keyTypes.includes('P-256')) { + it('For P-256 keys, the default hashing function, SHA-2 with 256 ' + + 'bits of output, MUST be used.', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#algorithms:~:text=For%20P%2D256%20keys%2C%20the%20default%20hashing%20function%2C%20SHA%2D2%20with%20256%20bits%20of%20output%2C%20MUST%20be%20used.'; + await assertions.verificationFail({ + credential: credentials.get('P-256').get('invalidHash'), + verifier, + reason: `Should not verify VC with invalid hash.` + }); + }); + } + if(keyTypes.includes('P-384')) { + it('For P-384 keys, SHA-2 with 384-bits of output MUST be used, ' + + 'specified via the RDFC-1.0 implementation-specific parameter.', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#algorithms:~:text=For%20P%2D384%20keys%2C%20SHA%2D2%20with%20384%2Dbits%20of%20output%20MUST%20be%20used%2C%20specified%20via%20the%20RDFC%2D1.0%20implementation%2Dspecific%20parameter.'; + await assertions.verificationFail({ + credential: credentials.get('P-384').get('invalidHash'), + verifier, + reason: `Should not verify VC with invalid hash.` + }); + }); + } + }); + } + }); +} + +export function ecdsaRdfc2019Algorithms({ + credential, + verifiers, + mandatoryPointers, + selectivePointers, + keyTypes, + suiteName, + vcVersion, + setup = _setup +}) { + return describe(`${suiteName} - Algorithms - 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({ + suiteName, + keyType, + credential, + mandatoryPointers, + selectivePointers + })); + } + }); + for(const [name, {endpoints}] of verifiers) { + const [verifier] = endpoints; + for(const keyType of keyTypes) { + this.implemented.push(`${name}: ${keyType}`); + describe(`${name}: ${keyType}`, function() { + beforeEach(function() { + this.currentTest.cell = { + rowId: this.currentTest.title, + columnId: this.currentTest.parent.title + }; + }); + 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://w3c.github.io/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('noTypeOrCryptosuite'), + reason: 'Should not verify VC w/ no type or cryptosuite ' + + 'identifier' + }); + }); + it('Whenever this algorithm encodes strings, it MUST use UTF-8 ' + + 'encoding. (proof.type)', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('notUTF8'), + reason: 'Should not verify VC w/ non UTF-8 encoding' + }); + }); + it('If options.type is not set to the string DataIntegrityProof ' + + 'and options.cryptosuite is not set to the string ecdsa-rdfc-2019, ' + + 'an error MUST be raised ', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#transformation-ecdsa-rdfc-2019:~:text=If%20options.type%20is%20not%20set%20to%20the%20string%20DataIntegrityProof%20and%20options.cryptosuite%20is%20not%20set%20to%20the%20string%20ecdsa%2Drdfc%2D2019%2C%20an%20error%20MUST%20be%20raised'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('noTypeOrCryptosuite'), + reason: 'Should not verify VC w/ no type or cryptosuite ' + + 'identifier' + }); + }); + 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://w3c.github.io/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('noTypeOrCryptosuite'), + reason: 'Should not verify VC w/ no type or cryptosuite ' + + 'identifier' + }); + }); + 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', async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019:~:text=If%20proofConfig.type%20is%20not%20set%20to%20DataIntegrityProof%20and/or%20proofConfig.cryptosuite%20is%20not%20set%20to%20ecdsa%2Drdfc%2D2019%2C%20an%20error%20MUST%20be%20raised'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('noTypeOrCryptosuite'), + reason: 'Should not verify VC w/ no type or cryptosuite ' + + 'identifier' + }); + }); + it('If proofConfig.created is set and if the value is not a valid ' + + '[XMLSCHEMA11-2] datetime, an error MUST be raised', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-ecdsa/#proof-configuration-ecdsa-rdfc-2019'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('invalidCreated'), + reason: 'Should not verify VC w/ invalid "proof.created"' + }); + }); + 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://w3c.github.io/vc-di-ecdsa/#proof-serialization-ecdsa-rdfc-2019'; + await assertions.verificationFail({ + verifier, + credentials: credentials.get('noTypeOrCryptosuite'), + reason: 'Should not verify VC w/ no type or cryptosuite ' + + 'identifier' + }); + }); + }); + + } + } + }); +} + +async function _setup({ + credential, + mandatoryPointers, + selectivePointers, + suiteName, + keyType +}) { + const credentials = new Map(); + const keyPair = await getMultiKey({keyType}); + const signer = keyPair.signer(); + const _credential = structuredClone(credential); + _credential.issuer = keyPair.controller; + const {invalidCreated} = generators?.dates; + credentials.set('invalidCreated', await issueCloned(invalidCreated({ + credential: structuredClone(_credential), + ...getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }) + }))); + // stub suite canonize via Proxy cryptosuite.canonize and set safe to false + credentials.set('noTypeOrCryptosuite', + await issueCloned(_generateNoTypeCryptosuite({ + signer, + suiteName, + credential: _credential, + mandatoryPointers, + selectivePointers + }))); + return credentials; +} + +function _generateNoTypeCryptosuite({ + signer, + suiteName, + credential, + mandatoryPointers, + selectivePointers +}) { + const {suite, selectiveSuite} = getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }); + const { + invalidProofType, + invalidCryptosuite + } = generators?.mandatory; + const noType = invalidProofType({ + credential: structuredClone(credential), + suite: unsafeProxy(suite), + selectiveSuite: unsafeProxy(selectiveSuite), + proofType: '' + }); + return invalidCryptosuite({...noType, cryptosuiteName: ''}); +} + +function unsafeProxy(suite) { + if(typeof suite !== 'object') { + return suite; + } + // if the suite has a cryptosuite object proxy it + if(suite._cryptosuite) { + suite._cryptosuite = new Proxy(suite._cryptosuite, { + get(target, prop) { + if(prop === 'canonize') { + return function(doc, options) { + return target.canonize(doc, {...options, safe: false}); + }; + } + return Reflect.get(...arguments); + } + }); + } + return new Proxy(suite, { + get(target, prop) { + if(prop === 'canonize') { + return function(doc, options) { + return target.canonize(doc, {...options, safe: false}); + }; + } + return Reflect.get(...arguments); + } + }); +} + +async function _commonSetup({ + credential, + mandatoryPointers, + selectivePointers, + suiteName, + keyType +}) { + const credentials = new Map(); + const keyPair = await getMultiKey({keyType}); + const signer = keyPair.signer(); + const _credential = structuredClone(credential); + _credential.issuer = keyPair.controller; + const {suite, selectiveSuite} = getSuites({ + signer, + suiteName, + selectivePointers, + mandatoryPointers + }); + credentials.set('invalidHash', await issueCloned({ + credential: _credential, + suite: invalidHashProxy({suite, suiteName, keyType}), + selectiveSuite: invalidHashProxy({ + suite: selectiveSuite, + suiteName, + keyType + }) + })); + return credentials; +} + +function invalidHashProxy({ + suiteName, + keyType, + suite, +}) { + if(typeof suite !== 'object') { + return suite; + } + if(suite._cryptosuite) { + if(suiteName !== 'ecdsa-rdfc-2019') { + throw new Error(`Unsupported suite ${suiteName}`); + } + suite._cryptosuite = new Proxy(suite._cryptosuite, { + get(target, prop) { + if(prop === 'createVerifyData') { + return async function({ + cryptosuite, document, proof, + documentLoader, dataIntegrityProof + } = {}) { + // this switch the hash to the wrong hash for that keyType + const algorithm = (keyType === 'P-256') ? 'sha384' : 'sha256'; + const c14nOptions = { + documentLoader, + safe: true, + base: null, + skipExpansion: false, + messageDigestAlgorithm: algorithm + }; + + // await both c14n proof hash and c14n document hash + const [proofHash, docHash] = await Promise.all([ + // canonize and hash proof + _canonizeProof(proof, { + document, cryptosuite, dataIntegrityProof, c14nOptions + }).then(c14nProofOptions => sha({ + algorithm, + string: c14nProofOptions + })), + // canonize and hash document + cryptosuite.canonize(document, c14nOptions).then( + c14nDocument => sha({algorithm, string: c14nDocument})) + ]); + // concatenate hash of c14n proof options and hash of c14n document + return _concat(proofHash, docHash); + }; + } + return Reflect.get(...arguments); + } + }); + } + return suite; +} + +function _concat(b1, b2) { + const rval = new Uint8Array(b1.length + b2.length); + rval.set(b1, 0); + rval.set(b2, b1.length); + return rval; +} + +export async function sha({algorithm, string}) { + return new Uint8Array(crypto.createHash(algorithm).update(string).digest()); +} + +async function _canonizeProof(proof, { + document, cryptosuite, dataIntegrityProof, c14nOptions +}) { + // `proofValue` must not be included in the proof options + proof = { + '@context': document['@context'], + ...proof + }; + dataIntegrityProof.ensureSuiteContext({ + document: proof, addSuiteContext: true + }); + delete proof.proofValue; + return cryptosuite.canonize(proof, c14nOptions); +} + diff --git a/tests/suites/conformance.js b/tests/suites/conformance.js index 8df7bce0..1e0e371f 100644 --- a/tests/suites/conformance.js +++ b/tests/suites/conformance.js @@ -7,9 +7,8 @@ import { 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'; +import {getSuites} from './helpers.js'; export function conformanceSuite({ verifiers, @@ -42,7 +41,7 @@ export function conformanceSuite({ for(const [name, {endpoints}] of verifiers) { const [verifier] = endpoints; for(const keyType of keyTypes) { - // add implementer name and keyType to test report + // add implementer name and keyType to test report this.implemented.push(`${name}: ${keyType}`); describe(`${name}: ${keyType}`, function() { beforeEach(function() { @@ -101,7 +100,7 @@ async function _setup({ // invalid cryptosuite name invalidCryptosuite credentials.set('invalid cryptosuite', await issueCloned(invalidCryptosuite({ credential: structuredClone(_credential), - ..._getSuites({ + ...getSuites({ signer, suiteName, selectivePointers, @@ -110,7 +109,7 @@ async function _setup({ }))); credentials.set('invalid VerificationMethod', await issueCloned(invalidVm({ credential: structuredClone(_credential), - ..._getSuites({ + ...getSuites({ signer, suiteName, selectivePointers, @@ -119,7 +118,7 @@ async function _setup({ }))); credentials.set('invalid Proof Type', await issueCloned(invalidProofType({ credential: structuredClone(_credential), - ..._getSuites({ + ...getSuites({ signer, suiteName, selectivePointers, @@ -128,30 +127,3 @@ async function _setup({ }))); 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/helpers.js b/tests/suites/helpers.js new file mode 100644 index 00000000..6d06791c --- /dev/null +++ b/tests/suites/helpers.js @@ -0,0 +1,36 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +import {getSuite} from '../vc-generator/cryptosuites.js'; + +// FIXME:an existing function in data integrity test suite assertion +// does something similar to this function, but is not currently exported +export 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; +}