@bedrock/kms-http 13.0.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/.eslintrc.cjs +12 -0
- package/.github/workflows/main.yml +103 -0
- package/CHANGELOG.md +332 -0
- package/LICENSE.md +115 -0
- package/README.md +292 -0
- package/lib/BedrockKeystoreConfigStorage.js +37 -0
- package/lib/config.js +37 -0
- package/lib/helpers.js +36 -0
- package/lib/http.js +263 -0
- package/lib/index.js +9 -0
- package/lib/logger.js +6 -0
- package/lib/metering.js +42 -0
- package/lib/middleware.js +181 -0
- package/package.json +58 -0
- package/schemas/bedrock-kms-http.js +176 -0
- package/test/mocha/.eslintrc +9 -0
- package/test/mocha/10-keystore.js +670 -0
- package/test/mocha/11-keystore-kms-client.js +66 -0
- package/test/mocha/20-revocation.js +545 -0
- package/test/mocha/21-revocation-ip-allow-list.js +143 -0
- package/test/mocha/30-operations-generate-key.js +180 -0
- package/test/mocha/31-operations-hmac.js +178 -0
- package/test/mocha/helpers.js +163 -0
- package/test/mocha/mock.data.js +40 -0
- package/test/package.json +64 -0
- package/test/test.config.js +36 -0
- package/test/test.js +31 -0
- package/test/web/.eslintrc +9 -0
- package/test/web/10-api.js +108 -0
- package/test/web/helpers.js +84 -0
package/lib/metering.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as brZCapStorage from '@bedrock/zcap-storage';
|
|
6
|
+
import {
|
|
7
|
+
defaultModuleManager as moduleManager,
|
|
8
|
+
keystores
|
|
9
|
+
} from '@bedrock/kms';
|
|
10
|
+
import {meters} from '@bedrock/meter-usage-reporter';
|
|
11
|
+
import {logger} from './logger.js';
|
|
12
|
+
|
|
13
|
+
// configure usage aggregator for webkms meters
|
|
14
|
+
export const SERVICE_TYPE = 'webkms';
|
|
15
|
+
meters.setAggregator({serviceType: SERVICE_TYPE, handler: _aggregateUsage});
|
|
16
|
+
|
|
17
|
+
export function reportOperationUsage({req}) {
|
|
18
|
+
// do not wait for usage to be reported
|
|
19
|
+
const {keystore, keystore: {meterId: id}} = req.webkms;
|
|
20
|
+
meters.use({id, operations: 1}).catch(
|
|
21
|
+
error => logger.error(
|
|
22
|
+
`Keystore (${keystore.id}) meter (${id}) usage error.`, {error}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function _aggregateUsage({meter, signal} = {}) {
|
|
26
|
+
const {id: meterId} = meter;
|
|
27
|
+
return keystores.getStorageUsage({
|
|
28
|
+
meterId, moduleManager, aggregate: _addRevocationUsage, signal
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function _addRevocationUsage({config, usage}) {
|
|
33
|
+
// add storage units for revocations associated with the keystore
|
|
34
|
+
const {id: keystoreId} = config;
|
|
35
|
+
const {storageCost} = bedrock.config.kms;
|
|
36
|
+
// if `count` is available, use it to count stored revocations
|
|
37
|
+
if(brZCapStorage.revocations.count) {
|
|
38
|
+
const {count} = await brZCapStorage.revocations.count(
|
|
39
|
+
{rootTarget: keystoreId});
|
|
40
|
+
usage.storage += count * storageCost.revocation;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import * as bedrock from '@bedrock/core';
|
|
5
|
+
import * as brZCapStorage from '@bedrock/zcap-storage';
|
|
6
|
+
import * as helpers from './helpers.js';
|
|
7
|
+
import {asyncHandler} from '@bedrock/express';
|
|
8
|
+
import {BedrockKeystoreConfigStorage} from './BedrockKeystoreConfigStorage.js';
|
|
9
|
+
import {createRequire} from 'module';
|
|
10
|
+
import {
|
|
11
|
+
defaultDocumentLoader as documentLoader,
|
|
12
|
+
defaultModuleManager as moduleManager
|
|
13
|
+
} from '@bedrock/kms';
|
|
14
|
+
import {reportOperationUsage} from './metering.js';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const {
|
|
17
|
+
authorizeZcapInvocation: _authorizeZcapInvocation,
|
|
18
|
+
authorizeZcapRevocation: _authorizeZcapRevocation
|
|
19
|
+
} = require('@digitalbazaar/ezcap-express');
|
|
20
|
+
const {createOperationMiddleware} = require('@digitalbazaar/webkms-switch');
|
|
21
|
+
const {CryptoLD} = require('crypto-ld');
|
|
22
|
+
const {Ed25519Signature2020} =
|
|
23
|
+
require('@digitalbazaar/ed25519-signature-2020');
|
|
24
|
+
const {Ed25519VerificationKey2020} =
|
|
25
|
+
require('@digitalbazaar/ed25519-verification-key-2020');
|
|
26
|
+
|
|
27
|
+
const {config, util: {BedrockError}} = bedrock;
|
|
28
|
+
|
|
29
|
+
const storage = new BedrockKeystoreConfigStorage();
|
|
30
|
+
|
|
31
|
+
// create `getVerifier` hook for verifying zcap invocation HTTP signatures
|
|
32
|
+
const cryptoLd = new CryptoLD();
|
|
33
|
+
cryptoLd.use(Ed25519VerificationKey2020);
|
|
34
|
+
|
|
35
|
+
// creates middleware to get the keystore config for the current request and
|
|
36
|
+
// caches it in `req.webkms.keystore`
|
|
37
|
+
export function createGetKeystoreConfig({routes}) {
|
|
38
|
+
return asyncHandler(async function _getKeystoreConfig(req, res, next) {
|
|
39
|
+
if(!req.webkms) {
|
|
40
|
+
const keystoreId = helpers.getKeystoreId(
|
|
41
|
+
{req, localId: req.params.keystoreId, routes});
|
|
42
|
+
const keystore = await storage.get({id: keystoreId, req});
|
|
43
|
+
req.webkms = {keystore};
|
|
44
|
+
}
|
|
45
|
+
next();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// creates middleware for handling KMS operations
|
|
50
|
+
export function createKmsOperationMiddleware() {
|
|
51
|
+
const {kmsOperationOptions} = config['kms-http'];
|
|
52
|
+
return createOperationMiddleware({
|
|
53
|
+
...kmsOperationOptions,
|
|
54
|
+
storage, moduleManager, documentLoader,
|
|
55
|
+
expectedHost: config.server.host, getVerifier,
|
|
56
|
+
inspectCapabilityChain,
|
|
57
|
+
onError, onSuccess: reportOperationUsage, suiteFactory
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// calls ezcap-express's authorizeZcapInvocation w/constant params, exposing
|
|
62
|
+
// only those params that change in this module
|
|
63
|
+
export function authorizeZcapInvocation({
|
|
64
|
+
getExpectedValues, getRootController
|
|
65
|
+
}) {
|
|
66
|
+
return _authorizeZcapInvocation({
|
|
67
|
+
documentLoader, getExpectedValues, getRootController,
|
|
68
|
+
getVerifier,
|
|
69
|
+
inspectCapabilityChain,
|
|
70
|
+
onError,
|
|
71
|
+
suiteFactory
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// creates middleware for keystore route authz checks
|
|
76
|
+
export function authorizeKeystoreZcapInvocation() {
|
|
77
|
+
return authorizeZcapInvocation({
|
|
78
|
+
async getExpectedValues({req}) {
|
|
79
|
+
return {
|
|
80
|
+
host: config.server.host,
|
|
81
|
+
rootInvocationTarget: req.webkms.keystore.id
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
async getRootController({req}) {
|
|
85
|
+
// this will always be present based on where this middleware is used
|
|
86
|
+
return req.webkms.keystore.controller;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// creates middleware for revocation of zcaps for keystores
|
|
92
|
+
export function authorizeZcapRevocation() {
|
|
93
|
+
return _authorizeZcapRevocation({
|
|
94
|
+
documentLoader,
|
|
95
|
+
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
|
+
},
|
|
100
|
+
getVerifier,
|
|
101
|
+
inspectCapabilityChain,
|
|
102
|
+
onError,
|
|
103
|
+
suiteFactory
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// hook used to verify zcap invocation HTTP signatures
|
|
108
|
+
async function getVerifier({keyId, documentLoader}) {
|
|
109
|
+
const key = await cryptoLd.fromKeyId({id: keyId, documentLoader});
|
|
110
|
+
const verificationMethod = await key.export(
|
|
111
|
+
{publicKey: true, includeContext: true});
|
|
112
|
+
const verifier = key.verifier();
|
|
113
|
+
return {verifier, verificationMethod};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function inspectCapabilityChain({
|
|
117
|
+
capabilityChain, capabilityChainMeta
|
|
118
|
+
}) {
|
|
119
|
+
// if capability chain has only root, there's nothing to check as root
|
|
120
|
+
// zcaps cannot be revoked
|
|
121
|
+
if(capabilityChain.length === 1) {
|
|
122
|
+
return {valid: true};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// collect capability IDs and delegators for all delegated capabilities in
|
|
126
|
+
// chain (skip root) so they can be checked for revocation
|
|
127
|
+
const capabilities = [];
|
|
128
|
+
for(const [i, capability] of capabilityChain.entries()) {
|
|
129
|
+
// skip root zcap, it cannot be revoked
|
|
130
|
+
if(i === 0) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const [{purposeResult}] = capabilityChainMeta[i].verifyResult.results;
|
|
134
|
+
if(purposeResult && purposeResult.delegator) {
|
|
135
|
+
capabilities.push({
|
|
136
|
+
capabilityId: capability.id,
|
|
137
|
+
delegator: purposeResult.delegator.id,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const revoked = await brZCapStorage.revocations.isRevoked({capabilities});
|
|
143
|
+
if(revoked) {
|
|
144
|
+
return {
|
|
145
|
+
valid: false,
|
|
146
|
+
error: new Error(
|
|
147
|
+
'One or more capabilities in the chain have been revoked.')
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {valid: true};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onError({error}) {
|
|
155
|
+
// cause must be a public BedrockError to be surfaced to the HTTP client
|
|
156
|
+
let cause;
|
|
157
|
+
if(error instanceof BedrockError) {
|
|
158
|
+
cause = error;
|
|
159
|
+
} else {
|
|
160
|
+
let details = {};
|
|
161
|
+
if(error.details && error.details.public) {
|
|
162
|
+
details = error.details;
|
|
163
|
+
}
|
|
164
|
+
cause = new BedrockError(
|
|
165
|
+
error.message,
|
|
166
|
+
error.name || 'NotAllowedError', {
|
|
167
|
+
...details,
|
|
168
|
+
public: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
throw new BedrockError(
|
|
172
|
+
'Authorization error.', 'NotAllowedError', {
|
|
173
|
+
httpStatusCode: 403,
|
|
174
|
+
public: true,
|
|
175
|
+
}, cause);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// hook used to create suites for verifying zcap delegation chains
|
|
179
|
+
async function suiteFactory() {
|
|
180
|
+
return new Ed25519Signature2020();
|
|
181
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bedrock/kms-http",
|
|
3
|
+
"version": "13.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Bedrock KMS HTTP API",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "eslint ."
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/digitalbazaar/bedrock-kms-http"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bedrock"
|
|
16
|
+
],
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Digital Bazaar, Inc.",
|
|
19
|
+
"email": "support@digitalbazaar.com",
|
|
20
|
+
"url": "https://digitalbazaar.com"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/digitalbazaar/bedrock-kms-http/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/digitalbazaar/bedrock-kms-http",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@digitalbazaar/ed25519-signature-2020": "^3.0.0",
|
|
28
|
+
"@digitalbazaar/ed25519-verification-key-2020": "^3.2.0",
|
|
29
|
+
"@digitalbazaar/ezcap-express": "^6.0.0",
|
|
30
|
+
"@digitalbazaar/webkms-switch": "^10.0.0",
|
|
31
|
+
"cidr-regex": "^3.1.1",
|
|
32
|
+
"cors": "^2.8.5",
|
|
33
|
+
"crypto-ld": "^6.0.0",
|
|
34
|
+
"forwarded": "^0.2.0",
|
|
35
|
+
"netmask": "^2.0.2"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@bedrock/core": "^5.0.0",
|
|
39
|
+
"@bedrock/app-identity": "^2.0.0",
|
|
40
|
+
"@bedrock/express": "^7.0.0",
|
|
41
|
+
"@bedrock/kms": "^9.0.0",
|
|
42
|
+
"@bedrock/meter-usage-reporter": "^6.0.0",
|
|
43
|
+
"@bedrock/validation": "^6.0.0",
|
|
44
|
+
"@bedrock/zcap-storage": "^6.0.0"
|
|
45
|
+
},
|
|
46
|
+
"directories": {
|
|
47
|
+
"lib": "./lib"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"eslint": "^7.32.0",
|
|
51
|
+
"eslint-config-digitalbazaar": "^2.8.0",
|
|
52
|
+
"eslint-plugin-jsdoc": "^37.9.7",
|
|
53
|
+
"jsdoc-to-markdown": "^7.1.1"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=14"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import cidrRegex from 'cidr-regex';
|
|
5
|
+
|
|
6
|
+
const controller = {
|
|
7
|
+
title: 'controller',
|
|
8
|
+
type: 'string'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const id = {
|
|
12
|
+
title: 'id',
|
|
13
|
+
type: 'string'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const referenceId = {
|
|
17
|
+
title: 'referenceId',
|
|
18
|
+
type: 'string'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const delegatedZcap = {
|
|
22
|
+
title: 'delegatedZcap',
|
|
23
|
+
type: 'object',
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
required: [
|
|
26
|
+
'@context', 'controller', 'expires', 'id', 'invocationTarget',
|
|
27
|
+
'parentCapability', 'proof'
|
|
28
|
+
],
|
|
29
|
+
properties: {
|
|
30
|
+
controller,
|
|
31
|
+
id,
|
|
32
|
+
allowedAction: {
|
|
33
|
+
anyOf: [{
|
|
34
|
+
type: 'string'
|
|
35
|
+
}, {
|
|
36
|
+
type: 'array',
|
|
37
|
+
minItems: 1,
|
|
38
|
+
items: {type: 'string'}
|
|
39
|
+
}]
|
|
40
|
+
},
|
|
41
|
+
expires: {
|
|
42
|
+
// FIXME: w3c datetime
|
|
43
|
+
title: 'expires',
|
|
44
|
+
type: 'string'
|
|
45
|
+
},
|
|
46
|
+
'@context': {
|
|
47
|
+
title: '@context',
|
|
48
|
+
anyOf: [{
|
|
49
|
+
type: 'string'
|
|
50
|
+
}, {
|
|
51
|
+
type: 'array',
|
|
52
|
+
minItems: 1,
|
|
53
|
+
items: {type: 'string'}
|
|
54
|
+
}]
|
|
55
|
+
},
|
|
56
|
+
invocationTarget: {
|
|
57
|
+
title: 'Invocation Target',
|
|
58
|
+
type: 'string'
|
|
59
|
+
},
|
|
60
|
+
parentCapability: {
|
|
61
|
+
title: 'Parent Capability',
|
|
62
|
+
type: 'string'
|
|
63
|
+
},
|
|
64
|
+
proof: {
|
|
65
|
+
title: 'Proof',
|
|
66
|
+
type: 'object',
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
required: [
|
|
69
|
+
'verificationMethod', 'type', 'created', 'proofPurpose',
|
|
70
|
+
'capabilityChain', 'proofValue'
|
|
71
|
+
],
|
|
72
|
+
properties: {
|
|
73
|
+
verificationMethod: {
|
|
74
|
+
title: 'verificationMethod',
|
|
75
|
+
type: 'string'
|
|
76
|
+
},
|
|
77
|
+
type: {
|
|
78
|
+
title: 'type',
|
|
79
|
+
type: 'string'
|
|
80
|
+
},
|
|
81
|
+
created: {
|
|
82
|
+
title: 'created',
|
|
83
|
+
type: 'string'
|
|
84
|
+
},
|
|
85
|
+
proofPurpose: {
|
|
86
|
+
title: 'proofPurpose',
|
|
87
|
+
type: 'string'
|
|
88
|
+
},
|
|
89
|
+
capabilityChain: {
|
|
90
|
+
title: 'capabilityChain',
|
|
91
|
+
type: 'array',
|
|
92
|
+
minItems: 1,
|
|
93
|
+
items: {
|
|
94
|
+
type: ['string', 'object']
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
proofValue: {
|
|
98
|
+
title: 'proofValue',
|
|
99
|
+
type: 'string'
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const meterId = {
|
|
107
|
+
title: 'Meter ID',
|
|
108
|
+
type: 'string'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const ipAllowList = {
|
|
112
|
+
type: 'array',
|
|
113
|
+
minItems: 1,
|
|
114
|
+
items: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
// leading and trailing slashes in regex must be removed
|
|
117
|
+
pattern: cidrRegex.v4({exact: true}).toString().slice(1, -1),
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sequence = {
|
|
122
|
+
title: 'sequence',
|
|
123
|
+
type: 'integer',
|
|
124
|
+
minimum: 0,
|
|
125
|
+
maximum: Number.MAX_SAFE_INTEGER - 1
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const kmsModule = {
|
|
129
|
+
title: 'kmsModule',
|
|
130
|
+
type: 'string'
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const postKeystoreBody = {
|
|
134
|
+
title: 'postKeystoreBody',
|
|
135
|
+
type: 'object',
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
required: ['sequence', 'controller', 'meterId'],
|
|
138
|
+
properties: {
|
|
139
|
+
controller,
|
|
140
|
+
ipAllowList,
|
|
141
|
+
referenceId,
|
|
142
|
+
sequence,
|
|
143
|
+
kmsModule,
|
|
144
|
+
meterId
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const updateKeystoreConfigBody = {
|
|
149
|
+
title: 'updateKeystoreConfigBody',
|
|
150
|
+
type: 'object',
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
required: [
|
|
153
|
+
'controller',
|
|
154
|
+
'id',
|
|
155
|
+
'sequence',
|
|
156
|
+
],
|
|
157
|
+
properties: {
|
|
158
|
+
controller,
|
|
159
|
+
id,
|
|
160
|
+
ipAllowList,
|
|
161
|
+
referenceId,
|
|
162
|
+
sequence,
|
|
163
|
+
kmsModule,
|
|
164
|
+
meterId
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const postRevocationBody = {
|
|
169
|
+
...delegatedZcap
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
postKeystoreBody,
|
|
174
|
+
postRevocationBody,
|
|
175
|
+
updateKeystoreConfigBody
|
|
176
|
+
};
|