diff --git a/tests/assertions.js b/tests/assertions.js index 388027d..ef2ce95 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -111,7 +111,7 @@ export const shouldNotUseCborTags = ({proof}) => { export const baseProofShouldHaveElementCount = ({ proof, - expectedLengths = [5, 6], + expectedLengths = [5, 6, 7], reason = 'Expected baseProof to have expected number of components' }) => { let error; diff --git a/tests/setup.js b/tests/setup.js index ff0d6aa..3728354 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -14,6 +14,10 @@ import { getMultikeys, issueCredentials } from './vc-generator/index.js'; +import { + encodeProofValue, + parseDisclosureProofValue +} from './vc-generator/stubMethods.js'; import {generators} from 'data-integrity-test-suite-assertion'; import {writeFile} from 'node:fs/promises'; @@ -144,6 +148,21 @@ export async function verifySetup({credentials, keyTypes, suite}) { } } disclosed.invalid.valuePrefix = valuePrefix; + // invalid element count means less than 4 components + const componentCount = disclosed.invalid.componentCount = new Map(); + // use the basic disclosed vc + for(const [keyType, versions] of disclosed?.basic) { + componentCount.set(keyType, new Map()); + for(const [vcVersion, vc] of versions) { + const modifiedVc = structuredClone(vc); + const params = parseDisclosureProofValue({proof: modifiedVc.proof}); + // create a payload with only 2 components + const payload = [params.bbsProof, params.presentationHeader]; + // replace the existing proofValue with the smaller payload + modifiedVc.proof.proofValue = encodeProofValue({payload}); + componentCount.get(keyType).set(vcVersion, modifiedVc); + } + } return { base, disclosed diff --git a/tests/suites/create.js b/tests/suites/create.js index 5971f73..873e1c9 100644 --- a/tests/suites/create.js +++ b/tests/suites/create.js @@ -4,7 +4,6 @@ * SPDX-License-Identifier: BSD-3-Clause */ import { - baseProofShouldHaveElementCount, checkEncoding, checkHmacKeyLength, shouldBeMultibaseEncoded, @@ -266,23 +265,12 @@ export function createSuite({ } }); it('The transformation options MUST contain an array of mandatory ' + - 'JSON pointers (mandatoryPointers)', function() { + 'JSON pointers (mandatoryPointers).', function() { this.test.link = 'https://w3c.github.io/vc-di-bbs/#:~:text=The%20transformation%20options%20MUST%20contain%20an%20array%20of%20mandatory%20JSON%20pointers%20(mandatoryPointers)'; for(const proof of bbsProofs) { shouldHaveMandatoryPointers({proof}); } }); - it('Initialize components to an array that is the result of ' + - 'CBOR-decoding the bytes that follow the three-byte BBS disclosure ' + - 'proof header. If the result is not an array of five or six elements ' + - '— a byte array, a map of integers to integers, two arrays of ' + - 'integers, and one or two byte arrays; an error MUST be raised and ' + - 'SHOULD convey an error type of PROOF_VERIFICATION_ERROR.', function() { - this.test.link = 'https://w3c.github.io/vc-di-bbs/#:~:text=%22pseudonym_hidden_pid%22.-,Initialize%20components%20to%20an%20array%20that%20is%20the%20result%20of%20CBOR,be%20raised%20and%20SHOULD%20convey%20an%20error%20type%20of%20PROOF_VERIFICATION_ERROR.,-Replace%20the%20second'; - for(const proof of bbsProofs) { - baseProofShouldHaveElementCount({proof}); - } - }); it.skip(' If featureOption is set to "anonymous_holder_binding" or ' + '"pseudonym_hidden_pid", the commitment_with_proof input MUST be ' + 'supplied.', async function() { diff --git a/tests/suites/verify.js b/tests/suites/verify.js index 5ced0da..11385e5 100644 --- a/tests/suites/verify.js +++ b/tests/suites/verify.js @@ -144,6 +144,18 @@ export function verifySuite({ const credential = cloneTestVector(disclosed?.invalid?.valuePrefix); await verificationFail({credential, verifier}); }); + it('If the result is not an array of five, six, or seven elements ' + + '— a byte array, a map of integers to integers, two arrays of ' + + 'integers, and one or two byte arrays — an error MUST be raised ' + + 'and SHOULD convey an error type of PROOF_VERIFICATION_ERROR.', + async function() { + this.test.link = 'https://w3c.github.io/vc-di-bbs/#:~:text=If%20the%20result%20is%20not%20an%20array%20of%20five%2C%20six%2C%20or%20seven%20elements%20%E2%80%94%20a%20byte%20array%2C%20a%20map%20of%20integers%20to%20integers%2C%20two%20arrays%20of%20integers%2C%20and%20one%20or%20two%20byte%20arrays%3B%20an%20error%20MUST%20be%20raised%20and%20SHOULD%20convey%20an%20error%20type%20of%20PROOF_VERIFICATION_ERROR'; + await verificationFail({ + credential: cloneTestVector(disclosed?.invalid?.componentCount), + verifier, + reason: 'Should not verify a disclosed VC w/ less than 4 components' + }); + }); }); } }); diff --git a/tests/vc-generator/stubMethods.js b/tests/vc-generator/stubMethods.js index 5fb1ab6..6b2c359 100644 --- a/tests/vc-generator/stubMethods.js +++ b/tests/vc-generator/stubMethods.js @@ -392,6 +392,10 @@ function serializeDisclosureProofValue({ // Uint8Array presentationHeader ]; + return encodeProofValue({payload, typeEncoders}); +} + +export function encodeProofValue({payload, typeEncoders}) { const cbor = concatBuffers([ CBOR_PREFIX_DERIVED, cborg.encode(payload, {useMaps: true, typeEncoders}) ]); @@ -435,3 +439,58 @@ async function _findProof({proofId, proofSet, dataIntegrityProof}) { export function stringToUtf8Bytes({str, utfOffset = 0}) { return TEXT_ENCODER.encode(str).map(b => b + utfOffset); } + +export function parseDisclosureProofValue({proof} = {}) { + try { + if(typeof proof?.proofValue !== 'string') { + throw new TypeError('"proof.proofValue" must be a string.'); + } + if(proof.proofValue[0] !== 'u') { + throw new Error('Only base64url multibase encoding is supported.'); + } + + // decode from base64url + const proofValue = base64url.decode(proof.proofValue.slice(1)); + if(!_startsWithBytes(proofValue, CBOR_PREFIX_DERIVED)) { + throw new TypeError('"proof.proofValue" must be a derived proof.'); + } + + const payload = proofValue.subarray(CBOR_PREFIX_DERIVED.length); + const [ + bbsProof, + compressedLabelMap, + mandatoryIndexes, + selectiveIndexes, + presentationHeader + ] = cborg.decode(payload, {useMaps: true, tags: TAGS}); + + const labelMap = _decompressLabelMap(compressedLabelMap); + const params = { + bbsProof, labelMap, mandatoryIndexes, selectiveIndexes, + presentationHeader + }; + return params; + } catch(e) { + const err = new TypeError( + 'The proof does not include a valid "proofValue" property.'); + err.cause = e; + throw err; + } +} + +function _decompressLabelMap(compressedLabelMap) { + const map = new Map(); + for(const [k, v] of compressedLabelMap.entries()) { + map.set(`c14n${k}`, `b${v}`); + } + return map; +} + +function _startsWithBytes(buffer, prefix) { + for(let i = 0; i < prefix.length; ++i) { + if(buffer[i] !== prefix[i]) { + return false; + } + } + return true; +}