@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 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, getVerifier,
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` to ensure that either:
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 verification method ID or
126
- controller match the keystore controller.
127
- 2. The `capability` is delegated and the controller of the first delegated
128
- zcap in the chain is the keystore controller.
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
- // fetch verification method early to compare against keystore controller
154
- const {signature: {params: {keyId}}} = req.ezcap;
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
- const second = capability?.proof?.capabilityChain[1];
161
- if(second && second.controller !== req.webkms.keystore.controller) {
162
- getFreshConfigRecord = true;
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(getFreshConfigRecord) {
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
- let details = {};
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.2.0",
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.0.0",
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.1.0",
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
  });