@digitalbazaar/ezcap-express 4.3.0 → 5.0.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,80 @@
1
1
  # @digitalbazaar/ezcap-express Changelog
2
2
 
3
+ ## 5.0.0 - 2022-01-11
4
+
5
+ ### Added
6
+ - Add optional parameters `maxChainLength`, `maxDelegationTtl`, and
7
+ `maxTimestampDelta` to allow for more fine grained control. These parameters
8
+ all have defaults in `@digitalbazaar/zcap` that could previously not be set
9
+ to other values at this layer.
10
+ - **BREAKING**: Add required `getVerifier` async function parameter. The
11
+ function will be passed `{keyId, documentLoader}` to verify an HTTP signature
12
+ and must return `{verifier, verificationMethod}`. The `verifier` object must
13
+ have a `verify` function that takes `{data, signature}` and returns a
14
+ boolean indicating whether the `Uint8Array` `signature` is verified
15
+ against the `Uint8Array` `data` -- or throws an error if there is a reason
16
+ the cryptographic signature verification check cannot be run.
17
+ - Include `capabilityChain` in `req.zcapRevocation` when using revocation
18
+ middleware. This property includes the entire dereferenced chain.
19
+
20
+ ### Changed
21
+ - **BREAKING**: Replace broken-out expected value parameters (e.g.,
22
+ `expectedHost`, `expectedTarget`), including duplicative / optional
23
+ parameters (e.g., `expectedAction`, `getExpectedAction`) with a single
24
+ async function `getExpectedValues({req})` that returns all required (and any
25
+ optional) expected values. This removes some optionality and simplifies
26
+ function signatures -- also allowing callers to decide how they want
27
+ to provide this information (e.g., by calling individual functions from
28
+ within `getExpectedValues` or whatever else).
29
+ - **BREAKING**: The `authorizeZcapRevocation` middleware may now only be used
30
+ on routes ending in `/revocations/:revocationId`. The API params have also
31
+ changed as the only expected value that is needed from the user is
32
+ `expectedHost`. The rest of the expected values are hard coded according to
33
+ a conventional pattern for supporting revocation of any zcaps delegated from
34
+ a root capability for a service object. The service object's root capability
35
+ MUST have an invocation target that matches the service object's URL (aka its
36
+ "ID", `<serviceObjectId>`). So for the absolute URL:
37
+
38
+ `<serviceObjectId>/revocations/:revocationId`
39
+
40
+ A zcap can only be revoked using the middleware if its chain has a root
41
+ zcap with an invocation target that is prefixed with `<serviceObjectId>`.
42
+ The middleware will use the `expectedHost` value to construct the absolute
43
+ URL.
44
+ - **BREAKING**: Require `suiteFactory` parameter, no default cryptosuites are
45
+ included with this package to ensure it is decoupled from particular
46
+ cryptosuites.
47
+
48
+ ### Fixed
49
+ - **BREAKING**: HTTP status error codes have been fixed so that client errors
50
+ will result in 4xx status codes instead of 5xx status codes.
51
+
52
+ ### Remove
53
+ - **BREAKING**: Remove `getExpectedRootCapabilityId` as there have been no
54
+ use cases that have needed it.
55
+ - **BREAKING**: Remove deprecated `suite` param, use `suiteFactory` instead.
56
+
57
+ ## 4.5.0 - 2021-12-17
58
+
59
+ ### Fixed
60
+ - Add `_createGetRevocationRootController` wrapper around
61
+ `_getRevocationRootController` and pass `getRootController` to it.
62
+
63
+ ### Added
64
+ - Add tests for `authorizeZcapRevocation`.
65
+
66
+ ## 4.4.0 - 2021-12-15
67
+
68
+ ### Added
69
+ - Add additional tests.
70
+
71
+ ## 4.3.1 - 2021-12-13
72
+
73
+ ### Fixed
74
+ - Fix `expectedAction` to be `write` for `DELETE` method.
75
+ - Throw error when no `expectedAction` is given for a given HTTP method and
76
+ provide defaults for all common HTTP methods.
77
+
3
78
  ## 4.3.0 - 2021-12-10
4
79
 
5
80
  ### Added
package/README.md CHANGED
@@ -56,19 +56,22 @@ npm install
56
56
  ### Define getRootController
57
57
 
58
58
  ```js
59
+ // this will only be called if `rootInvocationTarget` matches
60
+ // one of the expected root invocation targets specified
59
61
  async function getRootController({
60
62
  req, rootCapabilityId, rootInvocationTarget
61
63
  }) {
62
- // get associated capability controller from database
64
+ // get controller for a service object from a database
63
65
  let controller;
64
66
  try {
65
- const record = await database.getMyThingById({
67
+ const record = await database.getMyServiceObjectById({
68
+ // typically, root invocation target is a service object ID
66
69
  id: rootInvocationTarget
67
70
  });
68
71
  controller = record.controller;
69
72
  } catch(e) {
70
73
  if(e.type === 'NotFoundError') {
71
- const url = req.protocol + '://' + req.get('host') + req.url;
74
+ const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
72
75
  throw new Error(
73
76
  `Invalid capability identifier "${rootCapabilityId}" ` +
74
77
  `for URL "${url}".`);
@@ -76,6 +79,8 @@ async function getRootController({
76
79
  throw e;
77
80
  }
78
81
 
82
+ // return the service object's controller so it will be
83
+ // added to the root capability for the service object
79
84
  return controller;
80
85
  }
81
86
  ```
@@ -113,13 +118,20 @@ async function documentLoader(url) {
113
118
  ```js
114
119
  const {authorizeZcapInvocation} = require('ezcap-express');
115
120
 
116
- async function authorizeMyZcapInvocation({expectedTarget, expectedAction} = {}) {
121
+ async function authorizeMyZcapInvocation({expectedAction} = {}) {
117
122
  return authorizeZcapInvocation({
118
- expectedHost: 'ezcap.example',
119
- getRootController,
120
- documentLoader,
121
- expectedTarget,
122
- expectedAction,
123
+ getExpectedValues({req}) {
124
+ const expectedHost = 'ezcap.example';
125
+ const {localId} = req.params;
126
+ const serviceObjectId =
127
+ `https://${expectedHost}/${encodeURIComponent(localId)}`;
128
+ return {
129
+ action: expectedAction,
130
+ host: expectedHost,
131
+ rootInvocationTarget: serviceObjectId
132
+ };
133
+ },
134
+ getRootController
123
135
  });
124
136
  };
125
137
  ```
@@ -132,7 +144,7 @@ import asyncHandler from 'express-async-handler';
132
144
 
133
145
  const app = express();
134
146
 
135
- app.post('/foo',
147
+ app.post('/my-objects/:localId',
136
148
  authorizeMyZcapInvocation(),
137
149
  asyncHandler(async (req, res) => {
138
150
  // your code goes here
@@ -150,7 +162,7 @@ regarding the systems it interacts with:
150
162
  * The REST-ful systems center around reading and writing resources.
151
163
 
152
164
  If these assumptions do not apply to your system, the
153
- [zcapld](https://github.com/digitalbazaar/zcapld) library might
165
+ [zcap](https://github.com/digitalbazaar/zcap) library might
154
166
  be a better, albeit more complex, solution for you.
155
167
 
156
168
  Looking at each of these core assumptions more closely will help explain how designing systems to these constraints make it much easier to think about
@@ -194,8 +206,8 @@ These are the two assumptions that ezcap makes and with those two assumptions,
194
206
  ## authorizeZcapInvocation(options) ⇒ <code>function</code>
195
207
  Authorizes an incoming request.
196
208
 
197
- **Kind**: global function
198
- **Returns**: <code>function</code> - Returns an Express.js middleware route handler.
209
+ **Kind**: global function
210
+ **Returns**: <code>function</code> - Returns an Express.js middleware route handler.
199
211
 
200
212
  | Param | Type | Description |
201
213
  | --- | --- | --- |
@@ -204,12 +216,10 @@ Authorizes an incoming request.
204
216
  | [options.expectedAction] | <code>string</code> | The expected action for the invoked capability. |
205
217
  | options.expectedHost | <code>string</code> | The expected host for the invoked capability. |
206
218
  | [options.expectedTarget] | <code>string</code> \| <code>Array.&lt;string&gt;</code> | The expected target(s) for the invoked capability. |
207
- | [options.getExpectedRootCapabilityId] | <code>function</code> | Used to return the expected root capability identifiers for the expected targets. |
208
219
  | options.getRootController | <code>function</code> | Used to get the root capability controller for the given root capability ID. |
209
220
  | [options.logger] | <code>object</code> | The logger instance to use. |
210
221
  | [options.suite] | <code>object</code> | The expected cryptography suite to use when verifying digital signatures. |
211
222
 
212
-
213
223
  ## Contribute
214
224
 
215
225
  See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)!
package/lib/authorize.js CHANGED
@@ -1,87 +1,94 @@
1
1
  /*!
2
- * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
4
  import assert from 'assert-plus';
5
5
  import asyncHandler from 'express-async-handler';
6
- import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
7
6
  import * as helpers from './helpers.js';
8
7
  import {verifyCapabilityInvocation} from 'http-signature-zcap-verify';
9
8
 
10
9
  /**
11
10
  * Authorizes an incoming request.
12
11
  *
12
+ * @typedef GetExpectedValues - See helpers.js.
13
+ *
13
14
  * @param {object} options - Options hashmap.
14
- * @param {object} options.documentLoader - Document loader used to load
15
- * DID Documents, capability documents, and JSON-LD Contexts.
16
- * @param {string} options.expectedHost - The expected host for the invoked
17
- * capability.
18
- * @param {Function} options.getExpectedTarget - Used to return the expected
19
- * target(s) for the invoked capability.
20
- * @param {Function} options.getRootController - Used to get the root capability
21
- * controller for the given root capability ID.
22
15
  * @param {boolean} [options.allowTargetAttenuation=true] - Allow the
23
16
  * invocationTarget of a delegation chain to be increasingly restrictive
24
17
  * based on a hierarchical RESTful URL structure.
25
- * @param {string} [options.expectedAction] - The expected action for the
26
- * invoked capability; use this or `getExpectedAction`, not both.
27
- * @param {Function} [options.getExpectedAction] - Used to return the
28
- * expected action for the invoked capability; use this or `expectedAction`,
29
- * not both; if neither are provided, then the expected action will be
30
- * determined based on the HTTP method from the request -- which is only safe
31
- * provided that the handler code path is also determined based on the HTTP
32
- * method in the request (i.e., typical method-based express/connect routing);
33
- * if the handler code path is determined by some other means, e.g., the
34
- * request body, then `getExpectedAction` MUST be used.
35
- * @param {Function} [options.getExpectedRootCapabilityId] - Used to return the
36
- * expected root capability identifiers for the expected targets.
18
+ * @param {object} options.documentLoader - Document loader used to load
19
+ * DID Documents, capability documents, and JSON-LD Contexts.
20
+ * @param {GetExpectedValues} options.getExpectedValues - Used to get the
21
+ * expected values when checking the zcap invocation.
22
+ * @param {Function} options.getRootController - Used to get the controller
23
+ * of the root capability in the invoked capability's chain.
24
+ * @param {Function<Promise>} options.getVerifier - An async function to
25
+ * call to get a verifier and verification method for the key ID.
37
26
  * @param {Function} [options.inspectCapabilityChain] - A function that can
38
27
  * inspect a capability chain, e.g., to check for revocations.
39
- * @param {Function} [options.onError] - An error handler handler for
28
+ * @param {number} [options.maxChainLength=10] - The maximum length of the
29
+ * capability delegation chain.
30
+ * @param {number} [options.maxClockSkew=300] - A maximum number of seconds
31
+ * that clocks may be skewed when checking capability expiration date-times
32
+ * against `date`, when comparing invocation proof creation time against
33
+ * delegation proof creation time, and when comparing the capability
34
+ * invocation expiration time against `now`.
35
+ * @param {number} [options.maxDelegationTtl=1000*60*60*24*90] - The maximum
36
+ * milliseconds to live for a delegated zcap as measured by the time
37
+ * difference between `expires` and `created` on the delegation proof.
38
+ * @param {Function} [options.onError] - An error handler handler for
40
39
  * customizable error handling.
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.
40
+ * @param {object} options.suiteFactory - A factory for creating the
41
+ * supported suite(s) to use when verifying zcap delegation chains; this is
42
+ * different from `getVerifier` which is used to produce a verifier for
43
+ * verifying HTTP signatures used to invoke zcaps.
46
44
  *
47
45
  * @returns {Function} Returns an Express.js style middleware route handler.
48
46
  */
49
47
  export function authorizeZcapInvocation({
50
- documentLoader, expectedHost, getExpectedTarget, getRootController,
51
- allowTargetAttenuation = true, expectedAction, getExpectedAction,
52
- getExpectedRootCapabilityId, inspectCapabilityChain,
53
- onError, suite, suiteFactory
48
+ allowTargetAttenuation = true,
49
+ documentLoader, getExpectedValues, getRootController, getVerifier,
50
+ inspectCapabilityChain,
51
+ maxChainLength = 10,
52
+ // 300 second clock skew permitted by default
53
+ maxClockSkew = 300,
54
+ // 90 day max TTL by default
55
+ maxDelegationTtl = 1000 * 60 * 60 * 24 * 90,
56
+ onError,
57
+ suiteFactory
54
58
  } = {}) {
55
59
  // `helpers.createExpectationMiddleware` handles type checks on other params
60
+ assert.bool(allowTargetAttenuation, 'options.allowTargetAttenuation');
56
61
  assert.func(documentLoader, 'options.documentLoader');
57
62
  assert.func(getRootController, 'options.getRootController');
63
+ assert.func(getVerifier, 'options.getVerifier');
64
+ assert.number(maxChainLength, 'options.maxChainLength');
65
+ assert.number(maxClockSkew, 'options.maxClockSkew');
66
+ assert.number(maxDelegationTtl, 'options.maxDelegationTtl');
58
67
  assert.optionalFunc(inspectCapabilityChain, 'options.inspectCapabilityChain');
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;
68
- }
68
+ assert.func(suiteFactory, 'options.suiteFactory');
69
69
 
70
70
  return [
71
71
  helpers.createExpectationMiddleware({
72
- expectedHost, expectedAction, getExpectedAction, getExpectedTarget,
73
- getExpectedRootCapabilityId, onError
72
+ getExpectedValues, onError
74
73
  }),
75
74
  authorizeZcapInvocationAfterParse({
76
- documentLoader, getRootController, suiteFactory,
77
- allowTargetAttenuation, inspectCapabilityChain, onError
75
+ allowTargetAttenuation, documentLoader, getRootController, getVerifier,
76
+ inspectCapabilityChain,
77
+ maxChainLength, maxClockSkew, maxDelegationTtl,
78
+ onError, suiteFactory
78
79
  })
79
80
  ];
80
81
  }
81
82
 
82
83
  export function authorizeZcapInvocationAfterParse({
83
- documentLoader, getRootController, suiteFactory,
84
- allowTargetAttenuation = true, inspectCapabilityChain, onError
84
+ allowTargetAttenuation = true,
85
+ documentLoader, getRootController, getVerifier, inspectCapabilityChain,
86
+ maxChainLength = 10,
87
+ // 300 second clock skew permitted by default
88
+ maxClockSkew = 300,
89
+ // 90 day max TTL by default
90
+ maxDelegationTtl = 1000 * 60 * 60 * 24 * 90,
91
+ onError, suiteFactory
85
92
  } = {}) {
86
93
  return asyncHandler(async (req, res, next) => {
87
94
  const {
@@ -89,30 +96,6 @@ export function authorizeZcapInvocationAfterParse({
89
96
  signature: {params: {keyId}}
90
97
  } = req.ezcap;
91
98
 
92
- // retrieves the root capability that was invoked
93
- async function getInvokedCapability({id}) {
94
- let rootCapabilityId;
95
- if(Array.isArray(expectedRootCapability)) {
96
- rootCapabilityId = expectedRootCapability.find(_id => id === _id);
97
- } else if(id === expectedRootCapability) {
98
- rootCapabilityId = expectedRootCapability;
99
- }
100
- if(!rootCapabilityId) {
101
- const error = new Error(
102
- `The given capability "${id}" is not an expected root ` +
103
- `capability "${expectedRootCapability}".`);
104
- error.details = {
105
- actual: id,
106
- expected: expectedRootCapability,
107
- };
108
- return helpers.handleError({error, onError});
109
- }
110
- return helpers.getRootCapability({
111
- getRootController, req, expectedHost, expectedTarget,
112
- expectedAction, rootCapabilityId
113
- });
114
- }
115
-
116
99
  // perform the capability invocation...
117
100
  // `originalUrl` must be used to support nested express routers
118
101
  const {originalUrl: url, method, headers} = req;
@@ -122,27 +105,28 @@ export function authorizeZcapInvocationAfterParse({
122
105
  suite: await suiteFactory({req}),
123
106
  headers,
124
107
  expectedHost,
125
- documentLoader: helpers.wrappedDocumentLoader({
126
- documentLoader,
127
- expectedAction,
128
- expectedHost,
129
- expectedTarget,
130
- getRootController,
131
- req,
108
+ documentLoader: helpers.createRootCapabilityLoader({
109
+ documentLoader, getRootController, req
132
110
  }),
133
- getInvokedCapability,
134
- expectedTarget,
111
+ getVerifier,
135
112
  expectedAction,
113
+ expectedTarget,
136
114
  expectedRootCapability,
137
115
  inspectCapabilityChain,
138
116
  keyId,
139
- allowTargetAttenuation
117
+ allowTargetAttenuation,
118
+ maxChainLength,
119
+ maxClockSkew,
120
+ maxDelegationTtl
140
121
  });
141
122
 
142
123
  // return HTTP 403 if verification fails
143
124
  if(!result.verified) {
144
- helpers.handleError({error: result.error, onError, throwError: false});
145
- return res.status(403).send();
125
+ res.status(403);
126
+ helpers.handleError({
127
+ res, error: result.error, onError, throwError: false
128
+ });
129
+ return res.send();
146
130
  }
147
131
 
148
132
  // provide zcap verification results if verification succeeds
package/lib/helpers.js CHANGED
@@ -1,39 +1,43 @@
1
1
  /*!
2
- * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
+ import * as helpers from './helpers.js';
4
5
  import assert from 'assert-plus';
5
6
  import asyncHandler from 'express-async-handler';
6
- import {constants as zCapConstants} from '@digitalbazaar/zcapld';
7
- import * as helpers from './helpers.js';
7
+ import {
8
+ createRootCapability,
9
+ constants as zcapConstants
10
+ } from '@digitalbazaar/zcap';
8
11
  import {parseSignatureHeader} from 'http-signature-header';
9
12
  import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
10
13
 
11
- const {ZCAP_CONTEXT_URL} = zCapConstants;
14
+ export const {ZCAP_ROOT_PREFIX} = zcapConstants;
12
15
 
13
- export const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
16
+ const DEFAULT_ACTION_FOR_METHOD = new Map([
17
+ ['GET', 'read'],
18
+ ['HEAD', 'read'],
19
+ ['OPTIONS', 'read'],
20
+ ['POST', 'write'],
21
+ ['PUT', 'write'],
22
+ ['PATCH', 'write'],
23
+ ['DELETE', 'write'],
24
+ ['CONNECT', 'write'],
25
+ ['TRACE', 'write'],
26
+ ['PATCH', 'write']
27
+ ]);
14
28
 
15
29
  // middleware used to collect expected values for zcap authorization
16
30
  export function createExpectationMiddleware({
17
- expectedHost, getExpectedTarget,
18
- expectedAction, getExpectedAction,
19
- getExpectedRootCapabilityId, onError
31
+ getExpectedValues, onError
20
32
  }) {
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');
33
+ assert.func(getExpectedValues, 'options.getExpectedValues');
27
34
  assert.optionalFunc(onError, 'options.onError');
28
35
 
29
- if(getExpectedAction && expectedAction !== undefined) {
30
- throw new Error('Use "getExpectedAction" or "expectedAction", not both.');
31
- }
32
-
33
36
  return asyncHandler(async (req, res, next) => {
34
37
  // cache ezcap express info
35
- req.ezcap = {expectedHost};
38
+ req.ezcap = {};
36
39
 
40
+ // parse signature header for zcap invocation
37
41
  const {headers} = req;
38
42
  try {
39
43
  const {params} = parseSignatureHeader(headers.authorization);
@@ -42,7 +46,8 @@ export function createExpectationMiddleware({
42
46
  const error = new Error('Missing or invalid "authorization" header.');
43
47
  error.name = 'DataError';
44
48
  error.cause = e;
45
- return helpers.handleError({error, onError});
49
+ error.httpStatusCode = 400;
50
+ return helpers.handleError({res, error, onError});
46
51
  }
47
52
 
48
53
  // if body is present, ensure header digest value matches digest of body
@@ -53,7 +58,7 @@ export function createExpectationMiddleware({
53
58
  'A "digest" header must be present when an HTTP body is present.');
54
59
  error.name = 'DataError';
55
60
  error.httpStatusCode = 400;
56
- return helpers.handleError({error, onError});
61
+ return helpers.handleError({res, error, onError});
57
62
  }
58
63
  const {verified} = await verifyHeaderValue({
59
64
  data: req.body, headerValue: expectedDigest});
@@ -62,55 +67,67 @@ export function createExpectationMiddleware({
62
67
  'The "digest" header value does not match digest of body.');
63
68
  error.name = 'DataError';
64
69
  error.httpStatusCode = 400;
65
- return helpers.handleError({error, onError});
70
+ return helpers.handleError({res, error, onError});
66
71
  }
67
72
  } else {
68
73
  // prevent any unhandled `req.body` from being erroneously used
69
74
  req.body = undefined;
70
75
  }
71
76
 
77
+ // get all expected values
78
+ let expected;
72
79
  try {
73
- // getExpectedTarget may throw an error
74
- req.ezcap.expectedTarget = await helpers.getExpectedTarget(
75
- {req, getExpectedTarget});
80
+ // `getExpectedValues` may throw
81
+ expected = await getExpectedValues({req});
82
+ _checkExpectedValues({expected});
76
83
  } catch(error) {
77
- return helpers.handleError({error, onError});
84
+ return helpers.handleError({res, error, onError});
78
85
  }
79
86
 
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});
87
+ // default expected target is always the full request URL
88
+ if(expected.target === undefined) {
89
+ expected.target = `https://${expected.host}${req.originalUrl}`;
86
90
  }
87
91
 
92
+ // get default expected action
88
93
  /* Note: This is safe as long as the server's request handling
89
94
  infrastructure differentiates based on HTTP method (as is typical practice
90
- with express/connect routing. So, while the client specifies the HTTP
95
+ with express/connect routing). So, while the client specifies the HTTP
91
96
  method, the server specifies the handler for that HTTP method. For example,
92
97
  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
98
+ must be invoking a zcap that grants "write" action authority. Then,
99
+ provided that the server's router ensures that only the "POST" handler will
100
+ be executed (typical routing practice), all is well. If the handler code is
96
101
  chosen via some other means, e.g., via the request body, then the caller
97
102
  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';
103
+ if(expected.action === undefined) {
104
+ expected.action = DEFAULT_ACTION_FOR_METHOD.get(req.method);
105
+ if(expected.action === undefined) {
106
+ const error = new Error(
107
+ `The HTTP method ${req.method} has no expected capability action.`);
108
+ error.name = 'NotSupportedError';
109
+ error.httpStatusCode = 400;
110
+ return helpers.handleError({res, error, onError});
111
+ }
100
112
  }
101
113
 
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});
114
+ // produce expected root capability from expected root invocation target
115
+ let expectedRootCapability;
116
+ const {rootInvocationTarget} = expected;
117
+ if(Array.isArray(rootInvocationTarget)) {
118
+ expectedRootCapability = rootInvocationTarget.map(
119
+ t => `${ZCAP_ROOT_PREFIX}${encodeURIComponent(t)}`);
120
+ } else {
121
+ expectedRootCapability =
122
+ `${ZCAP_ROOT_PREFIX}${encodeURIComponent(rootInvocationTarget)}`;
112
123
  }
113
124
 
125
+ // save expected values
126
+ req.ezcap.expectedAction = expected.action;
127
+ req.ezcap.expectedHost = expected.host;
128
+ req.ezcap.expectedRootCapability = expectedRootCapability;
129
+ req.ezcap.expectedTarget = expected.target;
130
+
114
131
  // call `next` on the next tick to ensure the promise from this function
115
132
  // resolves and does not reject because some subsequent middleware throws
116
133
  // an error
@@ -118,7 +135,12 @@ export function createExpectationMiddleware({
118
135
  });
119
136
  }
120
137
 
121
- export function handleError({error, onError, throwError = true}) {
138
+ export function handleError({res, error, onError, throwError = true}) {
139
+ if(error.httpStatusCode) {
140
+ res.status(error.httpStatusCode);
141
+ } else if(res.status < 400) {
142
+ res.status(500);
143
+ }
122
144
  if(onError) {
123
145
  return onError({error});
124
146
  }
@@ -127,73 +149,36 @@ export function handleError({error, onError, throwError = true}) {
127
149
  }
128
150
  }
129
151
 
130
- export function wrappedDocumentLoader({
131
- req, documentLoader, expectedHost, expectedTarget, expectedAction,
132
- getRootController
152
+ export function createRootCapabilityLoader({
153
+ documentLoader, getRootController, req
133
154
  }) {
134
- return async url => {
155
+ return async function rootCapabilityLoader(...args) {
156
+ const [url] = args;
135
157
  if(url.startsWith(ZCAP_ROOT_PREFIX)) {
136
158
  const document = await getRootCapability({
137
- getRootController, req, expectedHost, expectedTarget, expectedAction,
138
- rootCapabilityId: url
159
+ getRootController, req, rootCapabilityId: url
139
160
  });
140
-
141
161
  return {
142
162
  contextUrl: null,
143
163
  documentUrl: url,
144
164
  document,
145
165
  };
146
166
  }
147
-
148
- return documentLoader(url);
167
+ return documentLoader(...args);
149
168
  };
150
169
  }
151
170
 
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
171
  export async function getRootCapability({
182
- getRootController, req, expectedHost, expectedTarget, expectedAction,
183
- rootCapabilityId
172
+ getRootController, req, rootCapabilityId
184
173
  }) {
185
174
  const rootInvocationTarget = decodeURIComponent(
186
175
  rootCapabilityId.substr(ZCAP_ROOT_PREFIX.length));
187
176
  const controller = await getRootController({
188
- req, expectedHost, expectedTarget, expectedAction,
189
- rootCapabilityId, rootInvocationTarget
177
+ req, rootCapabilityId, rootInvocationTarget
178
+ });
179
+ return createRootCapability({
180
+ controller, invocationTarget: rootInvocationTarget
190
181
  });
191
- return {
192
- '@context': ZCAP_CONTEXT_URL,
193
- id: rootCapabilityId,
194
- invocationTarget: rootInvocationTarget,
195
- controller
196
- };
197
182
  }
198
183
 
199
184
  export function hasBody({req}) {
@@ -203,3 +188,82 @@ export function hasBody({req}) {
203
188
  (req.get('transfer-encoding') !== undefined ||
204
189
  req.get('content-length') !== undefined);
205
190
  }
191
+
192
+ function _checkExpectedValues({expected}) {
193
+ if(!(expected && typeof expected === 'object')) {
194
+ throw new TypeError('"getExpectedValues" must return an object.');
195
+ }
196
+
197
+ const {action, host, rootInvocationTarget, target} = expected;
198
+
199
+ // expected `action` is optional
200
+ if(!(action === undefined || typeof action === 'string')) {
201
+ throw new TypeError('Expected "action" must be a string.');
202
+ }
203
+
204
+ // expected `host` is required
205
+ if(typeof host !== 'string') {
206
+ throw new TypeError('Expected "host" must be a string.');
207
+ }
208
+
209
+ // expected `rootInvocationTarget` is required
210
+ if(!_checkExpectedRootInvocationTarget({rootInvocationTarget})) {
211
+ throw new Error(
212
+ 'Expected "rootInvocationTarget" must be a string or an array of ' +
213
+ 'strings, each of which expresses an absolute URI.');
214
+ }
215
+
216
+ // expected `target` is optional
217
+ if(target !== undefined && !(typeof target === 'string') &&
218
+ target.includes(':')) {
219
+ throw new Error(
220
+ 'Expected "target" must be a string that expresses an absolute ' +
221
+ 'URI.');
222
+ }
223
+ }
224
+
225
+ function _checkExpectedRootInvocationTarget({rootInvocationTarget}) {
226
+ // must be a string or an array of strings each of which represents an
227
+ // absolute URI
228
+ if(typeof rootInvocationTarget === 'string') {
229
+ return rootInvocationTarget.includes(':');
230
+ }
231
+ if(Array.isArray(rootInvocationTarget) && rootInvocationTarget.length > 0) {
232
+ return rootInvocationTarget.every(
233
+ s => typeof s === 'string' && s.includes(':'));
234
+ }
235
+ return false;
236
+ }
237
+
238
+ // documentation typedefs
239
+
240
+ /**
241
+ * A function for returning expected values when checking a zcap invocation.
242
+ *
243
+ * @typedef {Function} GetExpectedValues
244
+ * @param {object} options - The options passed to the function.
245
+ * @param {object} options.req - The express request.
246
+ * @returns {ExpectedValues} - The expected values.
247
+ */
248
+
249
+ /**
250
+ * The expected values for checking a zcap invocation performed via an HTTP
251
+ * request.
252
+ *
253
+ * @typedef {object} ExpectedValues
254
+ * @property {string} [action] - The expected capability action; if no action
255
+ * is specified during an invocation check, then a default action will be
256
+ * determined based on the HTTP method from the request -- which is only safe
257
+ * provided that the handler code path is also determined based on the HTTP
258
+ * method in the request (i.e., typical method-based express/connect
259
+ * routing); if the handler code path is determined by some other means,
260
+ * e.g., the request body, then `action` MUST be set.
261
+ * @property {string} host - The expected host in the request header.
262
+ * @property {string|Array} rootInvocationTarget - The expected invocation
263
+ * target for every acceptable root capability; each string must express an
264
+ * absolute URI.
265
+ * @property {string} [target] - The expected invocation target; if no target
266
+ * is specified during an invocation check, then the target will default to
267
+ * the absolute URL computed from the relative request URL and expected host
268
+ * value.
269
+ */
package/lib/revoke.js CHANGED
@@ -1,96 +1,211 @@
1
1
  /*!
2
- * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
3
3
  */
4
- import assert from 'assert-plus';
5
- import asyncHandler from 'express-async-handler';
6
- import {CapabilityDelegation} from '@digitalbazaar/zcapld';
7
4
  import * as helpers from './helpers.js';
8
5
  import * as jsigs from 'jsonld-signatures';
6
+ import assert from 'assert-plus';
7
+ import asyncHandler from 'express-async-handler';
9
8
  import {authorizeZcapInvocationAfterParse} from './authorize.js';
9
+ import {CapabilityDelegation} from '@digitalbazaar/zcap';
10
10
 
11
11
  /**
12
12
  * Authorizes a request to submit a zcap revocation.
13
13
  *
14
+ * This middleware is opinionated; it MUST be attached to an endpoint that
15
+ * terminates in `/revocations/:revocationId`. This to enable the middleware to
16
+ * automatically generate expected values for running zcap checks and to
17
+ * support a common, conventional revocation API pattern.
18
+ *
19
+ * The pattern is in support of controlled objects on a service, aka
20
+ * "service objects". Each object's controller is used to populate the root
21
+ * zcap for the object's controller field. This root zcap has an invocation
22
+ * target that matches the URL for the service object, aka its
23
+ * "serviceObjectId".
24
+ *
25
+ * Therefore, any route that matches an invocation target for a root zcap for
26
+ * a service SHOULD attach this middleware to:
27
+ *
28
+ * `<serviceObjectId>/revocations/:revocationId`.
29
+ *
30
+ * This middleware will compute `serviceObjectId` by combining the expected
31
+ * host with the subpath from the request URL that occurs before
32
+ * `/revocations/`. It assumes that the request URL will have this pattern
33
+ * if the middleware code has been reached. IOW, `serviceObjectId` will
34
+ * be set using:
35
+ *
36
+ * `https://<expectedHost>/<URL subpath before "/revocations/">`.
37
+ *
38
+ * Note: This middleware does NOT support having `/revocations/` appear
39
+ * multiple places in the request URL.
40
+ *
41
+ * Attaching this middleware will enable any zcaps delegated from the service
42
+ * object's root zcap to be revoked without having to issue an additional zcap
43
+ * to use the revocation endpoint. This middleware makes that possible by
44
+ * supporting the invocation of a dynamically generated root zcap with an
45
+ * invocation target of:
46
+ *
47
+ * `<serviceObjectId>/revocations/:revocationId`.
48
+ *
49
+ * This middleware will set the `controller` of this root zcap to all
50
+ * controllers in the to-be-revoked zcap's delegation chain, permitting any
51
+ * participant to revoke it. An error will be thrown prior to populating this
52
+ * `controller` field if the root zcap in the to-be-revoked zcap's chain does
53
+ * not have `<serviceObjectId>` as its invocation target (or a prefix of it).
54
+ * This ensures that the only zcaps that have been delegated from a root zcap
55
+ * using the service object's ID as part of its invocation target can be
56
+ * revoked at its `/revocations` route, i.e., other zcaps intended for other
57
+ * service objects -- or entirely other services -- cannot be revoked via this
58
+ * middleware.
59
+ *
60
+ * This middleware will automatically generate two sets of expects values: one
61
+ * for checking the invocation to revoke a capability and one for verifying the
62
+ * delegation chain of the capability that is to be revoked. Only the expected
63
+ * host value can and must be given as a parameter.
64
+ *
65
+ * The expected values for checking the capability invocation will be:
66
+ *
67
+ * host: `<expectedHost>`,
68
+ * rootInvocationTarget: [
69
+ * // root zcap with this target, RZ1, can be delegated w/target attenuation
70
+ * // to allow delegates to revoke any zcap, Z1, with RZ1 as the root in its
71
+ * // chain, even if the delegate is not a controller in Z1's chain
72
+ * `<serviceObjectId>`,
73
+ * // root zcap that this target, RZ2, can be used to revoke a zcap, Z2,
74
+ * // with an "id" of `revocationId`; RZ2's controller will be populated
75
+ * // using all controllers from Z2's chain, enabling any controller in that
76
+ * // zcap's chain to invoke RZ2 to revoke Z2
77
+ * `<serviceObjectId>/revocations/<revocationId>`,
78
+ * ],
79
+ * action: 'write'
80
+ * .
81
+ *
14
82
  * @param {object} options - Options hashmap.
15
83
  * @param {object} options.documentLoader - Document loader used to load
16
84
  * 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.
85
+ * @param {string} options.expectedHost - The expected host header value
86
+ * when checking the zcap invocation.
87
+ * @param {Function} options.getRootController - Used to get the controller
88
+ * of the root capability for the service object.
89
+ * @param {Function<Promise>} options.getVerifier - An async function to
90
+ * call to get a verifier and verification method for the key ID.
30
91
  * @param {Function} [options.inspectCapabilityChain] - A function that can
31
- * inspect a capability chain, e.g., to check for revocations.
92
+ * inspect a capability chain, e.g., to check for revocations; it will be
93
+ * used when verifying the invocation and the delegation chain for the
94
+ * to-be-revoked capability.
32
95
  * @param {Function} [options.onError] - An error handler handler for
33
96
  * customizable error handling.
97
+ * @param {object} options.suiteFactory - A factory for creating the
98
+ * supported suite(s) to use when verifying zcap delegation chains; this is
99
+ * different from `getVerifier` which is used to produce a verifier for
100
+ * verifying HTTP signatures used to invoke zcaps.
34
101
  *
35
102
  * @returns {Function} Returns an Express.js style middleware route handler.
36
103
  */
37
104
  export function authorizeZcapRevocation({
38
- documentLoader, expectedHost, getExpectedTarget, getRootController,
39
- allowTargetAttenuation = true, getExpectedRootCapabilityId,
105
+ documentLoader, expectedHost, getRootController, getVerifier,
40
106
  inspectCapabilityChain, onError, suiteFactory
41
107
  }) {
42
- assert.func(suiteFactory, 'options.suiteFactory');
108
+ // other middleware created below checks other params
109
+ assert.string(expectedHost, 'options.expectedHost');
110
+
111
+ /* Note: Here we wrap `getRootController` to support the aforementioned
112
+ zcap-specific root zcap. This will be used for checking both the invocation
113
+ and the revocation, though the revocation has an additional check below to
114
+ ensure that the submitted revocation's chain has a root zcap with an
115
+ acceptable invocation target. See the note below in
116
+ `getRevocationRootController`. */
117
+ getRootController = _wrapGetRootController({expectedHost, getRootController});
118
+
119
+ // computes expected values for the invocation
120
+ async function getExpectedValues({req}) {
121
+ const serviceObjectId = _parseServiceObjectId({req, expectedHost});
122
+ const {revocationId} = req.params;
123
+ return {
124
+ host: expectedHost,
125
+ rootInvocationTarget: [
126
+ serviceObjectId,
127
+ `${serviceObjectId}/revocations/${encodeURIComponent(revocationId)}`
128
+ ]
129
+ };
130
+ }
131
+
132
+ async function getRevocationRootController(
133
+ {req, rootCapabilityId, rootInvocationTarget}) {
134
+ /* Note: This check prevents the client from successfully submitting
135
+ revocations for unrelated service objects or services that could then use
136
+ update storage in a revocation database. */
137
+ const serviceObjectId = _parseServiceObjectId({req, expectedHost});
138
+ if(!(rootInvocationTarget === serviceObjectId ||
139
+ rootInvocationTarget.startsWith(`${serviceObjectId}/`))) {
140
+ const error = new Error(
141
+ `The root capability from the revocation's delegation chain must ` +
142
+ `have an invocation target that starts with "${serviceObjectId}".`);
143
+ error.name = 'NotAllowedError';
144
+ error.httpStatusCode = 403;
145
+ throw error;
146
+ }
147
+ return getRootController({req, rootCapabilityId, rootInvocationTarget});
148
+ }
43
149
 
44
- // expected action is always `write` for submitting a revocation
45
- const expectedAction = 'write';
46
150
  return [
47
- helpers.createExpectationMiddleware({
48
- expectedHost, expectedAction, getExpectedTarget,
49
- getExpectedRootCapabilityId, onError
151
+ asyncHandler(async function(req, res, next) {
152
+ // ensure middleware is attached to opinionated route
153
+ if(!req.originalUrl.includes('/revocations/') ||
154
+ !req.params.revocationId) {
155
+ const error = new Error(
156
+ 'Revocation middleware must be attached to a route ending in ' +
157
+ '"/revocations/:revocationId".');
158
+ error.httpStatusCode = 500;
159
+ return helpers.handleError({res, error, onError});
160
+ }
161
+ // proceed to next middleware on next tick to prevent subsequent
162
+ // middleware from potentially throwing here
163
+ process.nextTick(next);
50
164
  }),
51
- verifyCapabilityDelegation({
52
- documentLoader, getRootController, suiteFactory, inspectCapabilityChain,
53
- onError
165
+ helpers.createExpectationMiddleware({getExpectedValues, onError}),
166
+ createCheckRevocationMiddleware({
167
+ documentLoader, getRootController: getRevocationRootController,
168
+ inspectCapabilityChain, onError, suiteFactory
54
169
  }),
55
170
  authorizeZcapInvocationAfterParse({
56
- documentLoader, getRootController: _getRevocationRootController,
57
- suiteFactory, allowTargetAttenuation, inspectCapabilityChain, onError
171
+ // target attenuation is always allowed on this endpoint
172
+ allowTargetAttenuation: true,
173
+ documentLoader, getRootController, getVerifier,
174
+ inspectCapabilityChain, onError, suiteFactory
58
175
  })
59
176
  ];
60
177
  }
61
178
 
62
- function verifyCapabilityDelegation({
63
- documentLoader, getRootController, inspectCapabilityChain, suiteFactory,
64
- onError
179
+ function createCheckRevocationMiddleware({
180
+ documentLoader, getRootController, inspectCapabilityChain,
181
+ onError, suiteFactory
65
182
  }) {
66
- return asyncHandler(async function getDelegator(req, res, next) {
67
- const {
68
- expectedAction, expectedHost, expectedTarget, expectedRootCapability
69
- } = req.ezcap;
70
-
183
+ return asyncHandler(async function verifyRevocation(req, res, next) {
71
184
  const {body: capability} = req;
72
185
 
73
- // early-disallow revocation of root zcaps that follow ID convention
186
+ // early-disallow revocation of root zcaps
74
187
  if(capability.id.startsWith(helpers.ZCAP_ROOT_PREFIX)) {
75
188
  const error = new Error('A root capability cannot be revoked.');
76
189
  error.name = 'NotAllowedError';
77
- return helpers.handleError({error, onError});
190
+ error.httpStatusCode = 400;
191
+ return helpers.handleError({res, error, onError});
78
192
  }
79
193
 
80
194
  // verify CapabilityDelegation
81
195
  let delegator;
196
+ const capture = {};
82
197
  const chainControllers = [];
83
198
  try {
84
199
  const results = await _verifyDelegation({
200
+ req,
85
201
  capability,
86
- documentLoader: helpers.wrappedDocumentLoader({
87
- req, documentLoader, expectedHost, expectedTarget, expectedAction,
88
- getRootController
202
+ documentLoader: helpers.createRootCapabilityLoader({
203
+ documentLoader, getRootController, req
89
204
  }),
90
- expectedRootCapability,
91
205
  inspectCapabilityChain: _captureChainControllers({
92
206
  inspectCapabilityChain,
93
- chainControllers
207
+ chainControllers,
208
+ capture
94
209
  }),
95
210
  suiteFactory
96
211
  });
@@ -100,10 +215,12 @@ function verifyCapabilityDelegation({
100
215
  const error = new Error('The provided capability delegation is invalid.');
101
216
  error.name = 'DataError';
102
217
  error.cause = e;
103
- return helpers.handleError({error, onError});
218
+ error.httpStatusCode = 400;
219
+ return helpers.handleError({res, error, onError});
104
220
  }
105
221
 
106
- req.zcapRevocation = {delegator, chainControllers};
222
+ const {capabilityChain} = capture;
223
+ req.zcapRevocation = {delegator, capabilityChain, chainControllers};
107
224
 
108
225
  // proceed to next middleware on next tick to prevent subsequent
109
226
  // middleware from potentially throwing here
@@ -112,18 +229,30 @@ function verifyCapabilityDelegation({
112
229
  }
113
230
 
114
231
  async function _verifyDelegation({
115
- req, capability, documentLoader,
116
- expectedRootCapability, inspectCapabilityChain, suiteFactory
232
+ req, capability, documentLoader, inspectCapabilityChain, suiteFactory
117
233
  }) {
234
+ // the expected values for the invocation are the same as those for checking
235
+ // the revocation delegation chain per the reasoning given in notes above
236
+ const {expectedRootCapability} = req.ezcap;
237
+ /* Note: We build the `expectedRootCapability` for the revoked capability
238
+ from the capability invocation expected values here. This is ok because the
239
+ revocation middleware feature presumes that the only zcaps that may be
240
+ revoked using it are rooted in the same authority... FIXME */
118
241
  const {verified, error, results} = await jsigs.verify(capability, {
119
- suite: await suiteFactory({req}),
242
+ documentLoader,
120
243
  purpose: new CapabilityDelegation({
244
+ /* Note: Path-based target attenuation must always be true to support the
245
+ convention described above. This is not a security problem even if the
246
+ to-be-revoked zcap cannot be invoked (because the invocation endpoint
247
+ doesn't allow such attenuation). It just means zcaps that can be
248
+ delegated with attenuation rules that aren't supported by the invocation
249
+ endpoint can still be revoked. */
121
250
  allowTargetAttenuation: true,
122
251
  expectedRootCapability,
123
252
  inspectCapabilityChain,
124
253
  suite: await suiteFactory({req})
125
254
  }),
126
- documentLoader
255
+ suite: await suiteFactory({req})
127
256
  });
128
257
  if(!verified) {
129
258
  throw error;
@@ -131,47 +260,61 @@ async function _verifyDelegation({
131
260
  return results;
132
261
  }
133
262
 
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
- }
263
+ function _wrapGetRootController({expectedHost, getRootController}) {
264
+ return async function _getRootController({
265
+ req, rootCapabilityId, rootInvocationTarget
266
+ }) {
267
+ const serviceObjectId = _parseServiceObjectId({req, expectedHost});
268
+ const {revocationId} = req.params;
269
+ const zcapSpecificRootTarget =
270
+ `${serviceObjectId}/revocations/${encodeURIComponent(revocationId)}`;
143
271
 
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.
272
+ // if `rootInvocationTarget` doesn't match the zcap-specific root
273
+ // invocation target, then use user-provided `getRootController` to provide
274
+ // the controller
275
+ if(rootInvocationTarget !== zcapSpecificRootTarget) {
276
+ return getRootController({req, rootCapabilityId, rootInvocationTarget});
277
+ }
147
278
 
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.
279
+ /* Note: If the root invocation target is a zcap-specific revocation
280
+ endpoint, we use all zcap controllers from the to-be-revoked zcap's chain
281
+ as the root controller. This applies to populating the controller for the
282
+ root zcap in the invoked zcap's chain and for the root zcap in the
283
+ to-be-revoked zcap's chain.
152
284
 
153
- To be clear, if the delegation chain is:
285
+ This approach allows any party that has delegated a zcap or received one
286
+ (where the root zcap includes `serviceObjectId` as a prefix in its
287
+ invocation target) to be able to send it for revocation. Other code
288
+ (in the revocation route handler) will confirm that the delegation is
289
+ proper and the zcap from which it was delegated has not itself been
290
+ revoked.
154
291
 
155
- root -> A -> B
292
+ As an example, if the delegation chain is:
156
293
 
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`.
294
+ root -> A -> B
161
295
 
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. */
296
+ Any zcap controller in the chain of B may invoke a root zcap with an
297
+ `invocationTarget` of `<baseUrl>/revocations/<ID of B>` (and an ID of
298
+ `urn:zcap:root:encodeURIComponent(<baseUrl>/revocations/<ID of B>)`). This
299
+ means that `root`, `A`, or `B` may revoke `B`.
165
300
 
166
- // use all `chainControllers`
167
- // presumes `verifyCapabilityDelegation` middleware already called
168
- return req.zcapRevocation.chainControllers;
301
+ As long no other zcap in the chain of `B` (e.g., `A`) has already been
302
+ revoked, then `B` will be revoked and stored as a revocation (storage must
303
+ be done via custom code after this middleware) until `B` expires. */
304
+
305
+ // use all `chainControllers`
306
+ // presumes `verifyCapabilityDelegation` middleware already called
307
+ return req.zcapRevocation.chainControllers;
308
+ };
169
309
  }
170
310
 
171
- function _captureChainControllers({inspectCapabilityChain, chainControllers}) {
311
+ function _captureChainControllers({
312
+ inspectCapabilityChain, chainControllers, capture
313
+ }) {
172
314
  return async function _inspectCapabilityChain(chainDetails) {
173
315
  // collect every controller in the chain
174
316
  const {capabilityChain} = chainDetails;
317
+ capture.capabilityChain = capabilityChain;
175
318
  for(const capability of capabilityChain.values()) {
176
319
  chainControllers.push(..._getCapabilityControllers({capability}));
177
320
  }
@@ -180,10 +323,13 @@ function _captureChainControllers({inspectCapabilityChain, chainControllers}) {
180
323
  }
181
324
 
182
325
  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];
326
+ const {controller} = capability;
327
+ return Array.isArray(controller) ? controller : [controller];
328
+ }
329
+
330
+ function _parseServiceObjectId({req, expectedHost}) {
331
+ // `serviceObjectId` is full URL prior to `/revocations/`
332
+ const idx = req.originalUrl.indexOf('/revocations/');
333
+ const path = req.originalUrl.substring(0, idx);
334
+ return `https://${expectedHost}${path}`;
189
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/ezcap-express",
3
- "version": "4.3.0",
3
+ "version": "5.0.0",
4
4
  "main": "lib",
5
5
  "module": "main.js",
6
6
  "repository": {
@@ -16,27 +16,42 @@
16
16
  "generate-readme": "jsdoc2md -t readme-template.hbs lib/*.js > README.md",
17
17
  "lint": "eslint .",
18
18
  "test": "npm run test-node",
19
- "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js"
19
+ "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js",
20
+ "coverage": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcov --reporter=text-summary npm test",
21
+ "coverage-ci": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcovonly npm test",
22
+ "coverage-report": "nyc report"
20
23
  },
21
24
  "dependencies": {
22
- "@digitalbazaar/ed25519-signature-2020": "^3.0.0",
23
25
  "@digitalbazaar/http-digest-header": "^1.0.1",
24
- "@digitalbazaar/zcapld": "^5.1.2",
26
+ "@digitalbazaar/zcap": "^7.0.0",
25
27
  "assert-plus": "^1.0.0",
26
28
  "esm": "^3.2.25",
27
29
  "express-async-handler": "^1.1.4",
28
30
  "http-signature-header": "^2.0.2",
29
- "http-signature-zcap-verify": "^8.1.1",
31
+ "http-signature-zcap-verify": "^9.0.0",
30
32
  "jsonld-signatures": "^9.3.0"
31
33
  },
32
34
  "devDependencies": {
35
+ "@digitalbazaar/ed25519-signature-2020": "^3.0.0",
36
+ "@digitalbazaar/ed25519-verification-key-2020": "^3.2.0",
37
+ "@digitalbazaar/did-method-key": "^2.0.0",
38
+ "@digitalbazaar/ezcap": "^2.0.0",
39
+ "@digitalbazaar/http-client": "^2.0.1",
40
+ "@digitalbazaar/security-document-loader": "^1.1.1",
41
+ "bnid": "^2.1.0",
33
42
  "chai": "^4.2.0",
43
+ "chai-http": "^4.3.0",
34
44
  "cross-env": "^7.0.2",
45
+ "crypto-ld": "^6.0.0",
35
46
  "eslint": "^7.30.0",
36
47
  "eslint-config-digitalbazaar": "^2.6.1",
37
48
  "eslint-plugin-jsdoc": "^32.2.0",
49
+ "express": "^4.17.1",
50
+ "http-signature-zcap-invoke": "^4.0.0",
38
51
  "jsdoc-to-markdown": "^6.0.1",
39
- "mocha": "^8.1.3"
52
+ "mocha": "^8.1.3",
53
+ "nyc": "^15.1.0",
54
+ "zcap-context": "^1.2.1"
40
55
  },
41
56
  "engines": {
42
57
  "node": ">=14.0.0"