@digitalbazaar/ezcap-express 4.0.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @digitalbazaar/ezcap-express Changelog
2
2
 
3
+ ## 4.3.0 - 2021-12-10
4
+
5
+ ### Added
6
+ - Allow any controller in a delegated zcap's chain to revoke it. This authority
7
+ is inherent in delegation and is now reflected in code. This feature gives
8
+ delegators more fine-grained control to revoke zcaps that they did not
9
+ delegate directly but one of their delegates did, allowing them to stop
10
+ specific zcap usage without having to revoke more of the chain. It also
11
+ gives zcap controllers the ability to revoke their own zcaps (if desired)
12
+ and adds a sanity check to prevent the revocation of root zcaps that use
13
+ the `urn:zcap:root:` ID scheme.
14
+
15
+ ## 4.2.0 - 2021-08-26
16
+
17
+ ### Added
18
+ - Add `suiteFactory` parameter to middleware creation functions. A
19
+ `suiteFactory` function should be passed and return the supported LD proof
20
+ suite (or an array of supported LD proof suites) that is supported for
21
+ authorizing zcap invocations and verifying capability chains.
22
+ - Add `authorizeZcapRevocation` middleware that can be attached to root
23
+ container/object endpoints to enable revocation of zcaps that have been
24
+ delegated to use them. This version assumes that the revocations endpoint
25
+ will follow this RESTful format: `<rootObjectUrl>/revocations/<zcapId>`
26
+ and that the body will be JSON and include a `capability` member with
27
+ the zcap to revoke. Future versions may allow for greater flexibility.
28
+
29
+ ### Changed
30
+ - Deprecate passing a `suite` to any middleware creation functions. Instead,
31
+ `suiteFactory` should be passed. The next major version will remove `suite`.
32
+ This approach allows this library to remove npm dependencies that provide
33
+ cryptographic suites preventing this library from being affected when those
34
+ dependencies need to change.
35
+
36
+ ## 4.1.1 - 2021-07-21
37
+
38
+ ### Changed
39
+ - Updated dependencies.
40
+
41
+ ## 4.1.0 - 2021-07-11
42
+
43
+ ### Changed
44
+ - Updated http-signature-zcap-verify to 8.1.x to bring in optimizations
45
+ for controllers that use DID Documents.
46
+
3
47
  ## 4.0.1 - 2021-07-10
4
48
 
5
49
  ### Fixed
package/lib/authorize.js CHANGED
@@ -4,13 +4,9 @@
4
4
  import assert from 'assert-plus';
5
5
  import asyncHandler from 'express-async-handler';
6
6
  import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
7
- import {parseSignatureHeader} from 'http-signature-header';
8
- import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
9
- import * as sec from 'security-context';
7
+ import * as helpers from './helpers.js';
10
8
  import {verifyCapabilityInvocation} from 'http-signature-zcap-verify';
11
9
 
12
- const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
13
-
14
10
  /**
15
11
  * Authorizes an incoming request.
16
12
  *
@@ -42,129 +38,56 @@ const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
42
38
  * inspect a capability chain, e.g., to check for revocations.
43
39
  * @param {Function} [options.onError] - An error handler handler for
44
40
  * customizable error handling.
45
- * @param {object} [options.suite] - The expected cryptography suite to use when
46
- * verifying digital signatures.
41
+ * @param {object} [options.suite] - The expected cryptography suite to use
42
+ * when verifying digital signatures (deprecated; use `suiteFactory`
43
+ * instead).
44
+ * @param {object} [options.suiteFactory] - A factory for creating the
45
+ * supported suite(s) to use when verifying digital signatures.
47
46
  *
48
- * @returns {Function} Returns an Express.js middleware route handler.
47
+ * @returns {Function} Returns an Express.js style middleware route handler.
49
48
  */
50
49
  export function authorizeZcapInvocation({
51
50
  documentLoader, expectedHost, getExpectedTarget, getRootController,
52
51
  allowTargetAttenuation = true, expectedAction, getExpectedAction,
53
52
  getExpectedRootCapabilityId, inspectCapabilityChain,
54
- onError, suite = new Ed25519Signature2020()
53
+ onError, suite, suiteFactory
55
54
  } = {}) {
55
+ // `helpers.createExpectationMiddleware` handles type checks on other params
56
56
  assert.func(documentLoader, 'options.documentLoader');
57
- assert.optionalString(expectedAction, 'options.expectedAction');
58
- assert.optionalFunc(
59
- getExpectedAction, 'options.getExpectedAction');
60
- assert.string(expectedHost, 'options.expectedHost');
61
- assert.func(getExpectedTarget, 'options.getExpectedTarget');
62
57
  assert.func(getRootController, 'options.getRootController');
63
- assert.optionalFunc(
64
- getExpectedRootCapabilityId, 'options.getExpectedRootCapabilityId');
65
58
  assert.optionalFunc(inspectCapabilityChain, 'options.inspectCapabilityChain');
66
- assert.optionalFunc(onError, 'options.onError');
67
-
68
- if(getExpectedAction && expectedAction !== undefined) {
69
- throw new Error('Use "getExpectedAction" or "expectedAction", not both.');
59
+ assert.optionalFunc(suiteFactory, 'options.suiteFactory');
60
+
61
+ // this code block is to be removed the next major release (5.0); `suite`
62
+ // should be removed as a parameter
63
+ if(!(suite && suiteFactory)) {
64
+ suiteFactory = () => new Ed25519Signature2020();
65
+ } else if(!suiteFactory) {
66
+ // backwards compatibility
67
+ suiteFactory = () => suite;
70
68
  }
71
69
 
72
- return asyncHandler(async (req, res, next) => {
73
- // originalUrl must be used to support nested express routers
74
- const {originalUrl: url, method, headers} = req;
75
- let params;
76
- try {
77
- ({params} = parseSignatureHeader(headers.authorization));
78
- } catch(e) {
79
- const error = new Error('Missing or invalid "authorization" header.');
80
- error.name = 'DataError';
81
- error.cause = e;
82
- return _handleError({error, onError});
83
- }
84
- const {keyId} = params;
85
-
86
- // if body is present, ensure header digest value matches digest of body
87
- if(_hasBody({req})) {
88
- const {digest: expectedDigest} = headers;
89
- if(!expectedDigest) {
90
- const error = new Error(
91
- 'A "digest" header must be present when an HTTP body is present.');
92
- error.name = 'DataError';
93
- error.httpStatusCode = 400;
94
- return _handleError({error, onError});
95
- }
96
- const {verified} = await verifyHeaderValue({
97
- data: req.body, headerValue: expectedDigest});
98
- if(!verified) {
99
- const error = new Error(
100
- 'The "digest" header value does not match digest of body.');
101
- error.name = 'DataError';
102
- error.httpStatusCode = 400;
103
- return _handleError({error, onError});
104
- }
105
- } else {
106
- // prevent any unhandled `req.body` from being erroneously used
107
- req.body = undefined;
108
- }
109
-
110
- // use `getExpectedAction` if provided
111
- if(getExpectedAction) {
112
- expectedAction = await getExpectedAction({req});
113
- }
114
-
115
- let expectedTarget;
116
- try {
117
- // getExpectedTarget may throw an error
118
- ({expectedTarget} = await getExpectedTarget({req}));
119
- if(!(typeof expectedTarget === 'string' ||
120
- Array.isArray(expectedTarget))) {
121
- throw new Error(
122
- 'Return value from "getExpectedTarget" must be an object with ' +
123
- '"expectedTarget" set to a string or an array.');
124
- }
125
- } catch(error) {
126
- return _handleError({error, onError});
127
- }
128
-
129
- let _expectedAction = expectedAction;
130
-
131
- // set expected action if it has not been specified
132
- /* Note: This is safe as long as the server's request handling
133
- infrastructure differentiates based on HTTP method (as is typical practice
134
- with express/connect routing. So, while the client specifies the HTTP
135
- method, the server specifies the handler for that HTTP method. For example,
136
- this middleware will ensure that if a client specifies "POST" then it
137
- must be invoking a zcap that grants "write" action authority. Then, provided
138
- that the server's router ensures that only the "POST" handler will be
139
- executed (typical routing practice), all is well. If the handler code is
140
- chosen via some other means, e.g., via the request body, then the caller
141
- MUST provide the expected action and not rely on default behavior. */
142
- if(_expectedAction === undefined) {
143
- _expectedAction = 'read';
144
- if(req.method === 'POST') {
145
- _expectedAction = 'write';
146
- }
147
- }
70
+ return [
71
+ helpers.createExpectationMiddleware({
72
+ expectedHost, expectedAction, getExpectedAction, getExpectedTarget,
73
+ getExpectedRootCapabilityId, onError
74
+ }),
75
+ authorizeZcapInvocationAfterParse({
76
+ documentLoader, getRootController, suiteFactory,
77
+ allowTargetAttenuation, inspectCapabilityChain, onError
78
+ })
79
+ ];
80
+ }
148
81
 
149
- let expectedRootCapability;
150
- try {
151
- if(getExpectedRootCapabilityId) {
152
- // retrieve the root capability given the request and expected params
153
- // return value can be a string or an array of strings
154
- expectedRootCapability = await getExpectedRootCapabilityId({
155
- req, expectedHost, expectedTarget,
156
- expectedAction: _expectedAction
157
- });
158
- } else if(Array.isArray(expectedTarget)) {
159
- expectedRootCapability = expectedTarget.map(
160
- t => `${ZCAP_ROOT_PREFIX}${encodeURIComponent(t)}`);
161
- } else {
162
- expectedRootCapability =
163
- `${ZCAP_ROOT_PREFIX}${encodeURIComponent(expectedTarget)}`;
164
- }
165
- } catch(error) {
166
- return _handleError({error, onError});
167
- }
82
+ export function authorizeZcapInvocationAfterParse({
83
+ documentLoader, getRootController, suiteFactory,
84
+ allowTargetAttenuation = true, inspectCapabilityChain, onError
85
+ } = {}) {
86
+ return asyncHandler(async (req, res, next) => {
87
+ const {
88
+ expectedAction, expectedHost, expectedRootCapability, expectedTarget,
89
+ signature: {params: {keyId}}
90
+ } = req.ezcap;
168
91
 
169
92
  // retrieves the root capability that was invoked
170
93
  async function getInvokedCapability({id}) {
@@ -182,24 +105,26 @@ export function authorizeZcapInvocation({
182
105
  actual: id,
183
106
  expected: expectedRootCapability,
184
107
  };
185
- return _handleError({error, onError});
108
+ return helpers.handleError({error, onError});
186
109
  }
187
- return _getRootCapability({
110
+ return helpers.getRootCapability({
188
111
  getRootController, req, expectedHost, expectedTarget,
189
- expectedAction: _expectedAction, rootCapabilityId
112
+ expectedAction, rootCapabilityId
190
113
  });
191
114
  }
192
115
 
193
- // perform the capability invocation
116
+ // perform the capability invocation...
117
+ // `originalUrl` must be used to support nested express routers
118
+ const {originalUrl: url, method, headers} = req;
194
119
  const result = await verifyCapabilityInvocation({
195
120
  url,
196
121
  method,
197
- suite,
122
+ suite: await suiteFactory({req}),
198
123
  headers,
199
124
  expectedHost,
200
- documentLoader: _wrappedDocumentLoader({
125
+ documentLoader: helpers.wrappedDocumentLoader({
201
126
  documentLoader,
202
- expectedAction: _expectedAction,
127
+ expectedAction,
203
128
  expectedHost,
204
129
  expectedTarget,
205
130
  getRootController,
@@ -207,7 +132,7 @@ export function authorizeZcapInvocation({
207
132
  }),
208
133
  getInvokedCapability,
209
134
  expectedTarget,
210
- expectedAction: _expectedAction,
135
+ expectedAction,
211
136
  expectedRootCapability,
212
137
  inspectCapabilityChain,
213
138
  keyId,
@@ -216,7 +141,7 @@ export function authorizeZcapInvocation({
216
141
 
217
142
  // return HTTP 403 if verification fails
218
143
  if(!result.verified) {
219
- _handleError({error: result.error, onError, throwError: false});
144
+ helpers.handleError({error: result.error, onError, throwError: false});
220
145
  return res.status(403).send();
221
146
  }
222
147
 
@@ -229,60 +154,3 @@ export function authorizeZcapInvocation({
229
154
  process.nextTick(next);
230
155
  });
231
156
  }
232
-
233
- function _handleError({error, onError, throwError = true}) {
234
- if(onError) {
235
- return onError({error});
236
- }
237
- if(throwError) {
238
- throw error;
239
- }
240
- }
241
-
242
- function _wrappedDocumentLoader({
243
- req, documentLoader, expectedHost, expectedTarget, expectedAction,
244
- getRootController
245
- }) {
246
- return async url => {
247
- if(url.startsWith(ZCAP_ROOT_PREFIX)) {
248
- const document = await _getRootCapability({
249
- getRootController, req, expectedHost, expectedTarget, expectedAction,
250
- rootCapabilityId: url
251
- });
252
-
253
- return {
254
- contextUrl: null,
255
- documentUrl: url,
256
- document,
257
- };
258
- }
259
-
260
- return documentLoader(url);
261
- };
262
- }
263
-
264
- async function _getRootCapability({
265
- getRootController, req, expectedHost, expectedTarget, expectedAction,
266
- rootCapabilityId
267
- }) {
268
- const rootInvocationTarget = decodeURIComponent(
269
- rootCapabilityId.substr(ZCAP_ROOT_PREFIX.length));
270
- const controller = await getRootController({
271
- req, expectedHost, expectedTarget, expectedAction,
272
- rootCapabilityId, rootInvocationTarget
273
- });
274
- return {
275
- '@context': sec.constants.SECURITY_CONTEXT_V2_URL,
276
- id: rootCapabilityId,
277
- invocationTarget: rootInvocationTarget,
278
- controller
279
- };
280
- }
281
-
282
- function _hasBody({req}) {
283
- // a request has a body if `transfer-encoding` or `content-length` headers
284
- // are set: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
285
- return req.body &&
286
- (req.get('transfer-encoding') !== undefined ||
287
- req.get('content-length') !== undefined);
288
- }
package/lib/helpers.js ADDED
@@ -0,0 +1,205 @@
1
+ /*!
2
+ * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import assert from 'assert-plus';
5
+ import asyncHandler from 'express-async-handler';
6
+ import {constants as zCapConstants} from '@digitalbazaar/zcapld';
7
+ import * as helpers from './helpers.js';
8
+ import {parseSignatureHeader} from 'http-signature-header';
9
+ import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
10
+
11
+ const {ZCAP_CONTEXT_URL} = zCapConstants;
12
+
13
+ export const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
14
+
15
+ // middleware used to collect expected values for zcap authorization
16
+ export function createExpectationMiddleware({
17
+ expectedHost, getExpectedTarget,
18
+ expectedAction, getExpectedAction,
19
+ getExpectedRootCapabilityId, onError
20
+ }) {
21
+ assert.string(expectedHost, 'options.expectedHost');
22
+ assert.func(getExpectedTarget, 'options.getExpectedTarget');
23
+ assert.optionalString(expectedAction, 'options.expectedAction');
24
+ assert.optionalFunc(getExpectedAction, 'options.getExpectedAction');
25
+ assert.optionalFunc(
26
+ getExpectedRootCapabilityId, 'options.getExpectedRootCapabilityId');
27
+ assert.optionalFunc(onError, 'options.onError');
28
+
29
+ if(getExpectedAction && expectedAction !== undefined) {
30
+ throw new Error('Use "getExpectedAction" or "expectedAction", not both.');
31
+ }
32
+
33
+ return asyncHandler(async (req, res, next) => {
34
+ // cache ezcap express info
35
+ req.ezcap = {expectedHost};
36
+
37
+ const {headers} = req;
38
+ try {
39
+ const {params} = parseSignatureHeader(headers.authorization);
40
+ req.ezcap.signature = {params};
41
+ } catch(e) {
42
+ const error = new Error('Missing or invalid "authorization" header.');
43
+ error.name = 'DataError';
44
+ error.cause = e;
45
+ return helpers.handleError({error, onError});
46
+ }
47
+
48
+ // if body is present, ensure header digest value matches digest of body
49
+ if(helpers.hasBody({req})) {
50
+ const {digest: expectedDigest} = headers;
51
+ if(!expectedDigest) {
52
+ const error = new Error(
53
+ 'A "digest" header must be present when an HTTP body is present.');
54
+ error.name = 'DataError';
55
+ error.httpStatusCode = 400;
56
+ return helpers.handleError({error, onError});
57
+ }
58
+ const {verified} = await verifyHeaderValue({
59
+ data: req.body, headerValue: expectedDigest});
60
+ if(!verified) {
61
+ const error = new Error(
62
+ 'The "digest" header value does not match digest of body.');
63
+ error.name = 'DataError';
64
+ error.httpStatusCode = 400;
65
+ return helpers.handleError({error, onError});
66
+ }
67
+ } else {
68
+ // prevent any unhandled `req.body` from being erroneously used
69
+ req.body = undefined;
70
+ }
71
+
72
+ try {
73
+ // getExpectedTarget may throw an error
74
+ req.ezcap.expectedTarget = await helpers.getExpectedTarget(
75
+ {req, getExpectedTarget});
76
+ } catch(error) {
77
+ return helpers.handleError({error, onError});
78
+ }
79
+
80
+ // set expected action
81
+ req.ezcap.expectedAction = expectedAction;
82
+
83
+ // use `getExpectedAction` if provided
84
+ if(getExpectedAction) {
85
+ req.ezcap.expectedAction = await getExpectedAction({req});
86
+ }
87
+
88
+ /* Note: This is safe as long as the server's request handling
89
+ infrastructure differentiates based on HTTP method (as is typical practice
90
+ with express/connect routing. So, while the client specifies the HTTP
91
+ method, the server specifies the handler for that HTTP method. For example,
92
+ this middleware will ensure that if a client specifies "POST" then it
93
+ must be invoking a zcap that grants "write" action authority. Then, provided
94
+ that the server's router ensures that only the "POST" handler will be
95
+ executed (typical routing practice), all is well. If the handler code is
96
+ chosen via some other means, e.g., via the request body, then the caller
97
+ MUST provide the expected action and not rely on default behavior. */
98
+ if(req.ezcap.expectedAction === undefined) {
99
+ req.ezcap.expectedAction = req.method === 'POST' ? 'write' : 'read';
100
+ }
101
+
102
+ try {
103
+ req.ezcap.expectedRootCapability = await helpers
104
+ .getExpectedRootCapability({
105
+ req, expectedHost,
106
+ expectedTarget: req.ezcap.expectedTarget,
107
+ expectedAction: req.ezcap.expectedAction,
108
+ getExpectedRootCapabilityId
109
+ });
110
+ } catch(error) {
111
+ return helpers.handleError({error, onError});
112
+ }
113
+
114
+ // call `next` on the next tick to ensure the promise from this function
115
+ // resolves and does not reject because some subsequent middleware throws
116
+ // an error
117
+ process.nextTick(next);
118
+ });
119
+ }
120
+
121
+ export function handleError({error, onError, throwError = true}) {
122
+ if(onError) {
123
+ return onError({error});
124
+ }
125
+ if(throwError) {
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ export function wrappedDocumentLoader({
131
+ req, documentLoader, expectedHost, expectedTarget, expectedAction,
132
+ getRootController
133
+ }) {
134
+ return async url => {
135
+ if(url.startsWith(ZCAP_ROOT_PREFIX)) {
136
+ const document = await getRootCapability({
137
+ getRootController, req, expectedHost, expectedTarget, expectedAction,
138
+ rootCapabilityId: url
139
+ });
140
+
141
+ return {
142
+ contextUrl: null,
143
+ documentUrl: url,
144
+ document,
145
+ };
146
+ }
147
+
148
+ return documentLoader(url);
149
+ };
150
+ }
151
+
152
+ export async function getExpectedRootCapability({
153
+ req, expectedHost, expectedTarget, expectedAction,
154
+ getExpectedRootCapabilityId
155
+ }) {
156
+ if(getExpectedRootCapabilityId) {
157
+ // retrieve the root capability given the request and expected params
158
+ // return value can be a string or an array of strings
159
+ return getExpectedRootCapabilityId({
160
+ req, expectedHost, expectedTarget, expectedAction
161
+ });
162
+ }
163
+ if(Array.isArray(expectedTarget)) {
164
+ return expectedTarget.map(
165
+ t => `${ZCAP_ROOT_PREFIX}${encodeURIComponent(t)}`);
166
+ }
167
+ return `${ZCAP_ROOT_PREFIX}${encodeURIComponent(expectedTarget)}`;
168
+ }
169
+
170
+ export async function getExpectedTarget({req, getExpectedTarget}) {
171
+ const {expectedTarget} = await getExpectedTarget({req});
172
+ if(!(typeof expectedTarget === 'string' ||
173
+ Array.isArray(expectedTarget))) {
174
+ throw new Error(
175
+ 'Return value from "getExpectedTarget" must be an object with ' +
176
+ '"expectedTarget" set to a string or an array.');
177
+ }
178
+ return expectedTarget;
179
+ }
180
+
181
+ export async function getRootCapability({
182
+ getRootController, req, expectedHost, expectedTarget, expectedAction,
183
+ rootCapabilityId
184
+ }) {
185
+ const rootInvocationTarget = decodeURIComponent(
186
+ rootCapabilityId.substr(ZCAP_ROOT_PREFIX.length));
187
+ const controller = await getRootController({
188
+ req, expectedHost, expectedTarget, expectedAction,
189
+ rootCapabilityId, rootInvocationTarget
190
+ });
191
+ return {
192
+ '@context': ZCAP_CONTEXT_URL,
193
+ id: rootCapabilityId,
194
+ invocationTarget: rootInvocationTarget,
195
+ controller
196
+ };
197
+ }
198
+
199
+ export function hasBody({req}) {
200
+ // a request has a body if `transfer-encoding` or `content-length` headers
201
+ // are set: http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
202
+ return req.body &&
203
+ (req.get('transfer-encoding') !== undefined ||
204
+ req.get('content-length') !== undefined);
205
+ }
package/lib/main.js CHANGED
@@ -2,3 +2,4 @@
2
2
  * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  export {authorizeZcapInvocation} from './authorize.js';
5
+ export {authorizeZcapRevocation} from './revoke.js';
package/lib/revoke.js ADDED
@@ -0,0 +1,189 @@
1
+ /*!
2
+ * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import assert from 'assert-plus';
5
+ import asyncHandler from 'express-async-handler';
6
+ import {CapabilityDelegation} from '@digitalbazaar/zcapld';
7
+ import * as helpers from './helpers.js';
8
+ import * as jsigs from 'jsonld-signatures';
9
+ import {authorizeZcapInvocationAfterParse} from './authorize.js';
10
+
11
+ /**
12
+ * Authorizes a request to submit a zcap revocation.
13
+ *
14
+ * @param {object} options - Options hashmap.
15
+ * @param {object} options.documentLoader - Document loader used to load
16
+ * DID Documents, capability documents, and JSON-LD Contexts.
17
+ * @param {string} options.expectedHost - The expected host for the invoked
18
+ * capability.
19
+ * @param {Function} options.getExpectedTarget - Used to return the expected
20
+ * target(s) for the invoked capability.
21
+ * @param {Function} options.getRootController - Used to get the root
22
+ * capability controller for the given root capability ID.
23
+ * @param {Function} options.suiteFactory - A factory for creating the
24
+ * supported suite(s) to use when verifying digital signatures.
25
+ * @param {boolean} [options.allowTargetAttenuation=true] - Allow the
26
+ * invocationTarget of a delegation chain to be increasingly restrictive
27
+ * based on a hierarchical RESTful URL structure.
28
+ * @param {Function} [options.getExpectedRootCapabilityId] - Used to return the
29
+ * expected root capability identifiers for the expected targets.
30
+ * @param {Function} [options.inspectCapabilityChain] - A function that can
31
+ * inspect a capability chain, e.g., to check for revocations.
32
+ * @param {Function} [options.onError] - An error handler handler for
33
+ * customizable error handling.
34
+ *
35
+ * @returns {Function} Returns an Express.js style middleware route handler.
36
+ */
37
+ export function authorizeZcapRevocation({
38
+ documentLoader, expectedHost, getExpectedTarget, getRootController,
39
+ allowTargetAttenuation = true, getExpectedRootCapabilityId,
40
+ inspectCapabilityChain, onError, suiteFactory
41
+ }) {
42
+ assert.func(suiteFactory, 'options.suiteFactory');
43
+
44
+ // expected action is always `write` for submitting a revocation
45
+ const expectedAction = 'write';
46
+ return [
47
+ helpers.createExpectationMiddleware({
48
+ expectedHost, expectedAction, getExpectedTarget,
49
+ getExpectedRootCapabilityId, onError
50
+ }),
51
+ verifyCapabilityDelegation({
52
+ documentLoader, getRootController, suiteFactory, inspectCapabilityChain,
53
+ onError
54
+ }),
55
+ authorizeZcapInvocationAfterParse({
56
+ documentLoader, getRootController: _getRevocationRootController,
57
+ suiteFactory, allowTargetAttenuation, inspectCapabilityChain, onError
58
+ })
59
+ ];
60
+ }
61
+
62
+ function verifyCapabilityDelegation({
63
+ documentLoader, getRootController, inspectCapabilityChain, suiteFactory,
64
+ onError
65
+ }) {
66
+ return asyncHandler(async function getDelegator(req, res, next) {
67
+ const {
68
+ expectedAction, expectedHost, expectedTarget, expectedRootCapability
69
+ } = req.ezcap;
70
+
71
+ const {body: capability} = req;
72
+
73
+ // early-disallow revocation of root zcaps that follow ID convention
74
+ if(capability.id.startsWith(helpers.ZCAP_ROOT_PREFIX)) {
75
+ const error = new Error('A root capability cannot be revoked.');
76
+ error.name = 'NotAllowedError';
77
+ return helpers.handleError({error, onError});
78
+ }
79
+
80
+ // verify CapabilityDelegation
81
+ let delegator;
82
+ const chainControllers = [];
83
+ try {
84
+ const results = await _verifyDelegation({
85
+ capability,
86
+ documentLoader: helpers.wrappedDocumentLoader({
87
+ req, documentLoader, expectedHost, expectedTarget, expectedAction,
88
+ getRootController
89
+ }),
90
+ expectedRootCapability,
91
+ inspectCapabilityChain: _captureChainControllers({
92
+ inspectCapabilityChain,
93
+ chainControllers
94
+ }),
95
+ suiteFactory
96
+ });
97
+ ({delegator} = results[0].purposeResult);
98
+ delegator = delegator.id || delegator;
99
+ } catch(e) {
100
+ const error = new Error('The provided capability delegation is invalid.');
101
+ error.name = 'DataError';
102
+ error.cause = e;
103
+ return helpers.handleError({error, onError});
104
+ }
105
+
106
+ req.zcapRevocation = {delegator, chainControllers};
107
+
108
+ // proceed to next middleware on next tick to prevent subsequent
109
+ // middleware from potentially throwing here
110
+ process.nextTick(next);
111
+ });
112
+ }
113
+
114
+ async function _verifyDelegation({
115
+ req, capability, documentLoader,
116
+ expectedRootCapability, inspectCapabilityChain, suiteFactory
117
+ }) {
118
+ const {verified, error, results} = await jsigs.verify(capability, {
119
+ suite: await suiteFactory({req}),
120
+ purpose: new CapabilityDelegation({
121
+ allowTargetAttenuation: true,
122
+ expectedRootCapability,
123
+ inspectCapabilityChain,
124
+ suite: await suiteFactory({req})
125
+ }),
126
+ documentLoader
127
+ });
128
+ if(!verified) {
129
+ throw error;
130
+ }
131
+ return results;
132
+ }
133
+
134
+ async function _getRevocationRootController({
135
+ req, rootCapabilityId, rootInvocationTarget, getRootController,
136
+ revocationsSubPath = '/revocations/'
137
+ }) {
138
+ // if `revocations` is not in the root invocation target, then defer to
139
+ // `getRootController` to try and provide the root controller
140
+ if(!rootInvocationTarget.includes(revocationsSubPath)) {
141
+ return getRootController({req, rootCapabilityId, rootInvocationTarget});
142
+ }
143
+
144
+ /* Note: If the invocation target is a zcap-specific revocation endpoint,
145
+ we use all zcap controllers from the submitted zcap's chain as the root
146
+ controller value for the target.
147
+
148
+ This approach allows any party that has delegated a zcap or received one
149
+ to be able to send it for revocation. Subsequent code (in the revocation
150
+ route handler) will confirm that the delegation is proper and the zcap from
151
+ which it was delegated has not itself been revoked.
152
+
153
+ To be clear, if the delegation chain is:
154
+
155
+ root -> A -> B
156
+
157
+ Any zcap controller in the chain of B may invoke a root zcap with a
158
+ `target` of `<baseUrl>/revocations/<ID of B>` (and an ID of
159
+ `urn:zcap:root:encodeURIComponent(<baseUrl>/revocations/<ID of B>)`). This
160
+ means that `root`, `A`, or `B` may revoke `B`.
161
+
162
+ As long no other zcap in the chain of `B` (e.g., `A`) has already been
163
+ revoked, then `B` will be revoked and stored as a revocation until `B`
164
+ expires. */
165
+
166
+ // use all `chainControllers`
167
+ // presumes `verifyCapabilityDelegation` middleware already called
168
+ return req.zcapRevocation.chainControllers;
169
+ }
170
+
171
+ function _captureChainControllers({inspectCapabilityChain, chainControllers}) {
172
+ return async function _inspectCapabilityChain(chainDetails) {
173
+ // collect every controller in the chain
174
+ const {capabilityChain} = chainDetails;
175
+ for(const capability of capabilityChain.values()) {
176
+ chainControllers.push(..._getCapabilityControllers({capability}));
177
+ }
178
+ return inspectCapabilityChain(chainDetails);
179
+ };
180
+ }
181
+
182
+ function _getCapabilityControllers({capability}) {
183
+ const {controller, id} = capability;
184
+ const result = controller || id;
185
+ if(!result) {
186
+ return [];
187
+ }
188
+ return Array.isArray(result) ? result : [result];
189
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/ezcap-express",
3
- "version": "4.0.1",
3
+ "version": "4.3.0",
4
4
  "main": "lib",
5
5
  "module": "main.js",
6
6
  "repository": {
@@ -21,12 +21,13 @@
21
21
  "dependencies": {
22
22
  "@digitalbazaar/ed25519-signature-2020": "^3.0.0",
23
23
  "@digitalbazaar/http-digest-header": "^1.0.1",
24
+ "@digitalbazaar/zcapld": "^5.1.2",
24
25
  "assert-plus": "^1.0.0",
25
26
  "esm": "^3.2.25",
26
27
  "express-async-handler": "^1.1.4",
27
28
  "http-signature-header": "^2.0.2",
28
- "http-signature-zcap-verify": "^8.0.0",
29
- "security-context": "^4.0.0"
29
+ "http-signature-zcap-verify": "^8.1.1",
30
+ "jsonld-signatures": "^9.3.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "chai": "^4.2.0",