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

Bitstring status list #5

Merged
merged 2 commits into from
Feb 24, 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
2 changes: 1 addition & 1 deletion lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async function _createOrRefreshStatusList({
}
}

// FIXME: check available storage via meter before allowing operation
// TODO: check available storage via meter before allowing operation
try {
const {config} = req.serviceObject;
const statusListId = _getStatusListId({req});
Expand Down
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ bedrock.events.on('bedrock.init', async () => {

async function usageAggregator({meter, signal, service} = {}) {
const {id: meterId} = meter;
// FIXME: add SLCs storage
// TODO: add SLCs storage
return service.configStorage.getUsage({meterId, signal});
}
17 changes: 13 additions & 4 deletions lib/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@
*/
import {getZcapClient} from './helpers.js';

const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1';

export async function issue({config, credential, updateValidity = true} = {}) {
if(updateValidity) {
// express date without milliseconds
const date = new Date();
// TODO: use `validFrom` and `validUntil` for v2 VCs
credential.issuanceDate = `${date.toISOString().slice(0, -5)}Z`;
// FIXME: get validity period via status instance config
const validFrom = `${date.toISOString().slice(0, -5)}Z`;
date.setDate(date.getDate() + 1);
credential.expirationDate = `${date.toISOString().slice(0, -5)}Z`;
const validUntil = `${date.toISOString().slice(0, -5)}Z`;

if(credential['@context'].includes(CREDENTIALS_CONTEXT_V1_URL)) {
credential.issuanceDate = validFrom;
credential.expirationDate = validUntil;
} else {
credential.validFrom = validFrom;
credential.validUntil = validUntil;
}

// delete existing proof
delete credential.proof;
}
Expand Down
38 changes: 23 additions & 15 deletions lib/slcs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
*/
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,
createCredential as createListCredential
} from '@digitalbazaar/vc-bitstring-status-list';
import {
createList as createList2021,
createCredential as createSlc
createCredential as createList2021Credential
} from '@digitalbazaar/vc-status-list';
import assert from 'assert-plus';
import {issue} from './issue.js';
Expand Down Expand Up @@ -84,11 +83,19 @@ export async function create({
}
});
}
// FIXME: implement `BitstringStatusList`
// assume `StatusList2021`
const list = await createList2021({length});
// FIXME: handle `statusPurpose` as an array (not just a single value)
let credential = await createSlc({id: credentialId, list, statusPurpose});
let credential;
if(type === 'BitstringStatusList') {
const list = await createList({length});
credential = await createListCredential({
id: credentialId, list, statusPurpose
});
} else {
// `type` must be `StatusList2021`
const list = await createList2021({length});
credential = await createList2021Credential({
id: credentialId, list, statusPurpose
});
}
credential.name = 'Status List Credential';
credential.description =
`This credential expresses status information for some ` +
Expand Down Expand Up @@ -196,15 +203,16 @@ export async function getFresh({config, statusListId} = {}) {
// any refreshed VC is still valid once returned to the client
const now = new Date();
now.setTime(now.getTime() + 1000 * 60);
// FIXME: support v2 VCs w/`validUntil`
const validUntil = new Date(record.credential.expirationDate);
const validUntil = new Date(
record.credential.validUntil ||
record.credential.expirationDate);
if(now <= validUntil) {
// SLC not expired
return {credential: record.credential};
}
// refresh SLC
const doc = await refresh({config, statusListId});
return {credential: doc.content};
const {credential} = await refresh({config, statusListId});
return {credential};
}

/**
Expand Down
15 changes: 9 additions & 6 deletions lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ 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 {decodeList} from '@digitalbazaar/vc-bitstring-status-list';
import {decodeList as decodeList2021} from '@digitalbazaar/vc-status-list';
import {issue} from './issue.js';
import {LIST_TYPE_TO_ENTRY_TYPE} from './constants.js';

Expand Down Expand Up @@ -135,7 +132,13 @@ export async function setStatus({
// check if `credential` status is already set, if so, done
let {credential: slc} = record;
const {credentialSubject: {encodedList}} = slc;
const list = await decodeList({encodedList});
let list;
if(slc.type.includes('BitstringStatusListCredential')) {
list = await decodeList({encodedList});
} else {
// type must be `StatusListCredential`
list = await decodeList2021({encodedList});
}
if(list.getStatus(bitstringIndex) === status) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@digitalbazaar/ed25519-signature-2020": "^5.2.0",
"@digitalbazaar/ezcap": "^4.1.0",
"@digitalbazaar/lru-memoize": "^3.0.0",
"@digitalbazaar/vc-bitstring-status-list": "digitalbazaar/vc-bitstring-status-list#main",
"@digitalbazaar/vc-status-list": "^7.0.0",
"assert-plus": "^1.0.0",
"bnid": "^3.0.0",
Expand Down
2 changes: 0 additions & 2 deletions schemas/bedrock-vc-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {MAX_LIST_SIZE} from '../lib/constants.js';
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: '^(.+):(.+)$'
};

Expand Down
132 changes: 117 additions & 15 deletions test/mocha/20-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,51 @@ describe('status APIs', () => {
]);
});

it('creates a "BitstringStatusList" status list', async () => {
const statusListId = `${statusInstanceId}/status-lists/${uuid()}`;
const statusListOptions = {
credentialId: statusListId,
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
};
let error;
let result;
try {
result = await helpers.createStatusList({
url: statusListId,
capabilityAgent,
capability: statusInstanceRootZcap,
statusListOptions
});
} catch(e) {
error = e;
}
assertNoError(error);
should.exist(result.id);
result.id.should.equal(statusListId);

// get status list and make assertions on it
const slc = await helpers.getStatusListCredential({statusListId});
should.exist(slc);
slc.should.include.keys([
'id', 'credentialSubject', 'validFrom', 'validUntil'
]);
slc.id.should.equal(statusListOptions.credentialId);
slc.id.should.equal(statusListId);
slc.credentialSubject.should.include.keys([
'id', 'type', 'encodedList', 'statusPurpose'
]);
});

it('creates a status list with non-equal credential ID', async () => {
// suffix must match
const suffix = `/status-lists/${uuid()}`;
const statusListId = `${statusInstanceId}${suffix}`;
const statusListOptions = {
credentialId: `https://foo.example/anything/111${suffix}`,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand All @@ -95,7 +133,7 @@ describe('status APIs', () => {
const slc = await helpers.getStatusListCredential({statusListId});
should.exist(slc);
slc.should.include.keys([
'id', 'credentialSubject', 'issuanceDate', 'expirationDate'
'id', 'credentialSubject', 'validFrom', 'validUntil'
]);
slc.id.should.equal(statusListOptions.credentialId);
slc.id.should.not.equal(statusListId);
Expand All @@ -111,7 +149,7 @@ describe('status APIs', () => {
const statusListId = `${statusInstanceId}${suffix}`;
const statusListOptions = {
credentialId: `https://foo.example/not-allowed/${localId}`,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand All @@ -135,11 +173,11 @@ describe('status APIs', () => {
`("/status-lists/${localId}").`);
});

it('creates a terse "StatusList2021" status list', async () => {
it('creates a terse "BitstringStatusList" status list', async () => {
const statusListId = `${statusInstanceId}/status-lists/revocation/0`;
const statusListOptions = {
credentialId: statusListId,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand All @@ -164,7 +202,7 @@ describe('status APIs', () => {
const slc = await helpers.getStatusListCredential({statusListId});
should.exist(slc);
slc.should.include.keys([
'id', 'credentialSubject', 'issuanceDate', 'expirationDate'
'id', 'credentialSubject', 'validFrom', 'validUntil'
]);
slc.id.should.equal(statusListId);
slc.credentialSubject.should.include.keys([
Expand All @@ -178,7 +216,7 @@ describe('status APIs', () => {
const statusListId = `${statusInstanceId}${suffix}`;
const statusListOptions = {
credentialId: `https://foo.example/anything/111${suffix}`,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand All @@ -203,7 +241,7 @@ describe('status APIs', () => {
const slc = await helpers.getStatusListCredential({statusListId});
should.exist(slc);
slc.should.include.keys([
'id', 'credentialSubject', 'issuanceDate', 'expirationDate'
'id', 'credentialSubject', 'validFrom', 'validUntil'
]);
slc.id.should.equal(statusListOptions.credentialId);
slc.id.should.not.equal(statusListId);
Expand All @@ -218,7 +256,7 @@ describe('status APIs', () => {
const statusListId = `${statusInstanceId}${suffix}`;
const statusListOptions = {
credentialId: `https://foo.example/not-allowed/revocation/0`,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand All @@ -244,7 +282,6 @@ describe('status APIs', () => {
});

describe('/credentials/status', () => {
// FIXME: add "BitstringStatusList" test
it('updates a "StatusList2021" revocation status', async () => {
// first create a status list
const statusListId = `${statusInstanceId}/status-lists/${uuid()}`;
Expand Down Expand Up @@ -310,12 +347,77 @@ describe('status APIs', () => {
status.should.equal(true);
});

it('updates a "BitstringStatusList" revocation status', async () => {
// first create a status list
const statusListId = `${statusInstanceId}/status-lists/${uuid()}`;
const statusListOptions = {
credentialId: statusListId,
type: 'BitstringStatusList',
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
const statusInfo = await helpers.getCredentialStatus({
statusListCredential, statusListIndex
});
let {status} = statusInfo;
status.should.equal(false);

// then revoke VC
const zcapClient = helpers.createZcapClient({capabilityAgent});
let error;
try {
await zcapClient.write({
url: `${statusInstanceId}/credentials/status`,
capability: statusInstanceRootZcap,
json: {
credentialId,
indexAllocator: statusListOptions.indexAllocator,
credentialStatus: {
type: 'BitstringStatusListEntry',
statusPurpose: 'revocation',
statusListCredential,
statusListIndex
}
}
});
} catch(e) {
error = e;
}
assertNoError(error);

// force refresh status list
await zcapClient.write({
url: `${statusListCredential}?refresh=true`,
capability: statusInstanceRootZcap,
json: {}
});

// check status of VC has changed
({status} = await helpers.getCredentialStatus({
statusListCredential, statusListIndex
}));
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',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand Down Expand Up @@ -348,7 +450,7 @@ describe('status APIs', () => {
json: {
credentialId,
credentialStatus: {
type: 'StatusList2021Entry',
type: 'BitstringStatusListEntry',
statusPurpose: 'revocation',
statusListCredential,
statusListIndex
Expand All @@ -364,12 +466,12 @@ describe('status APIs', () => {
'credential the first time.');
});

it('updates a terse "StatusList2021" revocation status', async () => {
it('updates a terse "BitstringStatusList" revocation status', async () => {
// first create a terse status list
const statusListId = `${statusInstanceId}/status-lists/revocation/0`;
const statusListOptions = {
credentialId: statusListId,
type: 'StatusList2021',
type: 'BitstringStatusList',
indexAllocator: `urn:uuid:${uuid()}`,
length: 131072,
statusPurpose: 'revocation'
Expand Down Expand Up @@ -403,7 +505,7 @@ describe('status APIs', () => {
credentialId,
indexAllocator: statusListOptions.indexAllocator,
credentialStatus: {
type: 'StatusList2021Entry',
type: 'BitstringStatusListEntry',
statusPurpose: 'revocation',
statusListCredential,
statusListIndex
Expand Down
Loading
Loading