Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add envelope support (w/ VC-JWT to start). #152

Merged
merged 13 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
`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).
instead of a single proof on a credential). Each cryptosuite can also have
`options` passed and doing so will additionally prevent clients from overriding
them (e.g., `options.mandatoryPointers`).

### Changed
- **BREAKING**: Management of status list index allocation has been rewritten
Expand Down
61 changes: 61 additions & 0 deletions lib/envelopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*!
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import {envelopeCredential} from './vcjwt.js';

const SUPPORTED_FORMATS = new Map([
['VC-JWT', {
createEnveloper: _createVCJWTEnveloper
}]
]);

const {util: {BedrockError}} = bedrock;

export function getEnvelopeParams({config, envelope}) {
const {format, zcapReferenceIds} = envelope;

// get zcap to use to invoke assertion method key
const referenceId = zcapReferenceIds.assertionMethod;
const zcap = config.zcaps[referenceId];

// ensure envelope is supported
const envelopeInfo = SUPPORTED_FORMATS.get(format);
if(!envelopeInfo) {
throw new BedrockError(`Unsupported envelope format "${format}".`, {
name: 'NotSupportedError',
details: {
httpStatusCode: 500,
public: true
}
});
}

// ensure zcap for assertion method is available
if(!zcap) {
throw new BedrockError(
`No capability available to sign using envelope format "${format}".`, {
name: 'DataError',
details: {
httpStatusCode: 500,
public: true
}
});
}

const {createEnveloper} = envelopeInfo;
return {zcap, createEnveloper, referenceId, envelope};
}

function _createVCJWTEnveloper({signer, options} = {}) {
return {
async envelope({verifiableCredential}) {
return {
data: await envelopeCredential({
verifiableCredential, signer, options
}),
mediaType: 'application/jwt'
};
}
};
}
65 changes: 23 additions & 42 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import * as vc from '@digitalbazaar/vc';
import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client';
import {didIo} from '@bedrock/did-io';
import {documentStores} from '@bedrock/service-agent';
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
import {generateId} from 'bnid';
import {getEnvelopeParams} from './envelopes.js';
import {getSuiteParams} from './suites.js';
import {httpsAgent} from '@bedrock/https-agent';
import {logger} from './logger.js';
Expand Down Expand Up @@ -53,22 +53,27 @@ export async function getDocumentStore({config}) {
return documentStore;
}

export async function getIssuerAndSuites({config, options}) {
export async function getIssuerAndSecuringMethods({config, options}) {
// get each suite's params for issuing a VC
let issuer;
let params;
let legacy = false;
if(config.issueOptions.suiteName) {
const {issueOptions} = config;
if(issueOptions.suiteName) {
// legacy mode
legacy = true;
params = [
getSuiteParams({config, suiteName: config.issueOptions.suiteName})
getSuiteParams({config, suiteName: issueOptions.suiteName})
];
} else {
// modern
({issuer} = config.issueOptions);
params = config.issueOptions.cryptosuites.map(
cryptosuite => getSuiteParams({config, cryptosuite}));
({issuer} = issueOptions);
params = issueOptions.cryptosuites?.map(
cryptosuite => getSuiteParams({config, cryptosuite})) || [];
const {envelope} = issueOptions;
if(envelope) {
params.push(getEnvelopeParams({config, envelope}));
}
}

// get assertion method key to use with each suite and ensure suites are
Expand All @@ -79,14 +84,21 @@ export async function getIssuerAndSuites({config, options}) {
} = await serviceAgents.getEphemeralAgent({config, serviceAgent});
const invocationSigner = capabilityAgent.getSigner();
const kmsClient = new KmsClient({httpsAgent});
let enveloper;
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
p.suite = await p.createSuite?.({
signer: p.assertionMethodKey, config, options,
cryptosuiteConfig: p.cryptosuite
});
// only one enveloper possible
enveloper = p.enveloper = await p.createEnveloper?.({
signer: p.assertionMethodKey, config, options,
envelopeConfig: p.envelope
});
} catch(cause) {
const error = new BedrockError(
Expand Down Expand Up @@ -122,37 +134,6 @@ export async function getIssuerAndSuites({config, options}) {
}
}

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, suites}) {
try {
// vc-js.issue may be fixed to not mutate credential
// see: https://github.com/digitalbazaar/vc-js/issues/76
credential = {...credential};
// issue using each suite
for(const suite of suites) {
// update credential with latest proof(s)
credential = await vc.issue({credential, documentLoader, suite});
}
// return credential with a proof for each suite
return credential;
} catch(e) {
// throw 400 for JSON pointer related errors
if(e.name === 'TypeError' && e.message?.includes('JSON pointer')) {
throw new BedrockError(
e.message, {
name: 'DataError',
details: {
httpStatusCode: 400,
public: true
},
cause: e
});
}
throw e;
}
const suites = params.filter(({suite}) => !!suite).map(({suite}) => suite);
return {issuer, suites, enveloper};
}
10 changes: 8 additions & 2 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ export async function addRoutes({app, service} = {}) {
try {
const {config} = req.serviceObject;
const {credential, options} = req.body;
const verifiableCredential = await issue({credential, config, options});
res.status(201).json({verifiableCredential});
const {
verifiableCredential, envelopedVerifiableCredential
} = await issue({credential, config, options});
const body = {
verifiableCredential:
envelopedVerifiableCredential ?? verifiableCredential
};
res.status(201).json(body);
} catch(error) {
logger.error(error.message, {error});
throw error;
Expand Down
13 changes: 10 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
addRoutes as addContextStoreRoutes
} from '@bedrock/service-context-store';
import {addRoutes} from './http.js';
import {getEnvelopeParams} from './envelopes.js';
import {getSuiteParams} from './suites.js';
import {initializeServiceAgent} from '@bedrock/service-agent';
import {klona} from 'klona';
Expand Down Expand Up @@ -108,9 +109,15 @@ async function validateConfigFn({config, op, existingConfig} = {}) {
// 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});
if(issueOptions.cryptosuites) {
// ensure every suite's params can be retrieved
for(const cryptosuite of issueOptions.cryptosuites) {
getSuiteParams({config, cryptosuite});
}
}
// ensure envelope's params can be retrieved
if(issueOptions.envelope) {
getEnvelopeParams({config, envelope: issueOptions.envelope});
}
}

Expand Down
Loading
Loading