diff --git a/package.json b/package.json index 6f196ab..dc98984 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@digitalbazaar/http-client": "^4.0.0", "@digitalbazaar/mocha-w3c-interop-reporter": "^1.6.0", "@digitalbazaar/multikey-context": "^2.0.1", + "@digitalbazaar/security-document-loader": "^3.0.0", "@digitalbazaar/vc": "^7.0.0", "@digitalcredentials/did-context": "^1.0.0", "base58-universal": "^2.0.0", diff --git a/tests/75-proof-chains.js b/tests/75-proof-chains.js new file mode 100644 index 0000000..812186f --- /dev/null +++ b/tests/75-proof-chains.js @@ -0,0 +1,62 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import { + addProof, + createVc, +} from './vc-issuer/index.js'; +import { + setupReportableTestSuite, + setupRow, +} from './helpers.js'; +import {endpoints} from 'vc-test-suite-implementations'; + +const cryptosuites = [ + 'ecdsa-rdfc-2019', +]; + +const {match: issuers} = endpoints.filterByTag({ + tags: cryptosuites, + property: 'issuers' +}); + +const {match: verifiers} = endpoints.filterByTag({ + tags: cryptosuites, + property: 'verifiers' +}); + +describe('Proof Chains', function() { + setupReportableTestSuite(this); + for(const [columnId, {endpoints}] of verifiers) { + describe(columnId, function() { + const [verifier] = endpoints; + let issuedCredential; + let issuedProofSet; + let issuedProofChain; + before(async function() { + // signedCredential = await addProof( + // generateCredential()); + issuedCredential = await createVc(); + issuedProofSet = await addProof( + structuredClone(issuedCredential)); + issuedProofChain = await addProof( + structuredClone(issuedProofSet), issuedCredential.proof[0].id); + }); + beforeEach(setupRow); + it('If a proof with id value equal to the value of previousProof ' + + 'does not exist in allProofs, an error MUST be raised and SHOULD ' + + 'convey an error type of PROOF_VERIFICATION_ERROR.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-data-integrity/#verify-proof-sets-and-chains'; + }); + it('If any element of previousProof list has an id attribute ' + + 'value that does not match the id attribute value of any ' + + 'element of allProofs, an error MUST be raised and SHOULD ' + + 'convey an error type of PROOF_VERIFICATION_ERROR.', + async function() { + this.test.link = 'https://www.w3.org/TR/vc-data-integrity/#verify-proof-sets-and-chains'; + }); + }); + } +}); diff --git a/tests/vc-issuer/documentLoader.js b/tests/vc-issuer/documentLoader.js new file mode 100644 index 0000000..417afc8 --- /dev/null +++ b/tests/vc-issuer/documentLoader.js @@ -0,0 +1,24 @@ +/*! + * Copyright (c) 2023-2024 Digital Bazaar, Inc. All rights reserved. + */ +import dataIntegrityContext from '@digitalbazaar/data-integrity-context'; +import multikeyContext from '@digitalbazaar/multikey-context'; +import {named} from '@digitalbazaar/credentials-context'; +import {securityLoader} from '@digitalbazaar/security-document-loader'; + +export const loader = securityLoader(); + +loader.addStatic( + named.get('v2').id, + named.get('v2').context +); + +loader.addStatic( + dataIntegrityContext.constants.CONTEXT_URL, + dataIntegrityContext.contexts.get(dataIntegrityContext.constants.CONTEXT_URL) +); + +loader.addStatic( + multikeyContext.constants.CONTEXT_URL, + multikeyContext.contexts.get(multikeyContext.constants.CONTEXT_URL) +); diff --git a/tests/vc-issuer/index.js b/tests/vc-issuer/index.js new file mode 100644 index 0000000..d4a0525 --- /dev/null +++ b/tests/vc-issuer/index.js @@ -0,0 +1,124 @@ +/*! + * Copyright 2024 Digital Bazaar, Inc. + * SPDX-License-Identifier: BSD-3-Clause + */ +import * as base58 from 'base58-universal'; +import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey'; +import * as rdfCanonize from 'rdf-canonize'; +import crypto from 'crypto'; +// import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +// import {cryptosuite as ecdsaRdfc2019Cryptosuite} from +// '@digitalbazaar/ecdsa-rdfc-2019-cryptosuite'; +// import jsigs from 'jsonld-signatures'; +import jsonld from 'jsonld'; +// const {purposes: {AssertionProofPurpose}} = jsigs; +import {loader} from './documentLoader.js'; + +const documentLoader = loader.build(); +const publicKeyMultibase = 'zDnaekGZTbQBerwcehBSXLqAg6s55hVEBms1zFy89VHXtJSa9'; +const secretKeyMultibase = 'z42tqZ5smVag3DtDhjY9YfVwTMyVHW6SCHJi2ZMrD23DGYS3'; +const controller = `did:key:${publicKeyMultibase}`; + +function generateProofId() { + return `urn:uuid:${crypto.randomUUID()}`; +} + +const dataIntegrityProof = { + type: 'DataIntegrityProof', + cryptosuite: 'ecdsa-rdfc-2019', + proofPurpose: 'assertionMethod', + verificationMethod: `${controller}#${publicKeyMultibase}`, +}; + +// create the keypair to use when signing +const keyPair = await EcdsaMultikey.from({ + '@context': 'https://w3id.org/security/multikey/v1', + id: `${controller}#${publicKeyMultibase}`, + type: 'Multikey', + controller, + publicKeyMultibase, + secretKeyMultibase +}); + +// // create suite +// const suite = new DataIntegrityProof({ +// signer: keyPair.signer(), cryptosuite: ecdsaRdfc2019Cryptosuite +// }); + +// create the unsigned credential +const unsignedCredential = { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + issuer: controller, + credentialSubject: {name: 'Alice'} +}; + +export async function createVc() { + return addProof(unsignedCredential); +} + +export async function addProof(credential, previousProof = null) { + // const unsecuredDocument = structuredClone(unsignedCredential); + // const signedDocument = await jsigs.sign(credential, { + // suite, + // purpose: new AssertionProofPurpose(), + // documentLoader + // }); + const proofSet = credential?.proof || []; + const unsecuredDocument = structuredClone(credential); + delete unsecuredDocument.proof; + const proofOptions = structuredClone(dataIntegrityProof); + if(previousProof) { + // const allProofs = []; + const matchingProofs = proofSet.filter(entry => entry.id === previousProof); + unsecuredDocument.proof = matchingProofs; + proofOptions.previousProof = previousProof; + } + + const securedDocument = structuredClone(unsecuredDocument); + const proof = await createProof(unsecuredDocument, proofOptions); + proofSet.push(proof); + securedDocument.proof = proofSet; + + return securedDocument; +} + +export async function createProof(unsecuredDocument, options) { + // https://www.w3.org/TR/vc-di-ecdsa/#create-proof-ecdsa-rdfc-2019 + options.id = generateProofId(); + const proof = structuredClone(options); + + options['@context'] = unsecuredDocument['@context']; + + const proofConfig = await canonize(options); + const proofConfigHash = + crypto.createHash('sha256').update(proofConfig).digest(); + + const transformedData = await canonize(unsecuredDocument); + const transformedDataHash = + crypto.createHash('sha256').update(transformedData).digest(); + + const hashData = Buffer.concat([proofConfigHash, transformedDataHash]); + + const proofbytes = await keyPair.signer().sign({data: hashData}); + + proof.proofValue = `z${base58.encode(proofbytes)}`; + + return proof; +} + +async function canonize(input) { + const options = { + algorithm: 'RDFC-1.0', + base: null, + // format: 'application/n-quads', + documentLoader, + safe: true, + skipExpansion: false, + produceGeneralizedRdf: false, + rdfDirection: 'i18n-datatype', + messageDigestAlgorithm: 'SHA-256', + }; + const dataset = await jsonld.toRDF(input, options); + return rdfCanonize.canonize(dataset, options); +}