Skip to content

Commit

Permalink
Add outline for envelope-based security issuance.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed May 15, 2024
1 parent 0ad2d04 commit 57832a1
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 51 deletions.
67 changes: 67 additions & 0 deletions lib/envelopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*!
* Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';

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};
}

function _createVCJWTEnveloper() {//{signer, options} = {}) {
// FIXME: `jws` / `jwt` options? claims? header?
//const {alg} = options;
// FIXME:
//const date = _getISODateTime();
return {
// FIXME:
async envelope() {//{verifiableCredential} = {}) {
// FIXME:
return {data: 'FIXME', mediaType: 'application/jwt'};
}
};
}
/*
function _getISODateTime(date = new Date()) {
// remove milliseconds precision
return date.toISOString().replace(/\.\d+Z$/, 'Z');
}
*/
34 changes: 1 addition & 33 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* 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';
Expand Down Expand Up @@ -53,7 +52,7 @@ 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;
Expand Down Expand Up @@ -125,34 +124,3 @@ 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;
}
}
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
4 changes: 3 additions & 1 deletion 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 @@ -114,8 +115,9 @@ async function validateConfigFn({config, op, existingConfig} = {}) {
getSuiteParams({config, cryptosuite});
}
}
// ensure envelope's params can be retrieved
if(issueOptions.envelope) {
// FIXME: do envelope validation
getEnvelopeParams({config, envelope: issueOptions.envelope});
}
}

Expand Down
102 changes: 87 additions & 15 deletions lib/issuer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
* Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import * as vc from '@digitalbazaar/vc';
import {
issue as _issue,
getDocumentStore, getIssuerAndSuites
getDocumentStore, getIssuerAndSecuringMethods
} from './helpers.js';
import assert from 'assert-plus';
import {createDocumentLoader} from './documentLoader.js';
import {CredentialStatusIssuer} from './CredentialStatusIssuer.js';
import {CredentialStatusWriter} from './CredentialStatusWriter.js';
import {v4 as uuid} from 'uuid';
import {constants as vcConstants} from '@bedrock/credentials-context';

const {util: {BedrockError}} = bedrock;

Expand All @@ -25,14 +26,16 @@ export async function issue({credential, config, options = {}} = {}) {
// see if config indicates a credential status should be set
const {statusListOptions = []} = config;

const [documentLoader, documentStore, {issuer, suites}] = await Promise.all([
const [documentLoader, documentStore, issuerInfo] = await Promise.all([
createDocumentLoader({config}),
// only fetch `documentStore` if a status list is configured; otherwise,
// it is not needed
statusListOptions.length > 0 ? getDocumentStore({config}) : {},
getIssuerAndSuites({config, options})
getIssuerAndSecuringMethods({config, options})
]);

const {issuer, suites, enveloper} = issuerInfo;

if(typeof credential.issuer === 'object') {
credential.issuer = {
...credential.issuer,
Expand All @@ -54,12 +57,27 @@ export async function issue({credential, config, options = {}} = {}) {

let issued = false;
let verifiableCredential;
let envelope;
let envelopedVerifiableCredential;
while(!issued) {
// issue any credential status(es)
const credentialStatus = await credentialStatusIssuer?.issue();

// issue VC
verifiableCredential = await _issue({credential, documentLoader, suites});
// secure VC with any cryptosuites
if(suites) {
verifiableCredential = await _secureWithSuites({
credential, documentLoader, suites
});
} else {
verifiableCredential = credential;
}

// secure VC with any envelope
if(enveloper) {
({envelope, envelopedVerifiableCredential} = await _secureWithEnvelope({
verifiableCredential, enveloper
}));
}

// 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
Expand All @@ -77,18 +95,23 @@ export async function issue({credential, config, options = {}} = {}) {
try {
// store issued VC, may throw on duplicate credential status(es) which
// can be ignored and issuance can be reattempted with new status(es)
const meta = {
// include `meta.type` as a non-user input type to validate against
type: 'VerifiableCredential',
// include status meta for uniqueness checks and other info
credentialStatus,
// include credential reference ID
credentialId
};
// add any envelope
if(envelope) {
meta.envelope = envelope;
}
await edvClient.insert({
doc: {
id: await edvClient.generateId(),
content: verifiableCredential,
meta: {
// include `meta.type` as a non-user input type to validate against
type: 'VerifiableCredential',
// include status meta for uniqueness checks and other info
credentialStatus,
// include credential reference ID
credentialId
}
meta
}
});
issued = true;
Expand Down Expand Up @@ -138,5 +161,54 @@ export async function issue({credential, config, options = {}} = {}) {
// finish issuing status (non-async function and can safely fail)
credentialStatusIssuer?.finish();

return verifiableCredential;
return {verifiableCredential, envelope, envelopedVerifiableCredential};
}

async function _secureWithSuites({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;
}
}

async function _secureWithEnvelope({verifiableCredential, enveloper}) {
// envelope: {data, mediaType}
const envelope = await enveloper.envelope({verifiableCredential});

// encode bytes to base64 for both storage and Data URL
if(envelope.data instanceof Uint8Array) {
envelope.data = Buffer.from(envelope.data).toString('base64');
envelope.encoding = 'base64';
}
const {data, mediaType, encoding} = envelope;
const dataURL = `data:${mediaType}${encoding ? ';base64,' : ','}${data}`;

const envelopedVerifiableCredential = {
'@context': vcConstants.CREDENTIALS_CONTEXT_V2_URL,
id: dataURL,
type: 'EnvelopedVerifiableCredential'
};
return {envelope, envelopedVerifiableCredential};
}

0 comments on commit 57832a1

Please sign in to comment.