@bedrock/kms-http 14.0.0 → 14.2.1
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 +18 -0
- package/lib/BedrockKeystoreConfigStorage.js +5 -4
- package/lib/middleware.js +98 -18
- package/package.json +3 -3
- package/test/mocha/11-keystore-kms-client.js +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# bedrock-kms-http ChangeLog
|
|
2
2
|
|
|
3
|
+
## 14.2.1 - 2022-05-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fix cache busting for invoked delegated zcap cases.
|
|
7
|
+
|
|
8
|
+
## 14.2.0 - 2022-05-13
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Be more resilient to keystore config controller changes. When a keystore
|
|
12
|
+
is new and KMS operations are performed on it that could cause false-positive
|
|
13
|
+
fail authz errors, fetch a fresh version of the config to ensure that the
|
|
14
|
+
controller has not changed.
|
|
15
|
+
|
|
16
|
+
## 14.1.0 - 2022-05-12
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Include full error as non-public `cause` in `onError` handler.
|
|
20
|
+
|
|
3
21
|
## 14.0.0 - 2022-04-29
|
|
4
22
|
|
|
5
23
|
### Changed
|
|
@@ -14,12 +14,13 @@ const {util: {BedrockError}} = bedrock;
|
|
|
14
14
|
import './config.js';
|
|
15
15
|
|
|
16
16
|
export class BedrockKeystoreConfigStorage extends KeystoreConfigStorage {
|
|
17
|
-
async get({id, req} = {}) {
|
|
18
|
-
const
|
|
17
|
+
async get({id, req, returnRecord = false, fresh = false} = {}) {
|
|
18
|
+
const record = await keystores.get({id, fresh});
|
|
19
|
+
const {config: keystoreConfig} = record;
|
|
19
20
|
|
|
20
21
|
// skip request checks if specifically requested
|
|
21
22
|
if(req === false) {
|
|
22
|
-
return keystoreConfig;
|
|
23
|
+
return returnRecord ? record : keystoreConfig;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// verify that request is from an IP that is allowed to access the config
|
|
@@ -32,6 +33,6 @@ export class BedrockKeystoreConfigStorage extends KeystoreConfigStorage {
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
return keystoreConfig;
|
|
36
|
+
return returnRecord ? record : keystoreConfig;
|
|
36
37
|
}
|
|
37
38
|
}
|
package/lib/middleware.js
CHANGED
|
@@ -26,6 +26,8 @@ const {Ed25519VerificationKey2020} =
|
|
|
26
26
|
|
|
27
27
|
const {config, util: {BedrockError}} = bedrock;
|
|
28
28
|
|
|
29
|
+
const FIVE_MINUTES = 1000 * 60 * 5;
|
|
30
|
+
|
|
29
31
|
const storage = new BedrockKeystoreConfigStorage();
|
|
30
32
|
|
|
31
33
|
// create `getVerifier` hook for verifying zcap invocation HTTP signatures
|
|
@@ -39,8 +41,11 @@ export function createGetKeystoreConfig({routes}) {
|
|
|
39
41
|
if(!req.webkms) {
|
|
40
42
|
const keystoreId = helpers.getKeystoreId(
|
|
41
43
|
{req, localId: req.params.keystoreId, routes});
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
+
const configRecord = await storage.get({
|
|
45
|
+
id: keystoreId, req, returnRecord: true
|
|
46
|
+
});
|
|
47
|
+
const {config: keystore} = configRecord;
|
|
48
|
+
req.webkms = {keystore, configRecord};
|
|
44
49
|
}
|
|
45
50
|
next();
|
|
46
51
|
});
|
|
@@ -81,10 +86,7 @@ export function authorizeKeystoreZcapInvocation() {
|
|
|
81
86
|
rootInvocationTarget: req.webkms.keystore.id
|
|
82
87
|
};
|
|
83
88
|
},
|
|
84
|
-
|
|
85
|
-
// this will always be present based on where this middleware is used
|
|
86
|
-
return req.webkms.keystore.controller;
|
|
87
|
-
}
|
|
89
|
+
getRootController: getKeystoreController,
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -93,10 +95,7 @@ export function authorizeZcapRevocation() {
|
|
|
93
95
|
return _authorizeZcapRevocation({
|
|
94
96
|
documentLoader,
|
|
95
97
|
expectedHost: config.server.host,
|
|
96
|
-
|
|
97
|
-
// this will always be present based on where this middleware is used
|
|
98
|
-
return req.webkms.keystore.controller;
|
|
99
|
-
},
|
|
98
|
+
getRootController: getKeystoreController,
|
|
100
99
|
getVerifier,
|
|
101
100
|
inspectCapabilityChain,
|
|
102
101
|
onError,
|
|
@@ -104,6 +103,89 @@ export function authorizeZcapRevocation() {
|
|
|
104
103
|
});
|
|
105
104
|
}
|
|
106
105
|
|
|
106
|
+
async function getKeystoreController({req}) {
|
|
107
|
+
/* The following code is to prevent false-negative authz errors that are the
|
|
108
|
+
result of stale cached keystore configs.
|
|
109
|
+
|
|
110
|
+
It is a common pattern for keystore configs to have their root controllers
|
|
111
|
+
updated immediately after creation based on the value of a key that is in the
|
|
112
|
+
keystore itself. When this happens, decentralized caches may hold a stale
|
|
113
|
+
config with the old controller value. A client that uses a zcap that is
|
|
114
|
+
delegated / controlled by the root controller would see a false negative
|
|
115
|
+
authz error if the authz check used the cached value.
|
|
116
|
+
|
|
117
|
+
Therefore, this code will check whether keystore configs are reasonably new
|
|
118
|
+
and whether a failure is likely to occur on account of a mismatched root
|
|
119
|
+
controller. If this seems likely, a fresh copy of the keystore config record
|
|
120
|
+
is retrieved.
|
|
121
|
+
|
|
122
|
+
Algorithm: If the keystore config is less than five minutes old, then inspect
|
|
123
|
+
`req.ezcap.invocationParameters.capability` or `req.ezcap.capabilityToRevoke`
|
|
124
|
+
to ensure that:
|
|
125
|
+
|
|
126
|
+
1. The `capability` is the root zcap and the ID or the controller of the
|
|
127
|
+
invoking verification method matches the keystore controller (note: can't
|
|
128
|
+
revoke a root zcap so this is an invoke-only case).
|
|
129
|
+
2. The `capability` (being invoked or revoked) is delegated and the
|
|
130
|
+
ID or controller of the delegating verification method for the first
|
|
131
|
+
delegated zcap in the chain matches the keystore controller.
|
|
132
|
+
|
|
133
|
+
If neither condition is true, fetch a fresh version of the keystore config
|
|
134
|
+
record and update `req.webkms`. */
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if((now - req.webkms.configRecord.meta.created) > FIVE_MINUTES ||
|
|
137
|
+
(req.ezcap.capabilityToRevoke && req.ezcap.invocationParameters)) {
|
|
138
|
+
// config is either older than five minutes or we've already been through
|
|
139
|
+
// this code path for the current request to run the checks against a
|
|
140
|
+
// zcap that is to be revoked (in which case we must not update again to
|
|
141
|
+
// ensure the keystore config used is consistent)
|
|
142
|
+
return req.webkms.keystore.controller;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// check for a zcap from the invocation params or for one to be revoked; the
|
|
146
|
+
// same rules above will be used to check if we need a fresh config record
|
|
147
|
+
const capability = req.ezcap?.invocationParameters?.capability ||
|
|
148
|
+
req.ezcap.capabilityToRevoke;
|
|
149
|
+
|
|
150
|
+
// capability is root if it is a string; and it must be invoked because you
|
|
151
|
+
// can't revoke a root zcap
|
|
152
|
+
const isRoot = typeof capability === 'string';
|
|
153
|
+
let keyId;
|
|
154
|
+
if(isRoot && req.ezcap.invocationParameters) {
|
|
155
|
+
// get verification method (key) ID from invocation params
|
|
156
|
+
({signature: {params: {keyId}}} = req.ezcap);
|
|
157
|
+
} else {
|
|
158
|
+
// get verification method (key) ID from first delegation proof
|
|
159
|
+
if(capability?.proof?.capabilityChain.length === 1) {
|
|
160
|
+
// capability *is* the delegated zcap
|
|
161
|
+
keyId = capability.proof?.verificationMethod;
|
|
162
|
+
} else {
|
|
163
|
+
// capability is delegated further down the chain
|
|
164
|
+
keyId = capability?.proof?.capabilityChain[1]?.proof?.verificationMethod;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if(!keyId) {
|
|
169
|
+
// no verification method to check, use cached keystore controller
|
|
170
|
+
return req.webkms.keystore.controller;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// fetch verification method early to compare against keystore controller
|
|
174
|
+
const {verificationMethod} = await getVerifier({keyId, documentLoader});
|
|
175
|
+
|
|
176
|
+
// get fresh record if VM ID nor controller matches keystore controller
|
|
177
|
+
if(!(verificationMethod.controller === req.webkms.keystore.controller ||
|
|
178
|
+
verificationMethod.id === req.webkms.keystore.controller)) {
|
|
179
|
+
const configRecord = await storage.get({
|
|
180
|
+
id: req.webkms.keystore.id, req, returnRecord: true, fresh: true
|
|
181
|
+
});
|
|
182
|
+
const {config: keystore} = configRecord;
|
|
183
|
+
req.webkms = {keystore, configRecord};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return req.webkms.keystore.controller;
|
|
187
|
+
}
|
|
188
|
+
|
|
107
189
|
// hook used to verify zcap invocation HTTP signatures
|
|
108
190
|
async function getVerifier({keyId, documentLoader}) {
|
|
109
191
|
const key = await cryptoLd.fromKeyId({id: keyId, documentLoader});
|
|
@@ -152,27 +234,25 @@ async function inspectCapabilityChain({
|
|
|
152
234
|
}
|
|
153
235
|
|
|
154
236
|
function onError({error}) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
cause = error;
|
|
159
|
-
} else {
|
|
237
|
+
if(!(error instanceof BedrockError)) {
|
|
238
|
+
// always expose cause message and name; expose cause details as
|
|
239
|
+
// BedrockError if error is marked public
|
|
160
240
|
let details = {};
|
|
161
241
|
if(error.details && error.details.public) {
|
|
162
242
|
details = error.details;
|
|
163
243
|
}
|
|
164
|
-
|
|
244
|
+
error = new BedrockError(
|
|
165
245
|
error.message,
|
|
166
246
|
error.name || 'NotAllowedError', {
|
|
167
247
|
...details,
|
|
168
248
|
public: true,
|
|
169
|
-
});
|
|
249
|
+
}, error);
|
|
170
250
|
}
|
|
171
251
|
throw new BedrockError(
|
|
172
252
|
'Authorization error.', 'NotAllowedError', {
|
|
173
253
|
httpStatusCode: 403,
|
|
174
254
|
public: true,
|
|
175
|
-
},
|
|
255
|
+
}, error);
|
|
176
256
|
}
|
|
177
257
|
|
|
178
258
|
// hook used to create suites for verifying zcap delegation chains
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/kms-http",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock KMS HTTP API",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@digitalbazaar/ed25519-signature-2020": "^3.0.0",
|
|
28
28
|
"@digitalbazaar/ed25519-verification-key-2020": "^3.2.0",
|
|
29
|
-
"@digitalbazaar/ezcap-express": "^6.
|
|
29
|
+
"@digitalbazaar/ezcap-express": "^6.2.0",
|
|
30
30
|
"@digitalbazaar/webkms-switch": "^10.0.0",
|
|
31
31
|
"cidr-regex": "^3.1.1",
|
|
32
32
|
"cors": "^2.8.5",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@bedrock/app-identity": "^3.0.0",
|
|
39
39
|
"@bedrock/core": "^6.0.0",
|
|
40
40
|
"@bedrock/express": "^8.0.0",
|
|
41
|
-
"@bedrock/kms": "^10.
|
|
41
|
+
"@bedrock/kms": "^10.2.0",
|
|
42
42
|
"@bedrock/meter-usage-reporter": "^7.0.0",
|
|
43
43
|
"@bedrock/validation": "^7.0.0",
|
|
44
44
|
"@bedrock/zcap-storage": "^7.0.0"
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as helpers from './helpers.js';
|
|
5
5
|
import {httpsAgent} from '@bedrock/https-agent';
|
|
6
|
+
import {keystores} from '@bedrock/kms';
|
|
6
7
|
import {createRequire} from 'node:module';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const {CapabilityAgent, KmsClient, KeystoreAgent} =
|
|
9
10
|
require('@digitalbazaar/webkms-client');
|
|
10
11
|
|
|
12
|
+
const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
|
|
13
|
+
|
|
11
14
|
describe('keystore API interactions using webkms-client', () => {
|
|
12
15
|
let aliceCapabilityAgent;
|
|
13
16
|
let aliceKeystoreConfig;
|
|
@@ -63,4 +66,108 @@ describe('keystore API interactions using webkms-client', () => {
|
|
|
63
66
|
err.data.details.expected.should.equal(
|
|
64
67
|
bobKeystoreAgent.keystoreId);
|
|
65
68
|
});
|
|
69
|
+
|
|
70
|
+
it('updates config controller twice proving cache busting', async () => {
|
|
71
|
+
const keystoreId = aliceKeystoreConfig.id;
|
|
72
|
+
const kmsClient = new KmsClient({httpsAgent});
|
|
73
|
+
|
|
74
|
+
// create zcap agents to take over the keystore
|
|
75
|
+
const agent1 = await CapabilityAgent.fromSecret(
|
|
76
|
+
{secret: 'bd926f4c-38e4-4c3a-9a17-0e2608ee0a01', handle: 'primary'});
|
|
77
|
+
const agent2 = await CapabilityAgent.fromSecret(
|
|
78
|
+
{secret: 'd830baa9-02a6-4bd3-9072-de256b0b6c2d', handle: 'primary'});
|
|
79
|
+
const agent3 = await CapabilityAgent.fromSecret(
|
|
80
|
+
{secret: '37224754-647e-469a-9639-193a2833c69a', handle: 'primary'});
|
|
81
|
+
|
|
82
|
+
const aliceKeystoreAgent = new KeystoreAgent(
|
|
83
|
+
{capabilityAgent: aliceCapabilityAgent, keystoreId, kmsClient});
|
|
84
|
+
const agent1KeystoreAgent = new KeystoreAgent(
|
|
85
|
+
{capabilityAgent: agent1, keystoreId, kmsClient});
|
|
86
|
+
// const agent2KeystoreAgent = new KeystoreAgent(
|
|
87
|
+
// {capabilityAgent: agent2, keystoreId, kmsClient});
|
|
88
|
+
const agent3KeystoreAgent = new KeystoreAgent(
|
|
89
|
+
{capabilityAgent: agent3, keystoreId, kmsClient});
|
|
90
|
+
|
|
91
|
+
// delegate zcap for updating config from agent2 to agent3 to test
|
|
92
|
+
// cache busting with delegated zcap
|
|
93
|
+
const rootCapability = ZCAP_ROOT_PREFIX + encodeURIComponent(keystoreId);
|
|
94
|
+
const agent3Zcap = await helpers.delegate({
|
|
95
|
+
parentCapability: rootCapability,
|
|
96
|
+
controller: agent3.id,
|
|
97
|
+
invocationTarget: keystoreId,
|
|
98
|
+
delegator: agent2
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// delegate zcap for updating config from agent3 to agent2 to agent1 to
|
|
102
|
+
// test cache busting with deep delegated zcap
|
|
103
|
+
const agent2Zcap = await helpers.delegate({
|
|
104
|
+
parentCapability: rootCapability,
|
|
105
|
+
controller: agent2.id,
|
|
106
|
+
invocationTarget: keystoreId,
|
|
107
|
+
delegator: agent3
|
|
108
|
+
});
|
|
109
|
+
const agent1Zcap = await helpers.delegate({
|
|
110
|
+
parentCapability: agent2Zcap,
|
|
111
|
+
controller: agent1.id,
|
|
112
|
+
invocationTarget: keystoreId,
|
|
113
|
+
delegator: agent2
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// update Alice's keystore config controller to agent1
|
|
117
|
+
let config = {...aliceKeystoreConfig};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// disable cache delete functionality on update
|
|
121
|
+
keystores._disableClearCacheOnUpdate(true);
|
|
122
|
+
|
|
123
|
+
// update controller alice => agent1 (no cache busting required
|
|
124
|
+
{
|
|
125
|
+
config.sequence++;
|
|
126
|
+
config.controller = agent1.id;
|
|
127
|
+
const result = await aliceKeystoreAgent.updateConfig({config});
|
|
128
|
+
result.config.id.should.eql(config.id);
|
|
129
|
+
result.config.controller.should.eql(agent1.id);
|
|
130
|
+
({config} = result);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// update controller agent1 => agent2 (requires cache busting by
|
|
134
|
+
// inspecting the root zcap)
|
|
135
|
+
{
|
|
136
|
+
config.sequence++;
|
|
137
|
+
config.controller = agent2.id;
|
|
138
|
+
const result = await agent1KeystoreAgent.updateConfig({config});
|
|
139
|
+
result.config.id.should.eql(config.id);
|
|
140
|
+
result.config.controller.should.eql(agent2.id);
|
|
141
|
+
({config} = result);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// update controller agent2 => agent3 (requires cache busting by
|
|
145
|
+
// inspecting a delegated zcap)
|
|
146
|
+
{
|
|
147
|
+
config.sequence++;
|
|
148
|
+
config.controller = agent3.id;
|
|
149
|
+
const result = await agent3KeystoreAgent.updateConfig(
|
|
150
|
+
{capability: agent3Zcap, config});
|
|
151
|
+
result.config.id.should.eql(config.id);
|
|
152
|
+
result.config.controller.should.eql(agent3.id);
|
|
153
|
+
({config} = result);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// update controller agent3 => agent1 (requires cache busting by
|
|
157
|
+
// inspecting a deep delegated zcap)
|
|
158
|
+
{
|
|
159
|
+
config.sequence++;
|
|
160
|
+
config.controller = agent1.id;
|
|
161
|
+
const result = await agent1KeystoreAgent.updateConfig(
|
|
162
|
+
{capability: agent1Zcap, config});
|
|
163
|
+
result.config.id.should.eql(config.id);
|
|
164
|
+
result.config.controller.should.eql(agent1.id);
|
|
165
|
+
({config} = result);
|
|
166
|
+
}
|
|
167
|
+
} catch(e) {
|
|
168
|
+
assertNoError(e);
|
|
169
|
+
} finally {
|
|
170
|
+
keystores._disableClearCacheOnUpdate(false);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
66
173
|
});
|