Skip to content

Commit

Permalink
Require indexAllocator when setting status.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Feb 23, 2024
1 parent 123ea1e commit ad13455
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 27 deletions.
16 changes: 9 additions & 7 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ export async function addRoutes({app, service} = {}) {
asyncHandler(async (req, res) => {
try {
const {config} = req.serviceObject;
// FIXME: require `indexAllocator` if not previously set
const {credentialId, credentialStatus, status = true} = req.body;

await setStatus({config, credentialId, credentialStatus, status});

const {
credentialId, indexAllocator, credentialStatus, status = true
} = req.body;
await setStatus({
config, credentialId, indexAllocator, credentialStatus, status
});
res.status(200).end();
} catch(error) {
logger.error(error.message, {error});
Expand Down Expand Up @@ -147,10 +148,11 @@ async function _createOrRefreshStatusList({
res.sendStatus(204);
} else {
const {
credentialId, type, /*indexAllocator,*/ length, statusPurpose
credentialId, indexAllocator, type, length, statusPurpose
} = req.body;
await slcs.create({
config, statusListId, credentialId, type, statusPurpose, length
config, statusListId, credentialId, indexAllocator,
type, statusPurpose, length
});
res.status(204).location(statusListId).send();
}
Expand Down
34 changes: 27 additions & 7 deletions lib/slcs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import * as bedrock from '@bedrock/core';
import * as database from '@bedrock/mongodb';
// FIXME: add bitstring status list support
// import {
// createBitstringStatusList,
// createBitstringStatusListCredential
// } from '@digitalbazaar/vc-bitstring-status-list';
import {
createList as createList2021,
createCredential as createSlc
Expand Down Expand Up @@ -39,6 +44,9 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
* @param {object} options - The options to use.
* @param {object} options.config - The status instance config.
* @param {string} options.statusListId - The ID of the status list.
* @param {string} options.indexAllocator - An unambiguous identifier for
* index allocation state; this must be provided whenever setting the
* status of a VC for the first time on the created status list.
* @param {string} options.credentialId - The ID of the status list credential.
* @param {string} options.type - The type of status list credential.
* @param {string} options.statusPurpose - The status purpose.
Expand All @@ -47,7 +55,8 @@ bedrock.events.on('bedrock-mongodb.ready', async () => {
* @returns {Promise<object>} Settles once the operation completes.
*/
export async function create({
config, statusListId, credentialId, type, statusPurpose, length
config, statusListId, indexAllocator,
credentialId, type, statusPurpose, length
} = {}) {
if(!LIST_TYPE_TO_ENTRY_TYPE.has(type)) {
throw new BedrockError(
Expand Down Expand Up @@ -85,8 +94,8 @@ export async function create({
`This credential expresses status information for some ` +
'other credentials in an encoded and compressed list.';
credential = await issue({config, credential});
await set({statusListId, credential, sequence: 0});
return {statusListId, credential};
await set({statusListId, indexAllocator, credential, sequence: 0});
return {statusListId, indexAllocator, credential};
}

/**
Expand All @@ -96,15 +105,21 @@ export async function create({
*
* @param {object} options - The options to use.
* @param {string} options.statusListId - The ID of the status list.
* @param {string} options.indexAllocator - An unambiguous identifier for
* index allocation state; this must be provided whenever setting the
* status of a VC for the first time on a status list.
* @param {object} options.credential - The status list credential.
* @param {number} options.sequence - The sequence number associated with the
* credential; used to ensure only newer versions of the credential are
* stored.
*
* @returns {Promise<object>} Settles once the operation completes.
*/
export async function set({statusListId, credential, sequence} = {}) {
export async function set({
statusListId, indexAllocator, credential, sequence
} = {}) {
assert.string(statusListId, 'statusListId');
assert.string(indexAllocator, 'indexAllocator');
assert.object(credential, 'credential');
assert.number(sequence, 'sequence');

Expand All @@ -117,7 +132,7 @@ export async function set({statusListId, credential, sequence} = {}) {
'meta.sequence': sequence === 0 ? null : sequence - 1
}, {
$set,
$setOnInsert: {statusListId, 'meta.created': now}
$setOnInsert: {statusListId, indexAllocator, 'meta.created': now}
}, {upsert: true});

if(result.result.n > 0) {
Expand Down Expand Up @@ -214,7 +229,10 @@ export async function refresh({config, statusListId} = {}) {
credential = await issue({config, credential});

// set updated SLC
await set({statusListId, credential, sequence: record.meta.sequence + 1});
await set({
statusListId, indexAllocator: record.indexAllocator,
credential, sequence: record.meta.sequence + 1
});

return {credential};
} catch(e) {
Expand All @@ -233,7 +251,9 @@ async function _getUncachedRecord({statusListId}) {
const collection = database.collections[COLLECTION_NAME];
const record = await collection.findOne(
{statusListId},
{projection: {_id: 0, statusListId: 1, credential: 1, meta: 1}});
{projection: {
_id: 0, statusListId: 1, indexAllocator: 1, credential: 1, meta: 1
}});
if(!record) {
throw new BedrockError(
'Status list credential not found.', {
Expand Down
35 changes: 33 additions & 2 deletions lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import * as bedrock from '@bedrock/core';
import * as mappings from './mappings.js';
import * as slcs from './slcs.js';
import assert from 'assert-plus';
// FIXME: add bitstring status list support
// import {
// decodeBitstringStatusList
// } from '@digitalbazaar/vc-bitstring-status-list';
import {decodeList} from '@digitalbazaar/vc-status-list';
import {issue} from './issue.js';
import {LIST_TYPE_TO_ENTRY_TYPE} from './constants.js';

const {util: {BedrockError}} = bedrock;

export async function setStatus({
config, credentialId, credentialStatus, status
config, credentialId, indexAllocator, credentialStatus, status
} = {}) {
assert.object(config, 'config');
assert.string(credentialId, 'credentialId');
assert.optionalString(indexAllocator, 'indexAllocator');
assert.object(credentialStatus, 'credentialStatus');
assert.bool(status, 'status');

Expand Down Expand Up @@ -88,7 +93,32 @@ export async function setStatus({
let record = await slcs.get({statusListId, useCache: false});
_assertStatusListMatch({slc: record.credential, credentialStatus});

// ensure `indexAllocator` value matches if given
if(indexAllocator !== undefined &&
record.indexAllocator !== indexAllocator) {
throw new BedrockError(
`"indexAllocator" (${indexAllocator}) ` +
`does not match the expected value (${record.indexAllocator}).`,
'DataError', {
actual: indexAllocator,
expected: record.indexAllocator,
httpStatusCode: 400,
public: true
});
}

// create new mapping...
if(!mapping) {
// `indexAllocator` is required when creating a new mapping
if(indexAllocator === undefined) {
throw new BedrockError(
`"indexAllocator" is required when setting the status of a ` +
'credential the first time.',
'DataError', {
httpStatusCode: 400,
public: true
});
}
// add new mapping
await mappings.set({
configId,
Expand Down Expand Up @@ -119,7 +149,8 @@ export async function setStatus({

// update SLC
await slcs.set({
statusListId, credential: slc, sequence: record.meta.sequence + 1
statusListId, indexAllocator: record.indexAllocator,
credential: slc, sequence: record.meta.sequence + 1
});
return;
} catch(e) {
Expand Down
22 changes: 12 additions & 10 deletions schemas/bedrock-vc-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@
*/
import {MAX_LIST_SIZE} from '../lib/constants.js';

// an ID value required to unambiguously identify index allocation state
const indexAllocator = {
// an ID (URL) referring to an index allocator
type: 'string',
// FIXME: pull in schema from bedrock-validation that uses
// `uri` pattern from ajv-formats once available
pattern: '^(.+):(.+)$'
};

export const createStatusListBody = {
title: 'Create Status List',
type: 'object',
additionalProperties: false,
required: [
'credentialId', 'type', 'indexAllocator', 'length', 'statusPurpose'
'credentialId', 'indexAllocator', 'type', 'length', 'statusPurpose'
],
properties: {
credentialId: {
type: 'string'
},
indexAllocator,
type: {
type: 'string',
// supported types in this version
Expand All @@ -23,14 +33,6 @@ export const createStatusListBody = {
'StatusList2021'
]
},
// an ID value required to track index allocation
indexAllocator: {
// an ID (URL) referring to an index allocator
type: 'string',
// FIXME: pull in schema from bedrock-validation that uses
// `uri` pattern from ajv-formats once available
pattern: '^(.+):(.+)$'
},
// length of the status list in bits
length: {
type: 'number',
Expand All @@ -46,13 +48,13 @@ export const createStatusListBody = {
export const updateCredentialStatusBody = {
title: 'Update Credential Status',
type: 'object',
// FIXME: consider if `indexAllocator` should be required
required: ['credentialId', 'credentialStatus'],
additionalProperties: false,
properties: {
credentialId: {
type: 'string'
},
indexAllocator,
credentialStatus: {
type: 'object',
required: ['type', 'statusPurpose'],
Expand Down
58 changes: 57 additions & 1 deletion test/mocha/20-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ describe('status APIs', () => {
const suffix = `/status-lists/revocation/0`;
const statusListId = `${statusInstanceId}${suffix}`;
const statusListOptions = {
credentialId: `https://foo.example/anything/111/${suffix}`,
credentialId: `https://foo.example/anything/111${suffix}`,
type: 'StatusList2021',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
Expand Down Expand Up @@ -282,6 +282,7 @@ describe('status APIs', () => {
capability: statusInstanceRootZcap,
json: {
credentialId,
indexAllocator: statusListOptions.indexAllocator,
credentialStatus: {
type: 'StatusList2021Entry',
statusPurpose: 'revocation',
Expand Down Expand Up @@ -309,6 +310,60 @@ describe('status APIs', () => {
status.should.equal(true);
});

it('fails to set status when no "indexAllocator" given', async () => {
// first create a status list
const statusListId = `${statusInstanceId}/status-lists/${uuid()}`;
const statusListOptions = {
credentialId: statusListId,
type: 'StatusList2021',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
};
const {id: statusListCredential} = await helpers.createStatusList({
url: statusListId,
capabilityAgent,
capability: statusInstanceRootZcap,
statusListOptions
});

// pretend a VC with this `credentialId` has been issued
const credentialId = `urn:uuid:${uuid()}`;
const statusListIndex = '0';

// get VC status, should work w/ initialized `false` value
const statusInfo = await helpers.getCredentialStatus({
statusListCredential, statusListIndex
});
const {status} = statusInfo;
status.should.equal(false);

// try to revoke VC w/o `indexAllocator`
const zcapClient = helpers.createZcapClient({capabilityAgent});
let error;
try {
await zcapClient.write({
url: `${statusInstanceId}/credentials/status`,
capability: statusInstanceRootZcap,
json: {
credentialId,
credentialStatus: {
type: 'StatusList2021Entry',
statusPurpose: 'revocation',
statusListCredential,
statusListIndex
}
}
});
} catch(e) {
error = e;
}
should.exist(error);
error.data.message.should.equal(
'"indexAllocator" is required when setting the status of a ' +
'credential the first time.');
});

it('updates a terse "StatusList2021" revocation status', async () => {
// first create a terse status list
const statusListId = `${statusInstanceId}/status-lists/revocation/0`;
Expand Down Expand Up @@ -346,6 +401,7 @@ describe('status APIs', () => {
capability: statusInstanceRootZcap,
json: {
credentialId,
indexAllocator: statusListOptions.indexAllocator,
credentialStatus: {
type: 'StatusList2021Entry',
statusPurpose: 'revocation',
Expand Down

0 comments on commit ad13455

Please sign in to comment.