From c3acfe5cb43d01974aa01b6cf63230b6281a8d5d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 12 Aug 2024 19:34:36 -0400 Subject: [PATCH] Improve error handling. - Return 400 instead of 500 for some common errors. --- CHANGELOG.md | 6 ++ lib/http.js | 15 ++++ test/mocha/70-misc.js | 203 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 test/mocha/70-misc.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ff0d75..14611296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # bedrock-vc-issuer ChangeLog +## 26.0.2 - 2024-xx-xx + +### Changed +- Improve error handling. + - Return 400 instead of 500 for some common errors. + ## 26.0.1 - 2024-08-02 ### Fixed diff --git a/lib/http.js b/lib/http.js index bbd39305..ede9078f 100644 --- a/lib/http.js +++ b/lib/http.js @@ -104,6 +104,21 @@ export async function addRoutes({app, service} = {}) { res.status(201).json(body); } catch(error) { logger.error(error.message, {error}); + // will be thrown for safe mode violations + if(error.name === 'jsonld.ValidationError') { + throw new BedrockError('Invalid credential.', { + name: 'SyntaxError', + details: { + httpStatusCode: 400, + public: true + } + }); + } + // TODO: handle other possible errors + // in particular @digitalbazaar/vc throws many simple 'TypeError' and + // 'Error' errors. Difficult to distinguish them from other errors. + + // default to other layers, likely will be an ISE/500 throw error; } diff --git a/test/mocha/70-misc.js b/test/mocha/70-misc.js new file mode 100644 index 00000000..60598627 --- /dev/null +++ b/test/mocha/70-misc.js @@ -0,0 +1,203 @@ +/*! + * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. + */ +import * as bedrock from '@bedrock/core'; +import * as helpers from './helpers.js'; +//import {createRequire} from 'node:module'; +import {klona} from 'klona'; +import {mockData} from './mock.data.js'; +import {v4 as uuid} from 'uuid'; + +//const require = createRequire(import.meta.url); + +// NOTE: using embedded context in mockCredential: +// https://www.w3.org/2018/credentials/examples/v1 +//const mockCredential = require('./mock-credential.json'); +//const mockCredentialV2 = require('./mock-credential-v2.json'); + +const badCredentials = [ + { + title: 'empty credential', + credential: {}, + expect: { + statusCode: 400, + name: 'ValidationError', + message: null + } + }, + { + title: 'unkonwn context', + credential: { + '@context': [ + 'https://www.w3.org/ns/credentials/v2', + 'bogus.example' + ], + type: ['VerifiableCredential'], + credentialSubject: { + 'ex:thing': true + } + }, + expect: { + // FIXME + statusCode: 400, + name: 'jsonld.InvalidUrl', + message: null + } + }, + { + title: 'empty subject', + credential: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential'], + credentialSubject: {} + }, + expect: { + // FIXME + statusCode: 500, + name: null, + message: null + } + }, + { + title: 'undefined terms', + credential: { + '@context': ['https://www.w3.org/ns/credentials/v2'], + type: ['VerifiableCredential', 'UndefinedType'], + credentialSubject: { + undefinedTerm: 'notDefinedInContext' + } + }, + expect: { + statusCode: 400, + name: 'SyntaxError', + message: 'Invalid credential.' + } + } +]; + +describe.only('fail for bed credentials', () => { + let suites; + let capabilityAgent; + let zcaps; + let noStatusListIssuerId; + let noStatusListIssuerRootZcap; + beforeEach(async () => { + // generate a proof set using all of these suites in each test + suites = [{ + name: 'Ed25519Signature2020', + algorithm: 'Ed25519' + }, { + name: 'eddsa-rdfc-2022', + algorithm: 'Ed25519' + }, { + name: 'ecdsa-rdfc-2019', + algorithm: 'P-256' + }, { + name: 'ecdsa-sd-2023', + algorithm: 'P-256', + // require these options (do not allow client to override) + options: { + mandatoryPointers: ['/issuer'] + } + }, { + name: 'ecdsa-xi-2023', + algorithm: 'P-256' + }, { + name: 'bbs-2023', + algorithm: 'Bls12381G2', + // require these options (do not allow client to override) + options: { + mandatoryPointers: ['/issuer'] + } + }]; + + // generate a `did:web` DID for the issuer + const {host} = bedrock.config.server; + const localId = uuid(); + const did = `did:web:${encodeURIComponent(host)}:did-web:${localId}`; + + // provision dependencies + ({capabilityAgent, zcaps} = await helpers.provisionDependencies({ + did, cryptosuites: suites, status: false, zcaps: true + })); + + // create issue options + const issueOptions = { + issuer: did, + cryptosuites: suites.map(suite => { + const {name, options, zcapReferenceIds} = suite; + const cryptosuite = {name, zcapReferenceIds}; + if(options) { + cryptosuite.options = options; + } + return cryptosuite; + }) + }; + + // create `did:web` DID document for issuer + const didDocument = { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + 'https://w3id.org/security/multikey/v1' + ], + id: did, + verificationMethod: [], + assertionMethod: [] + }; + for(const {assertionMethodKey} of suites) { + const description = await assertionMethodKey.getKeyDescription(); + delete description['@context']; + didDocument.verificationMethod.push(description); + didDocument.assertionMethod.push(description.id); + } + // add DID doc to map with DID docs to be served + mockData.didWebDocuments.set(localId, didDocument); + + // create issuer instance w/ no status list options + const noStatusListIssuerConfig = await helpers.createIssuerConfig({ + capabilityAgent, zcaps, issueOptions + }); + noStatusListIssuerId = noStatusListIssuerConfig.id; + noStatusListIssuerRootZcap = + `urn:zcap:root:${encodeURIComponent(noStatusListIssuerId)}`; + }); + // filter using 'only' and 'skip' + const _hasOnly = badCredentials.some(c => c.skip !== true && c.only === true); + const _badCredentials = badCredentials + .filter(c => c.skip !== true) + .filter(c => !_hasOnly || c.only === true); + for(const testCred of _badCredentials) { + it(`fails for ${testCred.title}`, async () => { + const credential = klona(testCred.credential); + let error; + let result; + try { + const zcapClient = helpers.createZcapClient({capabilityAgent}); + result = await zcapClient.write({ + url: `${noStatusListIssuerId}/credentials/issue`, + capability: noStatusListIssuerRootZcap, + json: { + credential, + options: { + extraInformation: 'abc' + } + } + }); + } catch(e) { + error = e; + } + should.exist(error); + should.not.exist(result); + if(testCred.expect.statusCode) { + error.status.should.equal(testCred.expect.statusCode); + } + if(testCred.expect.name !== null) { + error.data.name.should.equal(testCred.expect.name); + } + if(testCred.expect.message !== null) { + error.data.message.should.equal(testCred.expect.message); + } + }); + } +});