@bedrock/kms-http 14.1.0 → 14.2.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,13 @@
1
1
  # bedrock-kms-http ChangeLog
2
2
 
3
+ ## 14.2.0 - 2022-05-13
4
+
5
+ ### Changed
6
+ - Be more resilient to keystore config controller changes. When a keystore
7
+ is new and KMS operations are performed on it that could cause false-positive
8
+ fail authz errors, fetch a fresh version of the config to ensure that the
9
+ controller has not changed.
10
+
3
11
  ## 14.1.0 - 2022-05-12
4
12
 
5
13
  ### 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
@@ -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 keystore = await storage.get({id: keystoreId, req});
43
- req.webkms = {keystore};
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
- async getRootController({req}) {
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
- async getRootController({req}) {
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,77 @@ 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` to ensure that either:
124
+
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.
129
+
130
+ If neither condition is true, fetch a fresh version of the keystore config
131
+ record and update `req.webkms`. */
132
+ const now = Date.now();
133
+ if((now - req.webkms.configRecord.meta.created) > FIVE_MINUTES ||
134
+ (req.ezcap.capabilityToRevoke && req.ezcap.invocationParameters)) {
135
+ // config is either older than five minutes or we've already been through
136
+ // this code path for the current request to run the checks against a
137
+ // zcap that is to be revoked (in which case we must not update again to
138
+ // ensure the keystore config used is consistent)
139
+ return req.webkms.keystore.controller;
140
+ }
141
+
142
+ // check for a zcap from the invocation params or for one to be revoked; the
143
+ // same rules above will be used to check if we need a fresh config record
144
+ const capability = req.ezcap?.invocationParameters?.capability ||
145
+ req.ezcap.capabilityToRevoke;
146
+
147
+ let getFreshConfigRecord = false;
148
+
149
+ // capability is root if it is a string; and it must be invoked because you
150
+ // can't revoke a root zcap
151
+ const isRoot = typeof capability === 'string';
152
+ 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
+ } else {
160
+ const second = capability?.proof?.capabilityChain[1];
161
+ if(second && second.controller !== req.webkms.keystore.controller) {
162
+ getFreshConfigRecord = true;
163
+ }
164
+ }
165
+
166
+ if(getFreshConfigRecord) {
167
+ const configRecord = await storage.get({
168
+ id: req.webkms.keystore.id, req, returnRecord: true, fresh: true
169
+ });
170
+ const {config: keystore} = configRecord;
171
+ req.webkms = {keystore, configRecord};
172
+ }
173
+
174
+ return req.webkms.keystore.controller;
175
+ }
176
+
107
177
  // hook used to verify zcap invocation HTTP signatures
108
178
  async function getVerifier({keyId, documentLoader}) {
109
179
  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.2.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.1.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"