From 40655ee887e91bdda8fd499ba242c14e6452526d Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 17:33:38 -0400 Subject: [PATCH 1/8] Use latest `br-kms*` deps in tests. --- test/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/package.json b/test/package.json index bc21a570..3446ee44 100644 --- a/test/package.json +++ b/test/package.json @@ -21,8 +21,8 @@ "@bedrock/express": "^8.0.0", "@bedrock/https-agent": "^4.0.0", "@bedrock/jsonld-document-loader": "^4.0.0", - "@bedrock/kms": "^14.0.0", - "@bedrock/kms-http": "^19.0.0", + "@bedrock/kms": "^15.0.0", + "@bedrock/kms-http": "^20.0.0", "@bedrock/ledger-context": "^24.0.0", "@bedrock/meter": "^5.0.0", "@bedrock/meter-http": "^12.0.0", From 046cc3e8622abfb6f92796a23e3580416493fa97 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 19:01:08 -0400 Subject: [PATCH 2/8] Add missing `@digitalbazaar/ezcap` dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7e0f102b..f3428a3c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@digitalbazaar/ed25519-signature-2020": "^5.0.0", "@digitalbazaar/eddsa-2022-cryptosuite": "^1.0.0", "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.0.1", + "@digitalbazaar/ezcap": "^4.1.0", "@digitalbazaar/lru-memoize": "^3.0.0", "@digitalbazaar/vc": "github:digitalbazaar/vc#vc-2.0-time-props", "@digitalbazaar/webkms-client": "^14.1.0", From d5a4e93344b36eeedb6b64112be31d1bbfb39ba7 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 20:07:10 -0400 Subject: [PATCH 3/8] Use `@bedrock/did-io@10.3.1`. --- package.json | 2 +- test/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f3428a3c..e33024c0 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@bedrock/credentials-context": "digitalbazaar/bedrock-credentials-context#update-vc-2.0", "@bedrock/data-integrity-context": "^3.0.0", "@bedrock/did-context": "^5.0.0", - "@bedrock/did-io": "^10.1.0", + "@bedrock/did-io": "^10.3.1", "@bedrock/express": "^8.0.0", "@bedrock/https-agent": "^4.0.0", "@bedrock/jsonld-document-loader": "^4.0.0", diff --git a/test/package.json b/test/package.json index 3446ee44..593ea274 100644 --- a/test/package.json +++ b/test/package.json @@ -16,7 +16,7 @@ "@bedrock/credentials-context": "digitalbazaar/bedrock-credentials-context#update-vc-2.0", "@bedrock/data-integrity-context": "^3.0.0", "@bedrock/did-context": "^5.0.0", - "@bedrock/did-io": "^10.1.0", + "@bedrock/did-io": "^10.3.1", "@bedrock/edv-storage": "^18.0.0", "@bedrock/express": "^8.0.0", "@bedrock/https-agent": "^4.0.0", From a7861c7faf736ce79997512d09209a00db339981 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 21:58:49 -0400 Subject: [PATCH 4/8] Add `issuer` and `cryptosuites` issuer options. --- CHANGELOG.md | 5 ++ lib/constants.js | 3 ++ lib/helpers.js | 96 +++++++++++++++++++++++++----------- lib/index.js | 18 +++++-- lib/issuer.js | 9 ++-- lib/suites.js | 52 ++++++++++--------- schemas/bedrock-vc-issuer.js | 56 ++++++++++++++++++++- 7 files changed, 176 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd0e6b..7230130e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ list configuration options. - Allow passing `credentialId` when issuing a credential without an `id` to allow referencing it later. +- Allow passing `issuer` and `cryptosuites` instead of `suiteName` to provide + `issuer` (to eliminate the need for the instance to retrieve it during + issuance), to provide additional cryptosuite-specific options, and to allow + the use of multiple cryptosuites when issuing (generating a proof set + instead of a single proof on a credential). ### Changed - **BREAKING**: Management of status list index allocation has been rewritten diff --git a/lib/constants.js b/lib/constants.js index 0f6419c3..f7fdd0f6 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -8,6 +8,9 @@ export const DEFAULT_BLOCK_SIZE = 32; // divided by default list size = 2^32/2^17 = 2^15 = 32768 export const DEFAULT_TERSE_LIST_COUNT = 32768; +// maximum number of cryptosuites to be used in a proof set +export const MAX_CRYPTOSUITE_OPTIONS = 10; + // max list size is DEFAULT_BLOCK_COUNT * DEFAULT_BLOCK_SIZE = 131072 export const MAX_LIST_SIZE = DEFAULT_BLOCK_COUNT * DEFAULT_BLOCK_SIZE; diff --git a/lib/helpers.js b/lib/helpers.js index 012a2f3c..6a3b3b1e 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -52,50 +52,86 @@ export async function getDocumentStore({config}) { return documentStore; } -export async function getIssuerAndSuite({ - config, suiteName = config.issueOptions.suiteName, options -}) { - // get suite params for issuing a VC - const {createSuite, referenceId} = getSuiteParams({config, suiteName}); +export async function getIssuerAndSuites({config, options}) { + // get each suite's params for issuing a VC + let issuer; + let params; + let legacy = false; + if(config.issueOptions.suiteName) { + // legacy mode + legacy = true; + params = [ + getSuiteParams({config, suiteName: config.issueOptions.suiteName}) + ]; + } else { + // modern + ({issuer} = config.issuerOptions.issuer); + params = config.issueOptions.cryptosuites.map( + cryptosuite => getSuiteParams({config, cryptosuite})); + } - // get assertion method key to use for signing VCs + // get assertion method key to use with each suite and ensure suites are + // created in deterministic order by attaching suite instances to `params` const {serviceAgent} = await serviceAgents.get({serviceType}); const { capabilityAgent, zcaps } = await serviceAgents.getEphemeralAgent({config, serviceAgent}); const invocationSigner = capabilityAgent.getSigner(); - const zcap = zcaps[referenceId]; const kmsClient = new KmsClient({httpsAgent}); - const assertionMethodKey = await AsymmetricKey.fromCapability( - {capability: zcap, invocationSigner, kmsClient}); - - // get `issuer` ID by getting key's public controller - let issuer; - try { - const {controller} = await didIo.get({url: assertionMethodKey.id}); - issuer = controller; - } catch(cause) { - throw new BedrockError( - 'Unable to determine credential issuer.', { - name: 'AbortError', - details: { - httpStatusCode: 400, - public: true - }, - cause + await Promise.all(params.map(async p => { + const zcap = zcaps[p.referenceId]; + try { + p.assertionMethodKey = await AsymmetricKey.fromCapability({ + capability: zcap, invocationSigner, kmsClient + }); + p.suite = await p.createSuite({ + signer: p.assertionMethodKey, config, options }); + } catch(cause) { + throw new BedrockError( + 'Unable to create cryptosuite suite for issuance.', { + name: 'AbortError', + details: { + httpStatusCode: 500, + public: true + }, + cause + }); + } + })); + + if(legacy) { + // in legacy mode, get `issuer` ID by getting key's public controller + try { + const [{assertionMethodKey}] = params; + const {controller} = await didIo.get({url: assertionMethodKey.id}); + issuer = controller; + } catch(cause) { + throw new BedrockError( + 'Unable to determine credential issuer.', { + name: 'AbortError', + details: { + httpStatusCode: 500, + public: true + }, + cause + }); + } } - const suite = await createSuite({ - signer: assertionMethodKey, config, options - }); - return {issuer, suite}; + + const suites = params.map(({suite}) => suite); + return {issuer, suites}; } // helpers must export this function and not `issuer` to prevent circular // dependencies via `CredentialStatusWriter`, `ListManager` and `issuer` -export async function issue({credential, documentLoader, suite}) { +export async function issue({credential, documentLoader, suites}) { // vc-js.issue may be fixed to not mutate credential // see: https://github.com/digitalbazaar/vc-js/issues/76 credential = {...credential}; - return vc.issue({credential, documentLoader, suite}); + // issue using each suite + for(const suite of suites) { + credential = await vc.issue({credential, documentLoader, suite}); + } + return credential; } diff --git a/lib/index.js b/lib/index.js index 0c017fe8..bd23b92f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,7 @@ import * as issuer from './issuer.js'; import {createService, schemas} from '@bedrock/service-core'; import { DEFAULT_BLOCK_COUNT, DEFAULT_BLOCK_SIZE, DEFAULT_TERSE_LIST_COUNT, - MAX_LIST_SIZE, MAX_STATUS_LIST_OPTIONS, serviceType + MAX_CRYPTOSUITE_OPTIONS, MAX_LIST_SIZE, MAX_STATUS_LIST_OPTIONS, serviceType } from './constants.js'; import { issueOptions, statusListOptions @@ -44,8 +44,9 @@ bedrock.events.on('bedrock.init', async () => { // note: `assertionMethod` not required for backwards compatibility // purposes (as it has used other reference IDs in the past) schema.properties.zcaps.required = ['edv', 'hmac', 'keyAgreementKey']; - // max of 4 required zcaps + 1 per status list option - schema.properties.zcaps.maxProperties = 4 + MAX_STATUS_LIST_OPTIONS; + // max of 3 required zcaps + max cryptosuites opts + max status lists opts + schema.properties.zcaps.maxProperties = + 3 + MAX_CRYPTOSUITE_OPTIONS + MAX_STATUS_LIST_OPTIONS; schema.properties.zcaps.additionalProperties = schemas.delegatedZcap; } @@ -103,8 +104,15 @@ async function validateConfigFn({config, op, existingConfig} = {}) { } } - // ensure suite parameters can be retrieved for configured `issueOptions` - getSuiteParams({config, suiteName: issueOptions.suiteName}); + if(issueOptions.suiteName) { + // ensure suite parameters can be retrieved for configured `issueOptions` + getSuiteParams({config, suiteName: issueOptions.suiteName}); + } else { + // ensure every suite's params can be retrieved + for(const cryptosuite of issueOptions.cryptosuites) { + getSuiteParams({config, cryptosuite}); + } + } // validate `statusListOptions`... for(const statusConfig of statusListOptions) { diff --git a/lib/issuer.js b/lib/issuer.js index 85ade6f5..8477aaed 100644 --- a/lib/issuer.js +++ b/lib/issuer.js @@ -4,7 +4,7 @@ import * as bedrock from '@bedrock/core'; import { issue as _issue, - getDocumentStore, getIssuerAndSuite + getDocumentStore, getIssuerAndSuites } from './helpers.js'; import assert from 'assert-plus'; import {createDocumentLoader} from './documentLoader.js'; @@ -25,13 +25,12 @@ export async function issue({credential, config, options = {}} = {}) { // see if config indicates a credential status should be set const {statusListOptions = []} = config; - const {suiteName} = config.issueOptions; - const [documentLoader, documentStore, {issuer, suite}] = await Promise.all([ + const [documentLoader, documentStore, {issuer, suites}] = await Promise.all([ createDocumentLoader({config}), // only fetch `documentStore` if a status list is configured; otherwise, // it is not needed statusListOptions.length > 0 ? getDocumentStore({config}) : {}, - getIssuerAndSuite({config, suiteName, options}) + getIssuerAndSuites({config, options}) ]); if(typeof credential.issuer === 'object') { @@ -60,7 +59,7 @@ export async function issue({credential, config, options = {}} = {}) { const credentialStatus = await credentialStatusIssuer?.issue(); // issue VC - verifiableCredential = await _issue({credential, documentLoader, suite}); + verifiableCredential = await _issue({credential, documentLoader, suites}); // if no credential status written, do not store VC; note that this means // that VC IDs will not be checked for duplicates, this will be the diff --git a/lib/suites.js b/lib/suites.js index 0ed0ff78..6ce10232 100644 --- a/lib/suites.js +++ b/lib/suites.js @@ -61,39 +61,47 @@ const SUPPORTED_SUITES = new Map([ ]); const {util: {BedrockError}} = bedrock; -export function getSuiteParams({config, suiteName}) { +export function getSuiteParams({config, suiteName, cryptosuite}) { + // get zcap to use to invoke assertion method key + let zcap; + let referenceId; + if(cryptosuite) { + suiteName = cryptosuite.name; + referenceId = cryptosuite.zcapReferenceIds.assertionMethod; + } else { + referenceId = 'assertionMethod'; + zcap = config.zcaps[referenceId]; + if(!zcap) { + // older reference ID formats to check for backwards compatibility + const olderReferenceIdFormats = [ + 'assertionMethod:Ed25519', + 'assertionMethod:ed25519', + 'assertionMethod:P-256' + ]; + for(const referenceIdFormat of olderReferenceIdFormats) { + if(config.zcaps[referenceIdFormat]) { + referenceId = referenceIdFormat; + zcap = config.zcaps[referenceId]; + // break if a valid zcap is found + break; + } + } + } + } + // ensure suite is supported const suiteInfo = SUPPORTED_SUITES.get(suiteName); if(!suiteInfo) { throw new Error(`Unsupported suite "${suiteName}".`); } - // get zcap to use to invoke assertion method key - const {createSuite} = suiteInfo; - let referenceId = 'assertionMethod'; - let zcap = config.zcaps[referenceId]; - - // older reference ID formats to check for backwards compatibility - const olderReferenceIdFormats = [ - 'assertionMethod:Ed25519', - 'assertionMethod:ed25519', - 'assertionMethod:P-256' - ]; - if(!zcap) { - for(const referenceIdFormat of olderReferenceIdFormats) { - if(config.zcaps[referenceIdFormat]) { - referenceId = referenceIdFormat; - zcap = config.zcaps[referenceId]; - break; // exit if a valid zcap is found - } - } - } - + // ensure zcap for assertion method is available if(!zcap) { throw new Error( `No capability available to sign using suite "${suiteName}".`); } + const {createSuite} = suiteInfo; return {zcap, createSuite, referenceId}; } diff --git a/schemas/bedrock-vc-issuer.js b/schemas/bedrock-vc-issuer.js index 69d3cdb2..b4231ac7 100644 --- a/schemas/bedrock-vc-issuer.js +++ b/schemas/bedrock-vc-issuer.js @@ -15,12 +15,66 @@ const context = { } }; +const cryptosuite = { + title: 'Cryptosuite Options', + type: 'object', + required: ['name', 'zcapReferenceIds'], + additionalProperties: false, + properties: { + name: { + type: 'string', + // supported default suites in this version + enum: [ + 'ecdsa-rdfc-2019', 'eddsa-rdfc-2022', 'Ed25519Signature2020', + 'Ed25519Signature2018', 'ecdsa-sd-2023', 'ecdsa-xi-2023', + 'bbs-2023' + ] + }, + zcapReferenceIds: { + type: 'object', + required: ['assertionMethod'], + additionalProperties: false, + properties: { + assertionMethod: { + type: 'string' + } + } + } + } +}; + +const cryptosuites = { + title: 'Cryptosuites', + type: 'array', + additionalItems: false, + minItems: 1, + items: cryptosuite +}; + export const issueOptions = { title: 'Issue Options', type: 'object', - required: ['suiteName'], + oneOf: [{ + // preferred mechanism for specifying issuer and cryptosuites to sign with + required: ['issuer', 'cryptosuites'], + not: { + required: ['suiteName'] + } + }, { + // legacy; for backwards compatibility only + required: ['suiteName'], + not: { + required: ['issuer', 'cryptosuites'] + } + }], additionalProperties: false, properties: { + // modern + issuer: { + type: 'string' + }, + cryptosuites, + // legacy suiteName: { type: 'string', // supported default suites in this version From 2ca4eb172a791d1ae3a30872ab94c8f1bf62de32 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 21:59:31 -0400 Subject: [PATCH 5/8] Add ability to add `did:web` DID docs in tests. --- test/mocha/mock.data.js | 2 ++ test/test.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/test/mocha/mock.data.js b/test/mocha/mock.data.js index be481f71..6906053b 100644 --- a/test/mocha/mock.data.js +++ b/test/mocha/mock.data.js @@ -5,6 +5,8 @@ import {config} from '@bedrock/core'; export const mockData = {}; +mockData.didWebDocuments = new Map(); + // mock product IDs and reverse lookup for service products mockData.productIdMap = new Map([ // edv service diff --git a/test/test.js b/test/test.js index a9ab595d..cc95618e 100644 --- a/test/test.js +++ b/test/test.js @@ -17,6 +17,8 @@ import '@bedrock/vc-status'; import {mockData} from './mocha/mock.data.js'; +const {util: {BedrockError}} = bedrock; + bedrock.events.on('bedrock.init', async () => { /* Handlers need to be added before `bedrock.start` is called. These are no-op handlers to enable meter usage without restriction */ @@ -48,5 +50,23 @@ bedrock.events.on('bedrock-express.configure.routes', app => { }); }); +// mock DID web server routes +bedrock.events.on('bedrock-express.configure.routes', app => { + app.get('/did-web/:localId/did.json', (req, res) => { + const {localId} = req.params; + const didDocument = mockData.didWebDocuments.get(localId); + if(!didDocument) { + throw new BedrockError('DID document not found.', { + name: 'NotFoundError', + details: { + httpStatusCode: 404, + public: true + } + }); + } + res.json(didDocument); + }); +}); + import '@bedrock/test'; bedrock.start(); From 999848663bf9c8b0646f1acba5b53f70e09ec077 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 22:51:19 -0400 Subject: [PATCH 6/8] Fix typo and ensure assertion method zcap is retrieved. --- lib/helpers.js | 2 +- lib/suites.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index 6a3b3b1e..c2034f59 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -65,7 +65,7 @@ export async function getIssuerAndSuites({config, options}) { ]; } else { // modern - ({issuer} = config.issuerOptions.issuer); + ({issuer} = config.issueOptions); params = config.issueOptions.cryptosuites.map( cryptosuite => getSuiteParams({config, cryptosuite})); } diff --git a/lib/suites.js b/lib/suites.js index 6ce10232..e5fc6165 100644 --- a/lib/suites.js +++ b/lib/suites.js @@ -68,6 +68,7 @@ export function getSuiteParams({config, suiteName, cryptosuite}) { if(cryptosuite) { suiteName = cryptosuite.name; referenceId = cryptosuite.zcapReferenceIds.assertionMethod; + zcap = config.zcaps[referenceId]; } else { referenceId = 'assertionMethod'; zcap = config.zcaps[referenceId]; From dafc38cf7d657413f72df6d00e9dc9023a5bfd49 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Apr 2024 22:51:38 -0400 Subject: [PATCH 7/8] Add `did:web` issuance tests with proof sets. --- test/mocha/40-did-web-issuer.js | 247 ++++++++++++++++++++++++++++++++ test/mocha/helpers.js | 13 +- 2 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 test/mocha/40-did-web-issuer.js diff --git a/test/mocha/40-did-web-issuer.js b/test/mocha/40-did-web-issuer.js new file mode 100644 index 00000000..71384896 --- /dev/null +++ b/test/mocha/40-did-web-issuer.js @@ -0,0 +1,247 @@ +/*! + * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. + */ +import * as bedrock from '@bedrock/core'; +import * as helpers from './helpers.js'; +import {agent} from '@bedrock/https-agent'; +import {createRequire} from 'node:module'; +import {httpClient} from '@digitalbazaar/http-client'; +import {klona} from 'klona'; +import {mockData} from './mock.data.js'; +import {v4 as uuid} from 'uuid'; + +const require = createRequire(import.meta.url); + +const {baseUrl} = mockData; +const serviceType = 'vc-issuer'; + +// NOTE: using embedded context in mockCredential: +// https://www.w3.org/2018/credentials/examples/v1 +const mockCredential = require('./mock-credential.json'); +const mockCredentialV2 = require('./mock-credential-v2.json'); + +describe('issue using "did:web" issuer', () => { + let suites; + let capabilityAgent; + let noStatusListIssuerId; + let noStatusListIssuerRootZcap; + beforeEach(async () => { + // generate a proof set using all of these suites in each test + suites = [{ + name: 'Ed25519Signature2020', + algorithm: 'Ed25519' + }, { + name: 'eddsa-rdfc-2022', + algorithm: 'Ed25519' + }, { + name: 'ecdsa-rdfc-2019', + algorithm: 'P-256' + }, { + name: 'ecdsa-sd-2023', + algorithm: 'P-256' + }, { + name: 'ecdsa-xi-2023', + algorithm: 'P-256' + }, { + name: 'bbs-2023', + algorithm: 'Bls12381G2' + }]; + + // provision dependencies + ({capabilityAgent} = await helpers.provisionDependencies({status: false})); + + // create keystore for capability agent + const keystoreAgent = await helpers.createKeystoreAgent({capabilityAgent}); + + // generate a `did:web` DID for the issuer + const {host} = bedrock.config.server; + const localId = uuid(); + const did = `did:web:${encodeURIComponent(host)}:did-web:${localId}`; + + // generate an assertion method key for each suite to use + for(const suite of suites) { + const {algorithm} = suite; + let assertionMethodKey; + const publicAliasTemplate = `${did}#{publicKeyMultibase}`; + if(['P-256', 'P-384', 'Bls12381G2'].includes(algorithm)) { + assertionMethodKey = await helpers._generateMultikey({ + keystoreAgent, + type: `urn:webkms:multikey:${algorithm}`, + publicAliasTemplate + }); + } else { + assertionMethodKey = await keystoreAgent.generateKey({ + type: 'asymmetric', + publicAliasTemplate + }); + } + suite.assertionMethodKey = assertionMethodKey; + } + + // create EDV for storage (creating hmac and kak in the process) + const { + edvConfig, + hmac, + keyAgreementKey + } = await helpers.createEdv({capabilityAgent, keystoreAgent}); + + // get service agent to delegate to + const serviceAgentUrl = + `${baseUrl}/service-agents/${encodeURIComponent(serviceType)}`; + const {data: serviceAgent} = await httpClient.get(serviceAgentUrl, {agent}); + + // delegate edv, hmac, and key agreement key zcaps to service agent + const {id: edvId} = edvConfig; + const zcaps = {}; + zcaps.edv = await helpers.delegate({ + controller: serviceAgent.id, + delegator: capabilityAgent, + invocationTarget: edvId + }); + const {keystoreId} = keystoreAgent; + zcaps.hmac = await helpers.delegate({ + capability: `urn:zcap:root:${encodeURIComponent(keystoreId)}`, + controller: serviceAgent.id, + invocationTarget: hmac.id, + delegator: capabilityAgent + }); + zcaps.keyAgreementKey = await helpers.delegate({ + capability: `urn:zcap:root:${encodeURIComponent(keystoreId)}`, + controller: serviceAgent.id, + invocationTarget: keyAgreementKey.kmsId, + delegator: capabilityAgent + }); + + // delegate assertion method keys + for(const suite of suites) { + const {assertionMethodKey} = suite; + const zcap = await helpers.delegate({ + capability: `urn:zcap:root:${encodeURIComponent(keystoreId)}`, + controller: serviceAgent.id, + invocationTarget: assertionMethodKey.kmsId, + delegator: capabilityAgent + }); + suite.zcapReferenceIds = { + assertionMethod: uuid() + }; + zcaps[suite.zcapReferenceIds.assertionMethod] = zcap; + } + + // create issuer options + const issuerOptions = { + issuer: did, + cryptosuites: suites.map( + ({name, zcapReferenceIds}) => ({name, zcapReferenceIds})) + }; + + // create `did:web` DID document for issuer + const didDocument = { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + 'https://w3id.org/security/multikey/v1' + ], + id: did, + verificationMethod: [], + assertionMethod: [] + }; + for(const {assertionMethodKey} of suites) { + const description = await assertionMethodKey.getKeyDescription(); + delete description['@context']; + didDocument.verificationMethod.push(description); + didDocument.assertionMethod.push(description.id); + } + // add DID doc to map with DID docs to be served + mockData.didWebDocuments.set(localId, didDocument); + + // create issuer instance w/ no status list options + const noStatusListIssuerConfig = await helpers.createIssuerConfig({ + capabilityAgent, zcaps, issuerOptions + }); + noStatusListIssuerId = noStatusListIssuerConfig.id; + noStatusListIssuerRootZcap = + `urn:zcap:root:${encodeURIComponent(noStatusListIssuerId)}`; + }); + describe('/credentials/issue', () => { + it('issues a VC 1.1 credential with a proof set', async () => { + const credential = klona(mockCredential); + let error; + let result; + try { + const zcapClient = helpers.createZcapClient({capabilityAgent}); + result = await zcapClient.write({ + url: `${noStatusListIssuerId}/credentials/issue`, + capability: noStatusListIssuerRootZcap, + json: { + credential, + options: { + extraInformation: 'abc' + } + } + }); + } catch(e) { + error = e; + } + assertNoError(error); + should.exist(result.data); + should.exist(result.data.verifiableCredential); + const {verifiableCredential} = result.data; + verifiableCredential.should.be.an('object'); + should.exist(verifiableCredential['@context']); + should.exist(verifiableCredential.id); + should.exist(verifiableCredential.type); + should.exist(verifiableCredential.issuer); + should.exist(verifiableCredential.issuanceDate); + should.exist(verifiableCredential.credentialSubject); + verifiableCredential.credentialSubject.should.be.an('object'); + should.not.exist(verifiableCredential.credentialStatus); + should.exist(verifiableCredential.proof); + verifiableCredential.proof.should.be.an('array'); + verifiableCredential.proof.length.should.equal(suites.length); + const parsedCryptosuites = verifiableCredential.proof.map( + ({type, cryptosuite}) => cryptosuite ?? type); + const expectedCryptosuites = suites.map(({name}) => name); + parsedCryptosuites.should.deep.equal(expectedCryptosuites); + }); + it('issues a VC 2.0 credential with a proof set', async () => { + const credential = klona(mockCredentialV2); + let error; + let result; + try { + const zcapClient = helpers.createZcapClient({capabilityAgent}); + result = await zcapClient.write({ + url: `${noStatusListIssuerId}/credentials/issue`, + capability: noStatusListIssuerRootZcap, + json: { + credential, + options: { + extraInformation: 'abc', + mandatoryPointers: ['issuer'] + } + } + }); + } catch(e) { + error = e; + } + assertNoError(error); + should.exist(result.data); + should.exist(result.data.verifiableCredential); + const {verifiableCredential} = result.data; + verifiableCredential.should.be.an('object'); + should.exist(verifiableCredential['@context']); + should.exist(verifiableCredential.id); + should.exist(verifiableCredential.type); + should.exist(verifiableCredential.issuer); + should.exist(verifiableCredential.credentialSubject); + verifiableCredential.credentialSubject.should.be.an('object'); + should.not.exist(verifiableCredential.credentialStatus); + should.exist(verifiableCredential.proof); + verifiableCredential.proof.should.be.an('array'); + verifiableCredential.proof.length.should.equal(suites.length); + const parsedCryptosuites = verifiableCredential.proof.map( + ({type, cryptosuite}) => cryptosuite ?? type); + const expectedCryptosuites = suites.map(({name}) => name); + parsedCryptosuites.should.deep.equal(expectedCryptosuites); + }); + }); +}); diff --git a/test/mocha/helpers.js b/test/mocha/helpers.js index 52c974f0..01c53d20 100644 --- a/test/mocha/helpers.js +++ b/test/mocha/helpers.js @@ -340,10 +340,13 @@ async function keyResolver({id}) { return data; } -export async function provisionDependencies({suiteOptions}) { +export async function provisionDependencies({suiteOptions, status = true}) { const secret = '53ad64ce-8e1d-11ec-bb12-10bf48838a41'; const handle = 'test'; const capabilityAgent = await CapabilityAgent.fromSecret({secret, handle}); + if(!status) { + return {capabilityAgent}; + } // create keystore for capability agent const keystoreAgent = await createKeystoreAgent({capabilityAgent}); @@ -351,13 +354,9 @@ export async function provisionDependencies({suiteOptions}) { const { statusConfig, issuerCreateStatusListZcap - } = await provisionStatus({ - capabilityAgent, keystoreAgent, suiteOptions - }); + } = await provisionStatus({capabilityAgent, keystoreAgent, suiteOptions}); - return { - statusConfig, issuerCreateStatusListZcap, capabilityAgent - }; + return {statusConfig, issuerCreateStatusListZcap, capabilityAgent}; } export async function provisionIssuerForStatus({ From f744d6668eb8d0f2ac56e4fe1ea143c0078304f8 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Mon, 15 Apr 2024 13:50:29 -0400 Subject: [PATCH 8/8] Move `zcaps` initialization inside `beforeEach` in tests. --- test/mocha/20-credentials.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mocha/20-credentials.js b/test/mocha/20-credentials.js index 981c36f5..776ac0a3 100644 --- a/test/mocha/20-credentials.js +++ b/test/mocha/20-credentials.js @@ -131,7 +131,6 @@ describe('issue APIs', () => { let terseMultistatusStatusId; let terseMultistatusStatusRootZcap; let oauth2IssuerConfig; - const zcaps = {}; beforeEach(async () => { // provision dependencies ({capabilityAgent} = await helpers.provisionDependencies(depOptions)); @@ -171,6 +170,7 @@ describe('issue APIs', () => { // delegate edv, hmac, and key agreement key zcaps to service agent const {id: edvId} = edvConfig; + const zcaps = {}; zcaps.edv = await helpers.delegate({ controller: serviceAgent.id, delegator: capabilityAgent,