@bedrock/kms-http 23.1.0 → 23.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/README.md CHANGED
@@ -11,7 +11,10 @@
11
11
 
12
12
  ## Install
13
13
 
14
- - Node.js 18+ is required.
14
+ This software requires and supports maintained recent versions of Node.js.
15
+ Updates may remove support for older unmaintained platform versions. Please use
16
+ dependency version lock files and testing to ensure compatibility with this
17
+ software.
15
18
 
16
19
  ### NPM
17
20
 
@@ -36,7 +39,7 @@ npm install
36
39
  In `lib/index.js` (or `main.js`, as appropriate):
37
40
 
38
41
  ```js
39
- import 'bedrock-kms-http';
42
+ import '@bedrock/kms-http';
40
43
  ```
41
44
 
42
45
  ### KMS HTTP API
@@ -27,7 +27,7 @@ export class BedrockKeystoreConfigStorage extends KeystoreConfigStorage {
27
27
  throw new BedrockError(
28
28
  'Permission denied. Source IP is not allowed.', 'NotAllowedError', {
29
29
  httpStatusCode: 403,
30
- public: true,
30
+ public: true
31
31
  });
32
32
  }
33
33
 
@@ -51,7 +51,7 @@ export class BedrockKeystoreConfigStorage extends KeystoreConfigStorage {
51
51
  throw new BedrockError(
52
52
  'Permission denied. Source IP is not allowed.', 'NotAllowedError', {
53
53
  httpStatusCode: 403,
54
- public: true,
54
+ public: true
55
55
  });
56
56
  }
57
57
  }
package/lib/config.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /*!
2
- * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2026 Digital Bazaar, Inc.
3
3
  */
4
4
  import {config} from '@bedrock/core';
5
5
  import '@bedrock/app-identity';
6
+ import '@bedrock/express';
6
7
  import '@bedrock/meter-usage-reporter';
7
8
  import '@bedrock/validation';
8
9
 
@@ -35,3 +36,47 @@ cfg.kmsOperationOptions = {
35
36
  // 1000 year max TTL by default
36
37
  maxDelegationTtl: 1000 * 60 * 60 * 24 * 365 * 1000
37
38
  };
39
+
40
+ /* Optional proxy feature: An application running bedrock-kms-http can use
41
+ these configuration options to redirect KMS operations to another
42
+ bedrock-kms-http application, called a "proxy target". The proxy target must
43
+ shares the same database and be configured with the same public address
44
+ (domain / origin) but it is expected to be run behind a firewall (with
45
+ restricted access) and inside of an isolated environment, such as a secure
46
+ enclave, to add an additional layer of security during the processing of
47
+ KMS operations.
48
+
49
+ The proxying application will not have access to secret key material or to
50
+ secrets that can decrypt/unseal/unwrap other secrets, only the proxy target
51
+ will have access to some or all of these (or be capable of using some or all
52
+ of these via authz credentials only it can access). Both applications will
53
+ have access to the same database and will be expected to run compatible
54
+ versions of software to ensure database record compatibility. The proxy
55
+ target MUST properly check authz on all incoming requests; authz is is NOT
56
+ checked by the proxy.
57
+
58
+ Proxy entries are defined by KMS module, i.e., at most one proxy target can
59
+ be configured per KMS module. When requests for a particular KMS module arrive,
60
+ they are forwarded in their entirety to the related proxy target. */
61
+ cfg.kmsModuleProxy = {
62
+ /*
63
+ <kms-module-name>: {
64
+ proxyTarget: <address (base URL) for kms proxy target>
65
+ }
66
+ */
67
+ };
68
+
69
+ // require raw JSON payloads for proxied routes to preserve specific content
70
+ // expression as it is used in signed digests
71
+ const createRawBodyParserOptions = () => ({
72
+ raw: {
73
+ type: ['json', '+json']
74
+ }
75
+ });
76
+ const bodyParserRoutes = config.express.bodyParser.routes;
77
+ // for generating a new key
78
+ bodyParserRoutes['/kms/keystores/:keystoreId/keys'] =
79
+ createRawBodyParserOptions();
80
+ // for running a key operation on an existing key
81
+ bodyParserRoutes['/kms/keystores/:keystoreId/keys/:keyId'] =
82
+ createRawBodyParserOptions();
package/lib/http.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /*!
2
- * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved.
2
+ * Copyright (c) 2019-2026 Digital Bazaar, Inc.
3
3
  */
4
4
  import * as bedrock from '@bedrock/core';
5
5
  import * as brZCapStorage from '@bedrock/zcap-storage';
6
6
  import * as helpers from './helpers.js';
7
7
  import * as middleware from './middleware.js';
8
+ import * as zlib from 'node:zlib';
8
9
  import {
9
10
  getConfigsQuery,
10
11
  postKeystoreBody,
@@ -17,6 +18,7 @@ import {
17
18
  } from '@bedrock/kms';
18
19
  import {reportOperationUsage, SERVICE_TYPE} from './metering.js';
19
20
  import {asyncHandler} from '@bedrock/express';
21
+ import {createProxyMiddleware} from 'http-proxy-middleware';
20
22
  import {generateRandom} from '@digitalbazaar/webkms-switch';
21
23
  import {meters} from '@bedrock/meter-usage-reporter';
22
24
  import {createValidateMiddleware as validate} from '@bedrock/validation';
@@ -49,6 +51,11 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
49
51
  // associated with a request
50
52
  const getKeystoreConfig = middleware.createGetKeystoreConfig({routes});
51
53
 
54
+ // create KMS module proxy middleware for use with KMS operations
55
+ const kmsModuleProxyMiddleware = _createKmsModuleProxyMiddleware();
56
+ // create raw => json parser for routes using KMS module proxy middleware
57
+ const rawBodyToJsonMiddleware = asyncHandler(_parseRawBodyAsJson);
58
+
52
59
  /* Note: CORS is used on all endpoints. This is safe because authorization
53
60
  uses HTTP signatures + capabilities, not cookies; CSRF is not possible. */
54
61
 
@@ -96,7 +103,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
96
103
  // insufficient remaining storage
97
104
  throw new BedrockError('Permission denied.', 'NotAllowedError', {
98
105
  httpStatusCode: 403,
99
- public: true,
106
+ public: true
100
107
  });
101
108
  }
102
109
 
@@ -215,6 +222,8 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
215
222
  routes.keys,
216
223
  cors(),
217
224
  getKeystoreConfig,
225
+ kmsModuleProxyMiddleware,
226
+ rawBodyToJsonMiddleware,
218
227
  handleOperation);
219
228
 
220
229
  // invoke KMS operation on an existing key
@@ -223,6 +232,8 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
223
232
  routes.key,
224
233
  cors(),
225
234
  getKeystoreConfig,
235
+ kmsModuleProxyMiddleware,
236
+ rawBodyToJsonMiddleware,
226
237
  handleOperation);
227
238
 
228
239
  // TODO: consider whether this should be exposed w/o authorization
@@ -233,6 +244,8 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
233
244
  routes.key,
234
245
  cors(),
235
246
  getKeystoreConfig,
247
+ kmsModuleProxyMiddleware,
248
+ rawBodyToJsonMiddleware,
236
249
  asyncHandler(async (req, res) => {
237
250
  const {webkms: {keystore}} = req;
238
251
  const keyId = `${keystore.id}/keys/${req.params.keyId}`;
@@ -271,7 +284,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
271
284
  // meter is disabled, do not allow storage
272
285
  throw new BedrockError('Permission denied.', 'NotAllowedError', {
273
286
  httpStatusCode: 403,
274
- public: true,
287
+ public: true
275
288
  });
276
289
  }
277
290
 
@@ -286,3 +299,101 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
286
299
  reportOperationUsage({req});
287
300
  }));
288
301
  });
302
+
303
+ function _createKmsModuleProxyMiddleware() {
304
+ const cfg = config['kms-http'];
305
+
306
+ const kmsModuleProxyEntries = Object.entries(cfg.kmsModuleProxy ?? {});
307
+ if(kmsModuleProxyEntries.length === 0) {
308
+ // nothing to proxy; no-op
309
+ return function kmsModuleProxyMiddleware(req, res, next) {
310
+ next();
311
+ };
312
+ }
313
+
314
+ const map = new Map();
315
+ for(const [kmsModule, {proxyTarget}] of kmsModuleProxyEntries) {
316
+ map.set(kmsModule, createProxyMiddleware({
317
+ target: proxyTarget,
318
+ // do not rewrite host header
319
+ changeOrigin: false,
320
+ // match TLS certificate checking expectations w/bedrock config
321
+ secure: config['https-agent'].rejectUnauthorized,
322
+ // include x-forward* headers
323
+ xfwd: true,
324
+ // do not forward websockets
325
+ ws: false,
326
+ on: {
327
+ // required to rewrite body since it has been parsed
328
+ proxyReq: _reserializeRequestBody
329
+ }
330
+ }));
331
+ }
332
+ return function kmsModuleProxyMiddleware(req, res, next) {
333
+ const {webkms: {keystore}} = req;
334
+ const {kmsModule} = keystore;
335
+ const mw = map.get(kmsModule);
336
+ if(!mw) {
337
+ return next();
338
+ }
339
+ mw(req, res, next);
340
+ };
341
+ }
342
+
343
+ // parses raw body to json
344
+ function _parseRawBodyAsJson(req, res, next) {
345
+ try {
346
+ // special case empty body
347
+ req.body = req.body?.length > 0 ? JSON.parse(req.body.toString()) : {};
348
+ next();
349
+ } catch(cause) {
350
+ throw new BedrockError('Failed to parse expected JSON body.', {
351
+ name: 'DataError',
352
+ details: {httpStatusCode: 400, public: true},
353
+ cause
354
+ });
355
+ }
356
+ }
357
+
358
+ function _reserializeRequestBody(proxyReq, req) {
359
+ try {
360
+ if(!(req.body?.length > 0)) {
361
+ // nothing to send
362
+ return;
363
+ }
364
+
365
+ const contentType = proxyReq.getHeader('content-type');
366
+ if(!contentType) {
367
+ // nothing to send
368
+ return;
369
+ }
370
+
371
+ // reapply content-encoding
372
+ const contentEncoding = String(
373
+ proxyReq.getHeader('content-encoding')).toLowerCase();
374
+ let data = req.body;
375
+ if(contentEncoding === 'gzip') {
376
+ data = zlib.gzipSync(data);
377
+ } else if(contentEncoding === 'deflate') {
378
+ data = zlib.deflateSync(data);
379
+ } else if(contentEncoding === 'zstd') {
380
+ // note: not supported by body-parser, so unreachable
381
+ data = zlib.zstdCompressSync(data);
382
+ } else if(contentEncoding === 'br') {
383
+ // note: not supported by body-parser, so unreachable
384
+ data = zlib.brotliCompressSync(data);
385
+ } else {
386
+ proxyReq.removeHeader('content-encoding');
387
+ }
388
+
389
+ const transferEncoding = proxyReq.getHeader('transfer-encoding');
390
+ if(!transferEncoding) {
391
+ proxyReq.setHeader('content-length', Buffer.byteLength(data));
392
+ }
393
+ proxyReq.write(data);
394
+ } catch(e) {
395
+ // pass error through `proxyReq` processing channel
396
+ const error = e instanceof Error ? e : new Error(String(e));
397
+ proxyReq.destroy(error);
398
+ }
399
+ }
package/lib/middleware.js CHANGED
@@ -81,7 +81,7 @@ export function authorizeKeystoreZcapInvocation() {
81
81
  rootInvocationTarget: req.webkms.keystore.id
82
82
  };
83
83
  },
84
- getRootController: getKeystoreController,
84
+ getRootController: getKeystoreController
85
85
  });
86
86
  }
87
87
 
@@ -210,13 +210,13 @@ function onError({error}) {
210
210
  error.message,
211
211
  error.name || 'NotAllowedError', {
212
212
  ...details,
213
- public: true,
213
+ public: true
214
214
  }, error);
215
215
  }
216
216
  throw new BedrockError(
217
217
  'Authorization error.', 'NotAllowedError', {
218
218
  httpStatusCode: 403,
219
- public: true,
219
+ public: true
220
220
  }, error);
221
221
  }
222
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrock/kms-http",
3
- "version": "23.1.0",
3
+ "version": "23.3.0",
4
4
  "type": "module",
5
5
  "description": "Bedrock KMS HTTP API",
6
6
  "main": "./lib/index.js",
@@ -30,12 +30,13 @@
30
30
  "dependencies": {
31
31
  "@digitalbazaar/ed25519-multikey": "^1.3.1",
32
32
  "@digitalbazaar/ed25519-signature-2020": "^5.4.0",
33
- "@digitalbazaar/ezcap-express": "^7.1.0",
33
+ "@digitalbazaar/ezcap-express": "^7.3.0",
34
34
  "@digitalbazaar/webkms-switch": "^12.0.0",
35
- "cidr-regex": "^4.1.3",
36
- "cors": "^2.8.5",
35
+ "cidr-regex": "^7.1.2",
36
+ "cors": "^2.8.6",
37
37
  "forwarded": "^0.2.0",
38
- "ipaddr.js": "^2.2.0"
38
+ "http-proxy-middleware": "^4.2.0",
39
+ "ipaddr.js": "^2.5.0"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "@bedrock/app-identity": "^4.0.0",
@@ -55,12 +56,10 @@
55
56
  "lib": "./lib"
56
57
  },
57
58
  "devDependencies": {
58
- "eslint": "^8.57.1",
59
- "eslint-config-digitalbazaar": "^5.2.0",
60
- "eslint-plugin-jsdoc": "^50.6.3",
61
- "eslint-plugin-unicorn": "^56.0.1"
59
+ "@digitalbazaar/eslint-config": "^9.0.0",
60
+ "eslint": "^10.13.0"
62
61
  },
63
62
  "engines": {
64
- "node": ">=18"
63
+ "node": ">=22"
65
64
  }
66
65
  }
@@ -92,7 +92,7 @@ const delegatedZcap = {
92
92
  proofValue: {
93
93
  title: 'proofValue',
94
94
  type: 'string'
95
- },
95
+ }
96
96
  }
97
97
  }
98
98
  }
@@ -109,7 +109,7 @@ const ipAllowList = {
109
109
  items: {
110
110
  type: 'string',
111
111
  // leading and trailing slashes in regex must be removed
112
- pattern: cidrRegex({exact: true}).toString().slice(1, -1),
112
+ pattern: cidrRegex({exact: true}).toString().slice(1, -1)
113
113
  }
114
114
  };
115
115
 
@@ -146,7 +146,7 @@ const updateKeystoreConfigBody = {
146
146
  required: [
147
147
  'controller',
148
148
  'id',
149
- 'sequence',
149
+ 'sequence'
150
150
  ],
151
151
  properties: {
152
152
  controller,