@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.
@@ -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
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "env": {
3
+ "mocha": true
4
+ },
5
+ "globals": {
6
+ "assertNoError": true,
7
+ "should": true
8
+ }
9
+ }