@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 +5 -2
- package/lib/BedrockKeystoreConfigStorage.js +2 -2
- package/lib/config.js +46 -1
- package/lib/http.js +114 -3
- package/lib/middleware.js +3 -3
- package/package.json +9 -10
- package/schemas/bedrock-kms-http.js +3 -3
package/README.md
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
33
|
+
"@digitalbazaar/ezcap-express": "^7.3.0",
|
|
34
34
|
"@digitalbazaar/webkms-switch": "^12.0.0",
|
|
35
|
-
"cidr-regex": "^
|
|
36
|
-
"cors": "^2.8.
|
|
35
|
+
"cidr-regex": "^7.1.2",
|
|
36
|
+
"cors": "^2.8.6",
|
|
37
37
|
"forwarded": "^0.2.0",
|
|
38
|
-
"
|
|
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": "^
|
|
59
|
-
"eslint
|
|
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": ">=
|
|
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,
|