@bedrock/kms-http 14.2.0 → 14.3.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 +17 -0
- package/lib/http.js +2 -0
- package/lib/middleware.js +42 -24
- package/package.json +3 -3
- package/test/mocha/11-keystore-kms-client.js +176 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# bedrock-kms-http ChangeLog
|
|
2
2
|
|
|
3
|
+
## 14.3.1 - 2022-05-16
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Use `@digitalbazaar/webkms-switch@10.2` to ensure that pre-cached `webkms`
|
|
7
|
+
request meta data is not overwritten and that a custom `getRootController`
|
|
8
|
+
function is used (to enable cache busting on KMS operations).
|
|
9
|
+
|
|
10
|
+
## 14.3.0 - 2022-05-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add debug logging for keystore config cache busting.
|
|
14
|
+
|
|
15
|
+
## 14.2.1 - 2022-05-13
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fix cache busting for invoked delegated zcap cases.
|
|
19
|
+
|
|
3
20
|
## 14.2.0 - 2022-05-13
|
|
4
21
|
|
|
5
22
|
### Changed
|
package/lib/http.js
CHANGED
|
@@ -191,6 +191,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
|
|
|
191
191
|
app.post(
|
|
192
192
|
routes.keys,
|
|
193
193
|
cors(),
|
|
194
|
+
getKeystoreConfig,
|
|
194
195
|
handleOperation);
|
|
195
196
|
|
|
196
197
|
// invoke KMS operation on an existing key
|
|
@@ -198,6 +199,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
|
|
|
198
199
|
app.post(
|
|
199
200
|
routes.key,
|
|
200
201
|
cors(),
|
|
202
|
+
getKeystoreConfig,
|
|
201
203
|
handleOperation);
|
|
202
204
|
|
|
203
205
|
// TODO: consider whether this should be exposed w/o authorization
|
package/lib/middleware.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
defaultDocumentLoader as documentLoader,
|
|
12
12
|
defaultModuleManager as moduleManager
|
|
13
13
|
} from '@bedrock/kms';
|
|
14
|
+
import {logger} from './logger.js';
|
|
14
15
|
import {reportOperationUsage} from './metering.js';
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
const {
|
|
@@ -45,7 +46,7 @@ export function createGetKeystoreConfig({routes}) {
|
|
|
45
46
|
id: keystoreId, req, returnRecord: true
|
|
46
47
|
});
|
|
47
48
|
const {config: keystore} = configRecord;
|
|
48
|
-
req.webkms = {keystore, configRecord};
|
|
49
|
+
req.webkms = {...req.webkms, keystore, configRecord};
|
|
49
50
|
}
|
|
50
51
|
next();
|
|
51
52
|
});
|
|
@@ -57,7 +58,10 @@ export function createKmsOperationMiddleware() {
|
|
|
57
58
|
return createOperationMiddleware({
|
|
58
59
|
...kmsOperationOptions,
|
|
59
60
|
storage, moduleManager, documentLoader,
|
|
60
|
-
expectedHost: config.server.host,
|
|
61
|
+
expectedHost: config.server.host,
|
|
62
|
+
// override `getRootController` to provide cache busting capability
|
|
63
|
+
getRootController: getKeystoreController,
|
|
64
|
+
getVerifier,
|
|
61
65
|
inspectCapabilityChain,
|
|
62
66
|
onError, onSuccess: reportOperationUsage, suiteFactory
|
|
63
67
|
});
|
|
@@ -120,12 +124,15 @@ async function getKeystoreController({req}) {
|
|
|
120
124
|
is retrieved.
|
|
121
125
|
|
|
122
126
|
Algorithm: If the keystore config is less than five minutes old, then inspect
|
|
123
|
-
`req.ezcap.invocationParameters.capability`
|
|
127
|
+
`req.ezcap.invocationParameters.capability` or `req.ezcap.capabilityToRevoke`
|
|
128
|
+
to ensure that:
|
|
124
129
|
|
|
125
|
-
1. The `capability` is the root zcap and the
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
1. The `capability` is the root zcap and the ID or the controller of the
|
|
131
|
+
invoking verification method matches the keystore controller (note: can't
|
|
132
|
+
revoke a root zcap so this is an invoke-only case).
|
|
133
|
+
2. The `capability` (being invoked or revoked) is delegated and the
|
|
134
|
+
ID or controller of the delegating verification method for the first
|
|
135
|
+
delegated zcap in the chain matches the keystore controller.
|
|
129
136
|
|
|
130
137
|
If neither condition is true, fetch a fresh version of the keystore config
|
|
131
138
|
record and update `req.webkms`. */
|
|
@@ -144,31 +151,45 @@ async function getKeystoreController({req}) {
|
|
|
144
151
|
const capability = req.ezcap?.invocationParameters?.capability ||
|
|
145
152
|
req.ezcap.capabilityToRevoke;
|
|
146
153
|
|
|
147
|
-
let getFreshConfigRecord = false;
|
|
148
|
-
|
|
149
154
|
// capability is root if it is a string; and it must be invoked because you
|
|
150
155
|
// can't revoke a root zcap
|
|
151
156
|
const isRoot = typeof capability === 'string';
|
|
157
|
+
let keyId;
|
|
152
158
|
if(isRoot && req.ezcap.invocationParameters) {
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
const {verificationMethod} = await getVerifier({keyId, documentLoader});
|
|
156
|
-
getFreshConfigRecord =
|
|
157
|
-
!(verificationMethod.controller === req.webkms.keystore.controller ||
|
|
158
|
-
verificationMethod.id === req.webkms.keystore.controller);
|
|
159
|
+
// get verification method (key) ID from invocation params
|
|
160
|
+
({signature: {params: {keyId}}} = req.ezcap);
|
|
159
161
|
} else {
|
|
160
|
-
|
|
161
|
-
if(
|
|
162
|
-
|
|
162
|
+
// get verification method (key) ID from first delegation proof
|
|
163
|
+
if(capability?.proof?.capabilityChain.length === 1) {
|
|
164
|
+
// capability *is* the delegated zcap
|
|
165
|
+
keyId = capability.proof?.verificationMethod;
|
|
166
|
+
} else {
|
|
167
|
+
// capability is delegated further down the chain
|
|
168
|
+
keyId = capability?.proof?.capabilityChain[1]?.proof?.verificationMethod;
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
if(
|
|
172
|
+
if(!keyId) {
|
|
173
|
+
logger.debug(
|
|
174
|
+
'No verification method found in zcap when trying to bust keystore ' +
|
|
175
|
+
'config cache.');
|
|
176
|
+
|
|
177
|
+
// no verification method to check, use cached keystore controller
|
|
178
|
+
return req.webkms.keystore.controller;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// fetch verification method early to compare against keystore controller
|
|
182
|
+
const {verificationMethod} = await getVerifier({keyId, documentLoader});
|
|
183
|
+
|
|
184
|
+
// get fresh record if VM ID nor controller matches keystore controller
|
|
185
|
+
if(!(verificationMethod.controller === req.webkms.keystore.controller ||
|
|
186
|
+
verificationMethod.id === req.webkms.keystore.controller)) {
|
|
167
187
|
const configRecord = await storage.get({
|
|
168
188
|
id: req.webkms.keystore.id, req, returnRecord: true, fresh: true
|
|
169
189
|
});
|
|
170
190
|
const {config: keystore} = configRecord;
|
|
171
|
-
req.webkms = {keystore, configRecord};
|
|
191
|
+
req.webkms = {...req.webkms, keystore, configRecord};
|
|
192
|
+
logger.debug('Forced fresh keystore config retrieval during zcap check.');
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
return req.webkms.keystore.controller;
|
|
@@ -225,10 +246,7 @@ function onError({error}) {
|
|
|
225
246
|
if(!(error instanceof BedrockError)) {
|
|
226
247
|
// always expose cause message and name; expose cause details as
|
|
227
248
|
// BedrockError if error is marked public
|
|
228
|
-
|
|
229
|
-
if(error.details && error.details.public) {
|
|
230
|
-
details = error.details;
|
|
231
|
-
}
|
|
249
|
+
const details = error.details?.public ? error.details : {};
|
|
232
250
|
error = new BedrockError(
|
|
233
251
|
error.message,
|
|
234
252
|
error.name || 'NotAllowedError', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bedrock/kms-http",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bedrock KMS HTTP API",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@digitalbazaar/ed25519-signature-2020": "^3.0.0",
|
|
28
28
|
"@digitalbazaar/ed25519-verification-key-2020": "^3.2.0",
|
|
29
29
|
"@digitalbazaar/ezcap-express": "^6.2.0",
|
|
30
|
-
"@digitalbazaar/webkms-switch": "^10.
|
|
30
|
+
"@digitalbazaar/webkms-switch": "^10.2.0",
|
|
31
31
|
"cidr-regex": "^3.1.1",
|
|
32
32
|
"cors": "^2.8.5",
|
|
33
33
|
"crypto-ld": "^6.0.0",
|
|
@@ -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,177 @@ 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 and uses key 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
|
+
// generate a key for use
|
|
92
|
+
const hmacKey = await aliceKeystoreAgent.generateKey({type: 'hmac'});
|
|
93
|
+
|
|
94
|
+
// delegate zcap for updating config from agent2 to agent3 to test
|
|
95
|
+
// cache busting with delegated zcap
|
|
96
|
+
const rootCapability = ZCAP_ROOT_PREFIX + encodeURIComponent(keystoreId);
|
|
97
|
+
const agent3Zcap = await helpers.delegate({
|
|
98
|
+
parentCapability: rootCapability,
|
|
99
|
+
controller: agent3.id,
|
|
100
|
+
invocationTarget: keystoreId,
|
|
101
|
+
delegator: agent2
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// delegate zcap for updating config from agent3 to agent2 to agent1 to
|
|
105
|
+
// test cache busting with deep delegated zcap
|
|
106
|
+
const agent2Zcap = await helpers.delegate({
|
|
107
|
+
parentCapability: rootCapability,
|
|
108
|
+
controller: agent2.id,
|
|
109
|
+
invocationTarget: keystoreId,
|
|
110
|
+
delegator: agent3
|
|
111
|
+
});
|
|
112
|
+
const agent1Zcap = await helpers.delegate({
|
|
113
|
+
parentCapability: agent2Zcap,
|
|
114
|
+
controller: agent1.id,
|
|
115
|
+
invocationTarget: keystoreId,
|
|
116
|
+
delegator: agent2
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// update Alice's keystore config controller to agent1
|
|
120
|
+
let config = {...aliceKeystoreConfig};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// disable cache delete functionality on update
|
|
124
|
+
keystores._disableClearCacheOnUpdate(true);
|
|
125
|
+
|
|
126
|
+
// update controller alice => agent1 (no cache busting required)
|
|
127
|
+
{
|
|
128
|
+
config.sequence++;
|
|
129
|
+
config.controller = agent1.id;
|
|
130
|
+
const result = await aliceKeystoreAgent.updateConfig({config});
|
|
131
|
+
result.config.id.should.eql(config.id);
|
|
132
|
+
result.config.controller.should.eql(agent1.id);
|
|
133
|
+
({config} = result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// update controller agent1 => agent2 (requires cache busting by
|
|
137
|
+
// inspecting the root zcap)
|
|
138
|
+
{
|
|
139
|
+
config.sequence++;
|
|
140
|
+
config.controller = agent2.id;
|
|
141
|
+
const result = await agent1KeystoreAgent.updateConfig({config});
|
|
142
|
+
result.config.id.should.eql(config.id);
|
|
143
|
+
result.config.controller.should.eql(agent2.id);
|
|
144
|
+
({config} = result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// update controller agent2 => agent3 (requires cache busting by
|
|
148
|
+
// inspecting a delegated zcap)
|
|
149
|
+
{
|
|
150
|
+
config.sequence++;
|
|
151
|
+
config.controller = agent3.id;
|
|
152
|
+
const result = await agent3KeystoreAgent.updateConfig(
|
|
153
|
+
{capability: agent3Zcap, config});
|
|
154
|
+
result.config.id.should.eql(config.id);
|
|
155
|
+
result.config.controller.should.eql(agent3.id);
|
|
156
|
+
({config} = result);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// update controller agent3 => agent1 (requires cache busting by
|
|
160
|
+
// inspecting a deep delegated zcap)
|
|
161
|
+
{
|
|
162
|
+
config.sequence++;
|
|
163
|
+
config.controller = agent1.id;
|
|
164
|
+
const result = await agent1KeystoreAgent.updateConfig(
|
|
165
|
+
{capability: agent1Zcap, config});
|
|
166
|
+
result.config.id.should.eql(config.id);
|
|
167
|
+
result.config.controller.should.eql(agent1.id);
|
|
168
|
+
({config} = result);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// update controller back to alice to test key op cache busting
|
|
172
|
+
{
|
|
173
|
+
config.sequence++;
|
|
174
|
+
config.controller = aliceCapabilityAgent.id;
|
|
175
|
+
const result = await agent1KeystoreAgent.updateConfig({config});
|
|
176
|
+
result.config.id.should.eql(config.id);
|
|
177
|
+
result.config.controller.should.eql(aliceCapabilityAgent.id);
|
|
178
|
+
({config} = result);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// use hmac key as alice (requires cache busting by inspecting root
|
|
182
|
+
// zcap)
|
|
183
|
+
await hmacKey.sign({data: new Uint8Array([0])});
|
|
184
|
+
|
|
185
|
+
// update controller alice => agent1 (no cache busting required)
|
|
186
|
+
{
|
|
187
|
+
config.sequence++;
|
|
188
|
+
config.controller = agent1.id;
|
|
189
|
+
const result = await aliceKeystoreAgent.updateConfig({config});
|
|
190
|
+
result.config.id.should.eql(config.id);
|
|
191
|
+
result.config.controller.should.eql(agent1.id);
|
|
192
|
+
({config} = result);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// use hmac key as agent1 (requires cache busting by
|
|
196
|
+
// inspecting the root zcap)
|
|
197
|
+
hmacKey.invocationSigner = agent1.getSigner();
|
|
198
|
+
// use different data each time to avoid hmac caching
|
|
199
|
+
await hmacKey.sign({data: new Uint8Array([1])});
|
|
200
|
+
|
|
201
|
+
// update controller agent1 => agent2 (requires cache busting by
|
|
202
|
+
// inspecting the root zcap)
|
|
203
|
+
{
|
|
204
|
+
config.sequence++;
|
|
205
|
+
config.controller = agent2.id;
|
|
206
|
+
const result = await agent1KeystoreAgent.updateConfig({config});
|
|
207
|
+
result.config.id.should.eql(config.id);
|
|
208
|
+
result.config.controller.should.eql(agent2.id);
|
|
209
|
+
({config} = result);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// use hmac key as agent3 (requires cache busting by
|
|
213
|
+
// inspecting a delegated zcap)
|
|
214
|
+
hmacKey.capability = agent3Zcap;
|
|
215
|
+
hmacKey.invocationSigner = agent3.getSigner();
|
|
216
|
+
// use different data each time to avoid hmac caching
|
|
217
|
+
await hmacKey.sign({data: new Uint8Array([1])});
|
|
218
|
+
|
|
219
|
+
// update controller agent2 => agent3 (cache should already be fresh)
|
|
220
|
+
{
|
|
221
|
+
config.sequence++;
|
|
222
|
+
config.controller = agent3.id;
|
|
223
|
+
const result = await agent3KeystoreAgent.updateConfig(
|
|
224
|
+
{capability: agent3Zcap, config});
|
|
225
|
+
result.config.id.should.eql(config.id);
|
|
226
|
+
result.config.controller.should.eql(agent3.id);
|
|
227
|
+
({config} = result);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// use hmac key as agent1 (requires cache busting by
|
|
231
|
+
// inspecting a deep delegated zcap)
|
|
232
|
+
hmacKey.capability = agent1Zcap;
|
|
233
|
+
hmacKey.invocationSigner = agent1.getSigner();
|
|
234
|
+
// use different data each time to avoid hmac caching
|
|
235
|
+
await hmacKey.sign({data: new Uint8Array([1])});
|
|
236
|
+
} catch(e) {
|
|
237
|
+
assertNoError(e);
|
|
238
|
+
} finally {
|
|
239
|
+
keystores._disableClearCacheOnUpdate(false);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
66
242
|
});
|