From 18c51122c1dabd081331381b7d93fadf35282d03 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 23 Feb 2024 20:24:08 -0500 Subject: [PATCH] Start adding `BitstringStatusList` support. --- lib/http.js | 2 +- lib/index.js | 2 +- lib/issue.js | 17 +++-- lib/slcs.js | 34 +++++---- lib/status.js | 15 ++-- package.json | 1 + schemas/bedrock-vc-status.js | 2 - test/mocha/20-status.js | 136 ++++++++++++++++++++++++++++++----- test/mocha/helpers.js | 12 +++- test/package.json | 4 +- 10 files changed, 177 insertions(+), 48 deletions(-) diff --git a/lib/http.js b/lib/http.js index b760364..dbf88fe 100644 --- a/lib/http.js +++ b/lib/http.js @@ -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}); diff --git a/lib/index.js b/lib/index.js index 4cffb49..2192dc6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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}); } diff --git a/lib/issue.js b/lib/issue.js index c0f62af..a135a39 100644 --- a/lib/issue.js +++ b/lib/issue.js @@ -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.validFrom = validFrom; + } + // delete existing proof delete credential.proof; } diff --git a/lib/slcs.js b/lib/slcs.js index 482a0cc..58f4735 100644 --- a/lib/slcs.js +++ b/lib/slcs.js @@ -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'; @@ -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 ` + @@ -196,8 +203,9 @@ 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}; diff --git a/lib/status.js b/lib/status.js index 6aa162f..13061ef 100644 --- a/lib/status.js +++ b/lib/status.js @@ -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'; @@ -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; } diff --git a/package.json b/package.json index 381279f..cc39aa1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/schemas/bedrock-vc-status.js b/schemas/bedrock-vc-status.js index 7f99c7e..b699a95 100644 --- a/schemas/bedrock-vc-status.js +++ b/schemas/bedrock-vc-status.js @@ -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: '^(.+):(.+)$' }; diff --git a/test/mocha/20-status.js b/test/mocha/20-status.js index 7f0c682..4f055fe 100644 --- a/test/mocha/20-status.js +++ b/test/mocha/20-status.js @@ -26,7 +26,9 @@ describe('status APIs', () => { `urn:zcap:root:${encodeURIComponent(statusInstanceId)}`; }); describe('/status-lists', () => { - it('creates a "StatusList2021" status list', async () => { + // FIXME: enable `BitstringStatusList` tests once br-vc-status-list is + // updated and imported by bedrock-vc-issuer + it.only('creates a "StatusList2021" status list', async () => { const statusListId = `${statusInstanceId}/status-lists/${uuid()}`; const statusListOptions = { credentialId: statusListId, @@ -64,13 +66,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' @@ -95,7 +135,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); @@ -111,7 +151,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' @@ -135,11 +175,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' @@ -164,7 +204,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([ @@ -178,7 +218,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' @@ -203,7 +243,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); @@ -218,7 +258,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' @@ -244,7 +284,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()}`; @@ -310,12 +349,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' @@ -348,7 +452,7 @@ describe('status APIs', () => { json: { credentialId, credentialStatus: { - type: 'StatusList2021Entry', + type: 'BitstringStatusListEntry', statusPurpose: 'revocation', statusListCredential, statusListIndex @@ -364,12 +468,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' @@ -403,7 +507,7 @@ describe('status APIs', () => { credentialId, indexAllocator: statusListOptions.indexAllocator, credentialStatus: { - type: 'StatusList2021Entry', + type: 'BitstringStatusListEntry', statusPurpose: 'revocation', statusListCredential, statusListIndex diff --git a/test/mocha/helpers.js b/test/mocha/helpers.js index 91674bb..c4453ce 100644 --- a/test/mocha/helpers.js +++ b/test/mocha/helpers.js @@ -7,7 +7,8 @@ import {importJWK, SignJWT} from 'jose'; import {KeystoreAgent, KmsClient} from '@digitalbazaar/webkms-client'; import {agent} from '@bedrock/https-agent'; import {CapabilityAgent} from '@digitalbazaar/webkms-client'; -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 {didIo} from '@bedrock/did-io'; import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020'; import {EdvClient} from '@digitalbazaar/edv-client'; @@ -290,9 +291,14 @@ export async function getCredentialStatus({ }) { const {data: slc} = await httpClient.get( statusListCredential, {agent: httpsAgent}); - const {encodedList} = slc.credentialSubject; - 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}); + } const status = list.getStatus(parseInt(statusListIndex, 10)); return {status, statusListCredential, statusListIndex}; } diff --git a/test/package.json b/test/package.json index 97021dd..7c1fa8b 100644 --- a/test/package.json +++ b/test/package.json @@ -39,8 +39,7 @@ "@bedrock/ssm-mongodb": "^10.1.2", "@bedrock/test": "^8.0.5", "@bedrock/validation": "^7.0.0", - "@bedrock/vc-issuer": "^25.2.0", - "@bedrock/vc-revocation-list-context": "^4.0.0", + "@bedrock/vc-issuer": "digitalbazaar/bedrock-vc-issuer#vc-v2-latest", "@bedrock/vc-status": "file:..", "@bedrock/vc-status-list-context": "^5.0.0", "@bedrock/veres-one-context": "^15.0.0", @@ -50,6 +49,7 @@ "@digitalbazaar/ezcap": "^4.0.0", "@digitalbazaar/http-client": "^4.0.0", "@digitalbazaar/vc-status-list": "^7.0.0", + "@digitalbazaar/vc-bitstring-status-list": "digitalbazaar/vc-bitstring-status-list#main", "@digitalbazaar/webkms-client": "^13.0.0", "c8": "^7.11.3", "cross-env": "^7.0.3",