@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 +75 -0
- package/README.md +25 -15
- package/lib/authorize.js +68 -84
- package/lib/helpers.js +161 -97
- package/lib/revoke.js +233 -87
- package/package.json +21 -6
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
|
|
64
|
+
// get controller for a service object from a database
|
|
63
65
|
let controller;
|
|
64
66
|
try {
|
|
65
|
-
const record = await database.
|
|
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
|
|
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({
|
|
121
|
+
async function authorizeMyZcapInvocation({expectedAction} = {}) {
|
|
117
122
|
return authorizeZcapInvocation({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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('/
|
|
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
|
-
[
|
|
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.<string></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 {
|
|
26
|
-
*
|
|
27
|
-
* @param {
|
|
28
|
-
* expected
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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 {
|
|
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}
|
|
42
|
-
* when verifying
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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
|
-
|
|
73
|
-
getExpectedRootCapabilityId, onError
|
|
72
|
+
getExpectedValues, onError
|
|
74
73
|
}),
|
|
75
74
|
authorizeZcapInvocationAfterParse({
|
|
76
|
-
documentLoader, getRootController,
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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.
|
|
126
|
-
documentLoader,
|
|
127
|
-
expectedAction,
|
|
128
|
-
expectedHost,
|
|
129
|
-
expectedTarget,
|
|
130
|
-
getRootController,
|
|
131
|
-
req,
|
|
108
|
+
documentLoader: helpers.createRootCapabilityLoader({
|
|
109
|
+
documentLoader, getRootController, req
|
|
132
110
|
}),
|
|
133
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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 {
|
|
7
|
-
|
|
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 {
|
|
14
|
+
export const {ZCAP_ROOT_PREFIX} = zcapConstants;
|
|
12
15
|
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
expectedAction, getExpectedAction,
|
|
19
|
-
getExpectedRootCapabilityId, onError
|
|
31
|
+
getExpectedValues, onError
|
|
20
32
|
}) {
|
|
21
|
-
assert.
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
//
|
|
81
|
-
|
|
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,
|
|
94
|
-
that the server's router ensures that only the "POST" handler will
|
|
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(
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
getRootController
|
|
152
|
+
export function createRootCapabilityLoader({
|
|
153
|
+
documentLoader, getRootController, req
|
|
133
154
|
}) {
|
|
134
|
-
return async
|
|
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,
|
|
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,
|
|
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,
|
|
189
|
-
|
|
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
|
|
18
|
-
*
|
|
19
|
-
* @param {Function} options.
|
|
20
|
-
*
|
|
21
|
-
* @param {Function} options.
|
|
22
|
-
*
|
|
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,
|
|
39
|
-
allowTargetAttenuation = true, getExpectedRootCapabilityId,
|
|
105
|
+
documentLoader, expectedHost, getRootController, getVerifier,
|
|
40
106
|
inspectCapabilityChain, onError, suiteFactory
|
|
41
107
|
}) {
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
165
|
+
helpers.createExpectationMiddleware({getExpectedValues, onError}),
|
|
166
|
+
createCheckRevocationMiddleware({
|
|
167
|
+
documentLoader, getRootController: getRevocationRootController,
|
|
168
|
+
inspectCapabilityChain, onError, suiteFactory
|
|
54
169
|
}),
|
|
55
170
|
authorizeZcapInvocationAfterParse({
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
63
|
-
documentLoader, getRootController, inspectCapabilityChain,
|
|
64
|
-
onError
|
|
179
|
+
function createCheckRevocationMiddleware({
|
|
180
|
+
documentLoader, getRootController, inspectCapabilityChain,
|
|
181
|
+
onError, suiteFactory
|
|
65
182
|
}) {
|
|
66
|
-
return asyncHandler(async function
|
|
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
|
|
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
|
-
|
|
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.
|
|
87
|
-
|
|
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
|
-
|
|
218
|
+
error.httpStatusCode = 400;
|
|
219
|
+
return helpers.handleError({res, error, onError});
|
|
104
220
|
}
|
|
105
221
|
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
+
As an example, if the delegation chain is:
|
|
156
293
|
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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({
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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": "
|
|
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/
|
|
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": "^
|
|
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"
|