From 9a9af4f3fc0c2d54b908712e6268a8f718db4efe Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 17 Mar 2024 18:21:21 -0400 Subject: [PATCH] Add support for external DID resolver. --- CHANGELOG.md | 5 +++ lib/documentLoader.js | 73 ++++++++++++++++++++++++++++++++-- lib/index.js | 17 +++++++- package.json | 1 + schemas/bedrock-vc-verifier.js | 21 +++++++++- 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee1f058..d7b3306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # bedrock-vc-verifier ChangeLog +## 19.2.0 - 2024-mm-dd + +### Added +- Add support for using a configured external DID resolver. + ## 19.1.0 - 2024-01-25 ### Changed diff --git a/lib/documentLoader.js b/lib/documentLoader.js index f295c59..f805c07 100644 --- a/lib/documentLoader.js +++ b/lib/documentLoader.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import { @@ -9,6 +9,7 @@ import { } from '@bedrock/jsonld-document-loader'; import {createContextDocumentLoader} from '@bedrock/service-context-store'; import {didIo} from '@bedrock/did-io'; +import {klona} from 'klona'; import '@bedrock/credentials-context'; import '@bedrock/data-integrity-context'; import '@bedrock/did-context'; @@ -53,9 +54,17 @@ export async function createDocumentLoader({config} = {}) { {config, serviceType}); return async function documentLoader(url) { - // resolve all DID URLs through did-io + // handle DID URLs... if(url.startsWith('did:')) { - const document = await didIo.get({url}); + let document; + if(config.verifyOptions?.didResolver) { + // resolve via configured DID resolver + const {verifyOptions: {didResolver}} = config; + document = await _resolve({didResolver, didUrl: url}); + } else { + // resolve via did-io + document = await didIo.get({url}); + } return { contextUrl: null, documentUrl: url, @@ -83,3 +92,61 @@ export async function createDocumentLoader({config} = {}) { } }; } + +async function _resolve({didResolver, didUrl}) { + // split on `?` query or `#` fragment + const [did] = didUrl.split(/(?=[\?#])/); + + // fetch DID document using DID resolver; + // assume a universal DID resolver 1.0 API in this version + const url = `${didResolver.url}/1.0/identifiers/${encodeURIComponent(did)}`; + const data = await httpClientHandler.get({url}); + + if(data?.id !== did) { + throw new Error(`DID document for DID "${did}" not found.`); + } + + // FIXME: perform DID document validation + // FIXME: handle URL query param / services + const didDocument = data; + + // if a fragment was found use the fragment to dereference a subnode + // in the did doc + const [, fragment] = url.split('#'); + if(fragment) { + const id = `${didDocument.id}${fragment}`; + return _getNode({didDocument, id}); + } + // resolve the full DID Document + return didDocument; +} + +function _getNode({didDocument, id}) { + // do verification method search first + let match = didDocument?.verificationMethod?.find(vm => vm?.id === id); + if(!match) { + // check other top-level nodes + for(const [key, value] of Object.entries(didDocument)) { + if(key === '@context' || key === 'verificationMethod') { + continue; + } + if(Array.isArray(value)) { + match = value.find(e => e?.id === id); + } else if(value?.id === id) { + match = value; + } + if(match) { + break; + } + } + } + + if(!match) { + throw new Error(`DID document entity with id "${id}" not found.`); + } + + return { + '@context': klona(didDocument['@context']), + ...klona(match) + }; +} diff --git a/lib/index.js b/lib/index.js index 838a763..209b896 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,15 @@ /*! - * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; +import {createService, schemas} from '@bedrock/service-core'; import { addRoutes as addContextStoreRoutes } from '@bedrock/service-context-store'; import {addRoutes} from './http.js'; -import {createService} from '@bedrock/service-core'; import {initializeServiceAgent} from '@bedrock/service-agent'; +import {klona} from 'klona'; +import {verifyOptions} from '../schemas/bedrock-vc-verifier.js'; // load config defaults import './config.js'; @@ -15,6 +17,15 @@ import './config.js'; const serviceType = 'vc-verifier'; bedrock.events.on('bedrock.init', async () => { + // add customizations to config validators... + const createConfigBody = klona(schemas.createConfigBody); + const updateConfigBody = klona(schemas.updateConfigBody); + const schemasToUpdate = [createConfigBody, updateConfigBody]; + for(const schema of schemasToUpdate) { + // verify options + schema.properties.verifyOptions = verifyOptions; + } + // create `vc-verifier` service const service = await createService({ serviceType, @@ -24,6 +35,8 @@ bedrock.events.on('bedrock.init', async () => { revocation: 1 }, validation: { + createConfigBody, + updateConfigBody, // require these zcaps (by reference ID) zcapReferenceIds: [{ referenceId: 'edv', diff --git a/package.json b/package.json index be565c3..2e9c10d 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "bnid": "^3.0.0", "body-parser": "^1.20.0", "cors": "^2.8.5", + "klona": "^2.0.6", "serialize-error": "^11.0.0" }, "peerDependencies": { diff --git a/schemas/bedrock-vc-verifier.js b/schemas/bedrock-vc-verifier.js index 1df1bbc..fe0a1de 100644 --- a/schemas/bedrock-vc-verifier.js +++ b/schemas/bedrock-vc-verifier.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved. */ const context = { title: '@context', @@ -10,6 +10,25 @@ const context = { } }; +export const verifyOptions = { + title: 'Verify Options', + type: 'object', + required: ['didResolver'], + additionalProperties: false, + properties: { + didResolver: { + title: 'DID Resolver', + type: 'object', + required: ['url'], + additionalProperties: false, + url: { + type: 'string', + pattern: '^https://[^.]+.[^.]+' + } + } + } +}; + export const createChallengeBody = { title: 'Create Challenge Body', type: 'object',