@bedrock/kms-http 14.1.0 → 14.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # bedrock-kms-http ChangeLog
2
2
 
3
+ ## 14.3.0 - 2022-05-16
4
+
5
+ ### Added
6
+ - Add debug logging for keystore config cache busting.
7
+
8
+ ## 14.2.1 - 2022-05-13
9
+
10
+ ### Fixed
11
+ - Fix cache busting for invoked delegated zcap cases.
12
+
13
+ ## 14.2.0 - 2022-05-13
14
+
15
+ ### Changed
16
+ - Be more resilient to keystore config controller changes. When a keystore
17
+ is new and KMS operations are performed on it that could cause false-positive
18
+ fail authz errors, fetch a fresh version of the config to ensure that the
19
+ controller has not changed.
20
+
3
21
  ## 14.1.0 - 2022-05-12
4
22
 
5
23
  ### Added
@@ -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 {config: keystoreConfig} = await keystores.get({id});
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
@@ -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 {
@@ -26,6 +27,8 @@ const {Ed25519VerificationKey2020} =
26
27
 
27
28
  const {config, util: {BedrockError}} = bedrock;
28
29
 
30
+ const FIVE_MINUTES = 1000 * 60 * 5;
31
+
29
32
  const storage = new BedrockKeystoreConfigStorage();
30
33
 
31
34
  // create `getVerifier` hook for verifying zcap invocation HTTP signatures
@@ -39,8 +42,11 @@ export function createGetKeystoreConfig({routes}) {
39
42
  if(!req.webkms) {
40
43
  const keystoreId = helpers.getKeystoreId(
41
44
  {req, localId: req.params.keystoreId, routes});
42
- const keystore = await storage.get({id: keystoreId, req});
43
- req.webkms = {keystore};
45
+ const configRecord = await storage.get({
46
+ id: keystoreId, req, returnRecord: true
47
+ });
48
+ const {config: keystore} = configRecord;
49
+ req.webkms = {keystore, configRecord};
44
50
  }
45
51
  next();
46
52
  });
@@ -81,10 +87,7 @@ export function authorizeKeystoreZcapInvocation() {
81
87
  rootInvocationTarget: req.webkms.keystore.id
82
88
  };
83
89
  },
84
- async getRootController({req}) {
85
- // this will always be present based on where this middleware is used
86
- return req.webkms.keystore.controller;
87
- }
90
+ getRootController: getKeystoreController,
88
91
  });
89
92
  }
90
93
 
@@ -93,10 +96,7 @@ export function authorizeZcapRevocation() {
93
96
  return _authorizeZcapRevocation({
94
97
  documentLoader,
95
98
  expectedHost: config.server.host,
96
- async getRootController({req}) {
97
- // this will always be present based on where this middleware is used
98
- return req.webkms.keystore.controller;
99
- },
99
+ getRootController: getKeystoreController,
100
100
  getVerifier,
101
101
  inspectCapabilityChain,
102
102
  onError,
@@ -104,6 +104,94 @@ export function authorizeZcapRevocation() {
104
104
  });
105
105
  }
106
106
 
107
+ async function getKeystoreController({req}) {
108
+ /* The following code is to prevent false-negative authz errors that are the
109
+ result of stale cached keystore configs.
110
+
111
+ It is a common pattern for keystore configs to have their root controllers
112
+ updated immediately after creation based on the value of a key that is in the
113
+ keystore itself. When this happens, decentralized caches may hold a stale
114
+ config with the old controller value. A client that uses a zcap that is
115
+ delegated / controlled by the root controller would see a false negative
116
+ authz error if the authz check used the cached value.
117
+
118
+ Therefore, this code will check whether keystore configs are reasonably new
119
+ and whether a failure is likely to occur on account of a mismatched root
120
+ controller. If this seems likely, a fresh copy of the keystore config record
121
+ is retrieved.
122
+
123
+ Algorithm: If the keystore config is less than five minutes old, then inspect
124
+ `req.ezcap.invocationParameters.capability` or `req.ezcap.capabilityToRevoke`
125
+ to ensure that:
126
+
127
+ 1. The `capability` is the root zcap and the ID or the controller of the
128
+ invoking verification method matches the keystore controller (note: can't
129
+ revoke a root zcap so this is an invoke-only case).
130
+ 2. The `capability` (being invoked or revoked) is delegated and the
131
+ ID or controller of the delegating verification method for the first
132
+ delegated zcap in the chain matches the keystore controller.
133
+
134
+ If neither condition is true, fetch a fresh version of the keystore config
135
+ record and update `req.webkms`. */
136
+ const now = Date.now();
137
+ if((now - req.webkms.configRecord.meta.created) > FIVE_MINUTES ||
138
+ (req.ezcap.capabilityToRevoke && req.ezcap.invocationParameters)) {
139
+ // config is either older than five minutes or we've already been through
140
+ // this code path for the current request to run the checks against a
141
+ // zcap that is to be revoked (in which case we must not update again to
142
+ // ensure the keystore config used is consistent)
143
+ return req.webkms.keystore.controller;
144
+ }
145
+
146
+ // check for a zcap from the invocation params or for one to be revoked; the
147
+ // same rules above will be used to check if we need a fresh config record
148
+ const capability = req.ezcap?.invocationParameters?.capability ||
149
+ req.ezcap.capabilityToRevoke;
150
+
151
+ // capability is root if it is a string; and it must be invoked because you
152
+ // can't revoke a root zcap
153
+ const isRoot = typeof capability === 'string';
154
+ let keyId;
155
+ if(isRoot && req.ezcap.invocationParameters) {
156
+ // get verification method (key) ID from invocation params
157
+ ({signature: {params: {keyId}}} = req.ezcap);
158
+ } else {
159
+ // get verification method (key) ID from first delegation proof
160
+ if(capability?.proof?.capabilityChain.length === 1) {
161
+ // capability *is* the delegated zcap
162
+ keyId = capability.proof?.verificationMethod;
163
+ } else {
164
+ // capability is delegated further down the chain
165
+ keyId = capability?.proof?.capabilityChain[1]?.proof?.verificationMethod;
166
+ }
167
+ }
168
+
169
+ if(!keyId) {
170
+ logger.debug(
171
+ 'No verification method found in zcap when trying to bust keystore ' +
172
+ 'config cache.');
173
+
174
+ // no verification method to check, use cached keystore controller
175
+ return req.webkms.keystore.controller;
176
+ }
177
+
178
+ // fetch verification method early to compare against keystore controller
179
+ const {verificationMethod} = await getVerifier({keyId, documentLoader});
180
+
181
+ // get fresh record if VM ID nor controller matches keystore controller
182
+ if(!(verificationMethod.controller === req.webkms.keystore.controller ||
183
+ verificationMethod.id === req.webkms.keystore.controller)) {
184
+ const configRecord = await storage.get({
185
+ id: req.webkms.keystore.id, req, returnRecord: true, fresh: true
186
+ });
187
+ const {config: keystore} = configRecord;
188
+ req.webkms = {keystore, configRecord};
189
+ logger.debug('Forced fresh keystore config retrieval during zcap check.');
190
+ }
191
+
192
+ return req.webkms.keystore.controller;
193
+ }
194
+
107
195
  // hook used to verify zcap invocation HTTP signatures
108
196
  async function getVerifier({keyId, documentLoader}) {
109
197
  const key = await cryptoLd.fromKeyId({id: keyId, documentLoader});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/kms-http",
3
- "version": "14.1.0",
3
+ "version": "14.3.0",
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.0.0",
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.0.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,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
  });