was-teaching-server 0.9.1
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 +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handlers for WebKMS key operations (the `/kms` facet):
|
|
3
|
+
* - POST /kms/keystores/:keystoreId/keys (GenerateKeyOperation)
|
|
4
|
+
* - GET /kms/keystores/:keystoreId/keys (List Keys -- a fork extension beyond
|
|
5
|
+
* upstream webkms-switch; enumerate the keystore's public key descriptions)
|
|
6
|
+
* - POST /kms/keystores/:keystoreId/keys/:keyId (Sign / Verify / DeriveSecret /
|
|
7
|
+
* WrapKey / UnwrapKey operation, dispatched by envelope `type`)
|
|
8
|
+
* - GET /kms/keystores/:keystoreId/keys/:keyId (public key description)
|
|
9
|
+
*
|
|
10
|
+
* The wire contract is protocol-fixed by webkms-switch /
|
|
11
|
+
* `@interop/webkms-client` (the conformance suite): operation envelopes carry
|
|
12
|
+
* an optional `@context`, generate responds 200 (not 201) with a `Location`
|
|
13
|
+
* header and a `{ keyId, keyDescription }` body, every other operation
|
|
14
|
+
* responds 200 with its own single-field result. Every route roots its
|
|
15
|
+
* capability in the **keystore** URL (never the key -- the client computes the
|
|
16
|
+
* root zcap by stripping the key URL at the last `/keys/`), with the key URL
|
|
17
|
+
* accepted as an attenuated target; the expected zcap action is the operation
|
|
18
|
+
* name, decapitalized, minus `Operation` (`generateKey`, `sign`, ...), and
|
|
19
|
+
* `read` for the description GET (which this server authorizes rather than
|
|
20
|
+
* leaving open).
|
|
21
|
+
*/
|
|
22
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
23
|
+
import { generateId } from '@digitalcredentials/bnid'
|
|
24
|
+
import { fetchKeystoreAndVerify } from './keystoreContext.js'
|
|
25
|
+
import {
|
|
26
|
+
generateKmsKey,
|
|
27
|
+
describeKmsKey,
|
|
28
|
+
runKeyOperation
|
|
29
|
+
} from '../lib/kmsModule.js'
|
|
30
|
+
import {
|
|
31
|
+
encryptKeyRecord,
|
|
32
|
+
decryptKeyRecord,
|
|
33
|
+
currentRecordKek,
|
|
34
|
+
recordKekLoader
|
|
35
|
+
} from '../lib/kmsRecordCipher.js'
|
|
36
|
+
import { isUrlSafeSegment } from '../lib/validateId.js'
|
|
37
|
+
import { kmsKeysPath } from '../lib/paths.js'
|
|
38
|
+
import { encodeCursor, decodeCursor } from '../lib/cursor.js'
|
|
39
|
+
import { KEY_LIST_LIMIT } from '../config.default.js'
|
|
40
|
+
import {
|
|
41
|
+
InvalidRequestBodyError,
|
|
42
|
+
KeyNotFoundError,
|
|
43
|
+
KmsRecordCipherError,
|
|
44
|
+
UnauthorizedError,
|
|
45
|
+
UnsupportedKeyOperationError
|
|
46
|
+
} from '../errors.js'
|
|
47
|
+
import type { KmsKeyRecord, KmsRecordKekRegistry } from '../types.js'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The operation-specific body fields of each dispatchable (non-generate)
|
|
51
|
+
* operation envelope. Doubles as the recognized-operation registry: an
|
|
52
|
+
* envelope `type` outside this table is the clean 400 not-supported. The
|
|
53
|
+
* fields' *values* are validated by the KMS module's per-operation
|
|
54
|
+
* implementations; the request layer enforces the envelope shape
|
|
55
|
+
* (`additionalProperties: false`, per the webkms-switch schemas).
|
|
56
|
+
*/
|
|
57
|
+
const OPERATION_FIELDS: Record<string, string[]> = {
|
|
58
|
+
SignOperation: ['verifyData'],
|
|
59
|
+
VerifyOperation: ['verifyData', 'signatureValue'],
|
|
60
|
+
DeriveSecretOperation: ['publicKey'],
|
|
61
|
+
WrapKeyOperation: ['unwrappedKey'],
|
|
62
|
+
UnwrapKeyOperation: ['wrappedKey']
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derives the expected zcap action from an operation envelope `type`:
|
|
67
|
+
* decapitalized, minus the `Operation` suffix (webkms-switch `_parseAction`) --
|
|
68
|
+
* `SignOperation` to `sign`, `DeriveSecretOperation` to `deriveSecret`, etc.
|
|
69
|
+
* @param operationType {string}
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function operationAction(operationType: string): string {
|
|
73
|
+
const name = operationType.slice(0, operationType.indexOf('Operation'))
|
|
74
|
+
return name.charAt(0).toLowerCase() + name.slice(1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Asserts the request body is a JSON object envelope of the given operation
|
|
79
|
+
* `type` carrying only the allowed keys (`additionalProperties: false`).
|
|
80
|
+
* `@context` is allowed but never required -- the webkms-switch schemas make
|
|
81
|
+
* it optional and `@interop/webkms-client` omits it entirely.
|
|
82
|
+
*
|
|
83
|
+
* @param options {object}
|
|
84
|
+
* @param options.body {unknown} the parsed request body
|
|
85
|
+
* @param options.allowedKeys {string[]} allowed keys beyond `@context`/`type`
|
|
86
|
+
* @param options.requestName {string} request name used in error titles
|
|
87
|
+
* @returns {Record<string, unknown>} the body, narrowed
|
|
88
|
+
*/
|
|
89
|
+
function assertOperationEnvelope({
|
|
90
|
+
body,
|
|
91
|
+
allowedKeys,
|
|
92
|
+
requestName
|
|
93
|
+
}: {
|
|
94
|
+
body: unknown
|
|
95
|
+
allowedKeys: string[]
|
|
96
|
+
requestName: string
|
|
97
|
+
}): Record<string, unknown> {
|
|
98
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
99
|
+
throw new InvalidRequestBodyError({
|
|
100
|
+
requestName,
|
|
101
|
+
detail: 'Operation body must be a JSON object.'
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
const envelope = body as Record<string, unknown>
|
|
105
|
+
for (const key of Object.keys(envelope)) {
|
|
106
|
+
if (key !== '@context' && key !== 'type' && !allowedKeys.includes(key)) {
|
|
107
|
+
throw new InvalidRequestBodyError({
|
|
108
|
+
requestName,
|
|
109
|
+
detail: `Unexpected operation property "${key}".`,
|
|
110
|
+
pointer: `#/${key}`
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return envelope
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Decrypts a stored key record through the at-rest cipher seam
|
|
119
|
+
* (`KMS_RECORD_KEK`): a plaintext record passes through untouched; an encrypted
|
|
120
|
+
* one is unwrapped under its stored `kekId` (a failure -- missing KEK / bad
|
|
121
|
+
* ciphertext -- is a 500 `KmsRecordCipherError`, a server misconfiguration).
|
|
122
|
+
* The single funnel every path that needs the *secret* (operation dispatch,
|
|
123
|
+
* description GET) goes through, so no operation can read a ciphertext envelope
|
|
124
|
+
* as key material. List Keys deliberately does NOT use it: it projects only
|
|
125
|
+
* public fields, which stay plaintext at rest, so it never unwraps a secret.
|
|
126
|
+
*
|
|
127
|
+
* @param options {object}
|
|
128
|
+
* @param options.record {KmsKeyRecord} the record as stored (maybe encrypted)
|
|
129
|
+
* @param [options.kmsRecordKek] {KmsRecordKekRegistry} the configured KEK
|
|
130
|
+
* registry (`request.server.kmsRecordKek`), or `undefined` when disabled
|
|
131
|
+
* @param options.requestName {string} request name used in error titles
|
|
132
|
+
* @returns {KmsKeyRecord} the decrypted (secret-bearing) record
|
|
133
|
+
*/
|
|
134
|
+
function decryptStoredKeyRecord({
|
|
135
|
+
record,
|
|
136
|
+
kmsRecordKek,
|
|
137
|
+
requestName
|
|
138
|
+
}: {
|
|
139
|
+
record: KmsKeyRecord
|
|
140
|
+
kmsRecordKek?: KmsRecordKekRegistry
|
|
141
|
+
requestName: string
|
|
142
|
+
}): KmsKeyRecord {
|
|
143
|
+
try {
|
|
144
|
+
return decryptKeyRecord({
|
|
145
|
+
record,
|
|
146
|
+
kekLoader: recordKekLoader(kmsRecordKek)
|
|
147
|
+
})
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new KmsRecordCipherError({ cause: err as Error, requestName })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Loads a key record by its URL params, masking an unknown (or non-URL-safe)
|
|
155
|
+
* key id as the 404 `KeyNotFoundError`. Call after capability verification,
|
|
156
|
+
* so an under-authorized caller cannot distinguish absent from forbidden. The
|
|
157
|
+
* record is decrypted through the cipher seam (see `decryptStoredKeyRecord`)
|
|
158
|
+
* before it is returned.
|
|
159
|
+
*
|
|
160
|
+
* @param options {object}
|
|
161
|
+
* @param options.request {FastifyRequest} supplies `request.server.storage`
|
|
162
|
+
* @param options.keystoreId {string} the keystore's local id (URL param)
|
|
163
|
+
* @param options.keyId {string} the key's local id (URL param)
|
|
164
|
+
* @param options.requestName {string} request name used in error titles
|
|
165
|
+
* @returns {Promise<KmsKeyRecord>} the decrypted (secret-bearing) record
|
|
166
|
+
*/
|
|
167
|
+
async function fetchKeyRecord({
|
|
168
|
+
request,
|
|
169
|
+
keystoreId,
|
|
170
|
+
keyId,
|
|
171
|
+
requestName
|
|
172
|
+
}: {
|
|
173
|
+
request: FastifyRequest
|
|
174
|
+
keystoreId: string
|
|
175
|
+
keyId: string
|
|
176
|
+
requestName: string
|
|
177
|
+
}): Promise<KmsKeyRecord> {
|
|
178
|
+
if (!isUrlSafeSegment(keyId)) {
|
|
179
|
+
throw new KeyNotFoundError({ requestName })
|
|
180
|
+
}
|
|
181
|
+
const record = await request.server.storage.getKey({
|
|
182
|
+
keystoreId,
|
|
183
|
+
localId: keyId
|
|
184
|
+
})
|
|
185
|
+
if (!record) {
|
|
186
|
+
throw new KeyNotFoundError({ requestName })
|
|
187
|
+
}
|
|
188
|
+
return decryptStoredKeyRecord({
|
|
189
|
+
record,
|
|
190
|
+
kmsRecordKek: request.server.kmsRecordKek,
|
|
191
|
+
requestName
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Asserts a `GenerateKeyOperation`'s object `invocationTarget`: a required
|
|
197
|
+
* non-empty string `type`, an optional integer `maxCapabilityChainLength`
|
|
198
|
+
* (1-10; the chain includes the root, so 1 means controller-only), and at
|
|
199
|
+
* most one of `publicAlias` / `publicAliasTemplate` (non-empty strings).
|
|
200
|
+
*
|
|
201
|
+
* @param options {object}
|
|
202
|
+
* @param options.invocationTarget {unknown} the envelope's invocationTarget
|
|
203
|
+
* @param options.requestName {string} request name used in error titles
|
|
204
|
+
* @returns {object} the narrowed target fields
|
|
205
|
+
*/
|
|
206
|
+
function assertGenerateKeyTarget({
|
|
207
|
+
invocationTarget,
|
|
208
|
+
requestName
|
|
209
|
+
}: {
|
|
210
|
+
invocationTarget: unknown
|
|
211
|
+
requestName: string
|
|
212
|
+
}): {
|
|
213
|
+
type: string
|
|
214
|
+
maxCapabilityChainLength?: number
|
|
215
|
+
publicAlias?: string
|
|
216
|
+
publicAliasTemplate?: string
|
|
217
|
+
} {
|
|
218
|
+
if (
|
|
219
|
+
typeof invocationTarget !== 'object' ||
|
|
220
|
+
invocationTarget === null ||
|
|
221
|
+
Array.isArray(invocationTarget)
|
|
222
|
+
) {
|
|
223
|
+
throw new InvalidRequestBodyError({
|
|
224
|
+
requestName,
|
|
225
|
+
detail: 'Operation "invocationTarget" must be an object.',
|
|
226
|
+
pointer: '#/invocationTarget'
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
const allowedKeys = [
|
|
230
|
+
'type',
|
|
231
|
+
'maxCapabilityChainLength',
|
|
232
|
+
'publicAlias',
|
|
233
|
+
'publicAliasTemplate'
|
|
234
|
+
]
|
|
235
|
+
for (const key of Object.keys(invocationTarget)) {
|
|
236
|
+
if (!allowedKeys.includes(key)) {
|
|
237
|
+
throw new InvalidRequestBodyError({
|
|
238
|
+
requestName,
|
|
239
|
+
detail: `Unexpected invocationTarget property "${key}".`,
|
|
240
|
+
pointer: `#/invocationTarget/${key}`
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const target = invocationTarget as {
|
|
245
|
+
type?: unknown
|
|
246
|
+
maxCapabilityChainLength?: unknown
|
|
247
|
+
publicAlias?: unknown
|
|
248
|
+
publicAliasTemplate?: unknown
|
|
249
|
+
}
|
|
250
|
+
if (typeof target.type !== 'string' || target.type.length === 0) {
|
|
251
|
+
throw new InvalidRequestBodyError({
|
|
252
|
+
requestName,
|
|
253
|
+
detail: 'invocationTarget "type" must be a non-empty string.',
|
|
254
|
+
pointer: '#/invocationTarget/type'
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
target.maxCapabilityChainLength !== undefined &&
|
|
259
|
+
(!Number.isInteger(target.maxCapabilityChainLength) ||
|
|
260
|
+
(target.maxCapabilityChainLength as number) < 1 ||
|
|
261
|
+
(target.maxCapabilityChainLength as number) > 10)
|
|
262
|
+
) {
|
|
263
|
+
throw new InvalidRequestBodyError({
|
|
264
|
+
requestName,
|
|
265
|
+
detail:
|
|
266
|
+
'invocationTarget "maxCapabilityChainLength" must be an integer' +
|
|
267
|
+
' between 1 and 10.',
|
|
268
|
+
pointer: '#/invocationTarget/maxCapabilityChainLength'
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
for (const aliasField of ['publicAlias', 'publicAliasTemplate'] as const) {
|
|
272
|
+
const value = target[aliasField]
|
|
273
|
+
if (
|
|
274
|
+
value !== undefined &&
|
|
275
|
+
(typeof value !== 'string' || value.length === 0)
|
|
276
|
+
) {
|
|
277
|
+
throw new InvalidRequestBodyError({
|
|
278
|
+
requestName,
|
|
279
|
+
detail: `invocationTarget "${aliasField}" must be a non-empty string.`,
|
|
280
|
+
pointer: `#/invocationTarget/${aliasField}`
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
target.publicAlias !== undefined &&
|
|
286
|
+
target.publicAliasTemplate !== undefined
|
|
287
|
+
) {
|
|
288
|
+
throw new InvalidRequestBodyError({
|
|
289
|
+
requestName,
|
|
290
|
+
detail:
|
|
291
|
+
'Only one of "publicAlias" or "publicAliasTemplate" may be given.',
|
|
292
|
+
pointer: '#/invocationTarget/publicAlias'
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
return target as {
|
|
296
|
+
type: string
|
|
297
|
+
maxCapabilityChainLength?: number
|
|
298
|
+
publicAlias?: string
|
|
299
|
+
publicAliasTemplate?: string
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export class KeyRequest {
|
|
304
|
+
/**
|
|
305
|
+
* POST /kms/keystores/:keystoreId/keys
|
|
306
|
+
* Generate Key (`GenerateKeyOperation`). The envelope's `invocationTarget`
|
|
307
|
+
* is an *object* (`{ type, maxCapabilityChainLength?, publicAlias? |
|
|
308
|
+
* publicAliasTemplate? }`), unlike every other operation's string. Verified
|
|
309
|
+
* against the keystore's controller with the `generateKey` action; the new
|
|
310
|
+
* key's local id is server-generated (the same bnid multihash scheme as
|
|
311
|
+
* keystore ids, per webkms-switch `generateRandom`). Responds 200 with a
|
|
312
|
+
* `Location` header and `{ keyId, keyDescription }` -- the client reads the
|
|
313
|
+
* key id from the *body*, never the header.
|
|
314
|
+
*
|
|
315
|
+
* @param request {import('fastify').FastifyRequest}
|
|
316
|
+
* @param reply {import('fastify').FastifyReply}
|
|
317
|
+
* @returns {Promise<FastifyReply>}
|
|
318
|
+
*/
|
|
319
|
+
static async generate(
|
|
320
|
+
request: FastifyRequest<{
|
|
321
|
+
Params: { keystoreId: string }
|
|
322
|
+
Body: Record<string, unknown>
|
|
323
|
+
}>,
|
|
324
|
+
reply: FastifyReply
|
|
325
|
+
): Promise<FastifyReply> {
|
|
326
|
+
const requestName = 'Generate Key'
|
|
327
|
+
const { keystoreId } = request.params
|
|
328
|
+
const { storage } = request.server
|
|
329
|
+
|
|
330
|
+
const envelope = assertOperationEnvelope({
|
|
331
|
+
body: request.body,
|
|
332
|
+
allowedKeys: ['invocationTarget'],
|
|
333
|
+
requestName
|
|
334
|
+
})
|
|
335
|
+
if (envelope.type !== 'GenerateKeyOperation') {
|
|
336
|
+
throw new InvalidRequestBodyError({
|
|
337
|
+
requestName,
|
|
338
|
+
detail: 'Operation "type" must be "GenerateKeyOperation".',
|
|
339
|
+
pointer: '#/type'
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
const target = assertGenerateKeyTarget({
|
|
343
|
+
invocationTarget: envelope.invocationTarget,
|
|
344
|
+
requestName
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const { config } = await fetchKeystoreAndVerify({
|
|
348
|
+
request,
|
|
349
|
+
keystoreId,
|
|
350
|
+
allowedAction: 'generateKey',
|
|
351
|
+
requestName,
|
|
352
|
+
allowTargetAttenuation: true
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// Server-generated local id, per webkms-switch `generateRandom`: multibase
|
|
356
|
+
// base58btc of a multihash-framed (identity, 16-byte) 128-bit random value.
|
|
357
|
+
const localId = await generateId({ multihash: true })
|
|
358
|
+
const keyId = `${config.id}/keys/${localId}`
|
|
359
|
+
const { key, keyDescription } = await generateKmsKey({
|
|
360
|
+
keyId,
|
|
361
|
+
type: target.type,
|
|
362
|
+
controller: config.controller,
|
|
363
|
+
maxCapabilityChainLength: target.maxCapabilityChainLength,
|
|
364
|
+
publicAlias: target.publicAlias,
|
|
365
|
+
publicAliasTemplate: target.publicAliasTemplate
|
|
366
|
+
})
|
|
367
|
+
const now = new Date().toISOString()
|
|
368
|
+
// At-rest cipher seam (`KMS_RECORD_KEK`): when a current KEK is configured,
|
|
369
|
+
// encrypt the record's secret fields before it reaches storage (the record
|
|
370
|
+
// stays opaque to the backend); unconfigured, it is stored plaintext.
|
|
371
|
+
let record: KmsKeyRecord = {
|
|
372
|
+
keystoreId,
|
|
373
|
+
localId,
|
|
374
|
+
meta: { created: now, updated: now },
|
|
375
|
+
key
|
|
376
|
+
}
|
|
377
|
+
const kek = currentRecordKek(request.server.kmsRecordKek)
|
|
378
|
+
if (kek !== undefined) {
|
|
379
|
+
record = encryptKeyRecord({ record, kek })
|
|
380
|
+
}
|
|
381
|
+
await storage.insertKey({ keystoreId, localId, record })
|
|
382
|
+
|
|
383
|
+
// 200 (not the keystore create's 201) with a Location header, per
|
|
384
|
+
// webkms-switch `runOperationMiddleware`.
|
|
385
|
+
reply.header('Location', keyId)
|
|
386
|
+
return reply.send({ keyId, keyDescription })
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* POST /kms/keystores/:keystoreId/keys/:keyId
|
|
391
|
+
* Key operation dispatch by envelope `type` (Sign / Verify / DeriveSecret /
|
|
392
|
+
* WrapKey / UnwrapKey). The envelope's string `invocationTarget` must equal
|
|
393
|
+
* the request URL exactly (400, per webkms-switch); the expected zcap action
|
|
394
|
+
* is the decapitalized operation name, verified against the keystore's
|
|
395
|
+
* controller with the key URL as an attenuated target. An operation the KMS
|
|
396
|
+
* does not recognize -- or does not serve for the key's type, e.g.
|
|
397
|
+
* `VerifyOperation` on an asymmetric key -- is a clean 400 not-supported.
|
|
398
|
+
* Responds 200 with the operation's single-field result.
|
|
399
|
+
*
|
|
400
|
+
* @param request {import('fastify').FastifyRequest}
|
|
401
|
+
* @param reply {import('fastify').FastifyReply}
|
|
402
|
+
* @returns {Promise<FastifyReply>}
|
|
403
|
+
*/
|
|
404
|
+
static async operation(
|
|
405
|
+
request: FastifyRequest<{
|
|
406
|
+
Params: { keystoreId: string; keyId: string }
|
|
407
|
+
Body: Record<string, unknown>
|
|
408
|
+
}>,
|
|
409
|
+
reply: FastifyReply
|
|
410
|
+
): Promise<FastifyReply> {
|
|
411
|
+
const requestName = 'Key Operation'
|
|
412
|
+
const { keystoreId, keyId } = request.params
|
|
413
|
+
const { serverUrl } = request.server
|
|
414
|
+
|
|
415
|
+
if (
|
|
416
|
+
typeof request.body !== 'object' ||
|
|
417
|
+
request.body === null ||
|
|
418
|
+
typeof (request.body as Record<string, unknown>).type !== 'string'
|
|
419
|
+
) {
|
|
420
|
+
throw new InvalidRequestBodyError({
|
|
421
|
+
requestName,
|
|
422
|
+
detail: 'Operation body must be a JSON object with a string "type".',
|
|
423
|
+
pointer: '#/type'
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
const operationType = (request.body as Record<string, unknown>)
|
|
427
|
+
.type as string
|
|
428
|
+
const operationFields = OPERATION_FIELDS[operationType]
|
|
429
|
+
if (operationFields === undefined) {
|
|
430
|
+
// Unknown-but-well-formed operation types (GenerateKeyOperation included:
|
|
431
|
+
// it is only dispatchable at the keys-collection URL) are not served.
|
|
432
|
+
throw new UnsupportedKeyOperationError({ operationType })
|
|
433
|
+
}
|
|
434
|
+
const envelope = assertOperationEnvelope({
|
|
435
|
+
body: request.body,
|
|
436
|
+
allowedKeys: ['invocationTarget', ...operationFields],
|
|
437
|
+
requestName
|
|
438
|
+
})
|
|
439
|
+
// The envelope names the key it operates on; it must be the key it was
|
|
440
|
+
// posted to (webkms-switch's invocation-target-vs-request-URL 400).
|
|
441
|
+
const fullRequestUrl = new URL(
|
|
442
|
+
kmsKeysPath({ keystoreId, keyId }),
|
|
443
|
+
serverUrl
|
|
444
|
+
).toString()
|
|
445
|
+
if (envelope.invocationTarget !== fullRequestUrl) {
|
|
446
|
+
throw new InvalidRequestBodyError({
|
|
447
|
+
requestName,
|
|
448
|
+
detail: 'Operation "invocationTarget" does not match the request URL.',
|
|
449
|
+
pointer: '#/invocationTarget'
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const { dereferencedChainLength } = await fetchKeystoreAndVerify({
|
|
454
|
+
request,
|
|
455
|
+
keystoreId,
|
|
456
|
+
allowedAction: operationAction(operationType),
|
|
457
|
+
requestName,
|
|
458
|
+
allowTargetAttenuation: true
|
|
459
|
+
})
|
|
460
|
+
const record = await fetchKeyRecord({
|
|
461
|
+
request,
|
|
462
|
+
keystoreId,
|
|
463
|
+
keyId,
|
|
464
|
+
requestName
|
|
465
|
+
})
|
|
466
|
+
// Per-key invocation chain bound, set at generate time and enforced at
|
|
467
|
+
// operation time. The chain includes the root, so a bound of 1 means
|
|
468
|
+
// controller-only. An
|
|
469
|
+
// over-long chain is an authorization failure: masked 404, per the
|
|
470
|
+
// server-wide convention.
|
|
471
|
+
if (
|
|
472
|
+
record.key.maxCapabilityChainLength !== undefined &&
|
|
473
|
+
dereferencedChainLength > record.key.maxCapabilityChainLength
|
|
474
|
+
) {
|
|
475
|
+
throw new UnauthorizedError({ requestName })
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const result = await runKeyOperation({
|
|
479
|
+
key: record.key,
|
|
480
|
+
operation: envelope
|
|
481
|
+
})
|
|
482
|
+
return reply.send(result)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* GET /kms/keystores/:keystoreId/keys/:keyId
|
|
487
|
+
* Public key description: capability-verified against the keystore's
|
|
488
|
+
* controller (`read`, with the key URL as an attenuated target -- this
|
|
489
|
+
* server authorizes the route rather than serving it open). The
|
|
490
|
+
* description's `controller` is the keystore's *live*
|
|
491
|
+
* controller, and the `publicAlias` / `publicAliasTemplate` override is
|
|
492
|
+
* re-applied on every read, so descriptions are stable (the client caches
|
|
493
|
+
* them for five minutes).
|
|
494
|
+
*
|
|
495
|
+
* @param request {import('fastify').FastifyRequest}
|
|
496
|
+
* @param reply {import('fastify').FastifyReply}
|
|
497
|
+
* @returns {Promise<FastifyReply>}
|
|
498
|
+
*/
|
|
499
|
+
static async get(
|
|
500
|
+
request: FastifyRequest<{
|
|
501
|
+
Params: { keystoreId: string; keyId: string }
|
|
502
|
+
}>,
|
|
503
|
+
reply: FastifyReply
|
|
504
|
+
): Promise<FastifyReply> {
|
|
505
|
+
const requestName = 'Get Key Description'
|
|
506
|
+
const { keystoreId, keyId } = request.params
|
|
507
|
+
|
|
508
|
+
const { config } = await fetchKeystoreAndVerify({
|
|
509
|
+
request,
|
|
510
|
+
keystoreId,
|
|
511
|
+
allowedAction: 'read',
|
|
512
|
+
requestName,
|
|
513
|
+
allowTargetAttenuation: true
|
|
514
|
+
})
|
|
515
|
+
const record = await fetchKeyRecord({
|
|
516
|
+
request,
|
|
517
|
+
keystoreId,
|
|
518
|
+
keyId,
|
|
519
|
+
requestName
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
return reply.send(
|
|
523
|
+
describeKmsKey({ key: record.key, controller: config.controller })
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* GET /kms/keystores/:keystoreId/keys
|
|
529
|
+
* List Keys (a fork extension beyond upstream webkms-switch). Enumerates the
|
|
530
|
+
* keystore's public key descriptions -- the Get Key Description projection
|
|
531
|
+
* per key (`describeKmsKey`: allowlisted public fields, the keystore's live
|
|
532
|
+
* controller, alias re-applied; never a secret field), plus `keyUrl`, the
|
|
533
|
+
* key's canonical invocation URL (which the alias override erases from `id`
|
|
534
|
+
* for precisely the keys a recovery client must rediscover) --
|
|
535
|
+
* sorted by local id and paginated (`KEY_LIST_LIMIT` per page, an opaque
|
|
536
|
+
* `cursor` following the standard convention). Capability-verified against the
|
|
537
|
+
* keystore's controller (`read`), with the `<keystoreId>/keys` URL accepted as
|
|
538
|
+
* an attenuated target (the same treatment Get Key gives the key URL). An
|
|
539
|
+
* empty keystore returns `{ results: [] }`, not a 404. Responds `{ results }`,
|
|
540
|
+
* plus a `next` cursor URL when a further page follows.
|
|
541
|
+
*
|
|
542
|
+
* @param request {import('fastify').FastifyRequest}
|
|
543
|
+
* @param reply {import('fastify').FastifyReply}
|
|
544
|
+
* @returns {Promise<FastifyReply>}
|
|
545
|
+
*/
|
|
546
|
+
static async list(
|
|
547
|
+
request: FastifyRequest<{
|
|
548
|
+
Params: { keystoreId: string }
|
|
549
|
+
Querystring: { cursor?: string }
|
|
550
|
+
}>,
|
|
551
|
+
reply: FastifyReply
|
|
552
|
+
): Promise<FastifyReply> {
|
|
553
|
+
const requestName = 'List Keys'
|
|
554
|
+
const { keystoreId } = request.params
|
|
555
|
+
const { cursor } = request.query
|
|
556
|
+
const { storage } = request.server
|
|
557
|
+
|
|
558
|
+
// The `?cursor` pagination parameter rides the signed request URL, so
|
|
559
|
+
// verification runs in target-query mode (as List Keystores / List
|
|
560
|
+
// Collection do). Authorization runs before any cursor validation below, so
|
|
561
|
+
// an under-authorized caller gets the merged 404 -- never `invalid-cursor`.
|
|
562
|
+
const { config } = await fetchKeystoreAndVerify({
|
|
563
|
+
request,
|
|
564
|
+
keystoreId,
|
|
565
|
+
allowedAction: 'read',
|
|
566
|
+
requestName,
|
|
567
|
+
allowTargetAttenuation: true,
|
|
568
|
+
allowTargetQuery: true
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// The backend returns every record sorted by local id; key records are
|
|
572
|
+
// insert-once and immutable, so the cursor's `after` names a stable
|
|
573
|
+
// position -- resume from just past it (falling back to a keyset compare
|
|
574
|
+
// should the id somehow be absent). The cursor is opaque and validated
|
|
575
|
+
// here (400 `invalid-cursor` on a malformed token).
|
|
576
|
+
const keys = await storage.listKeys({ keystoreId })
|
|
577
|
+
let remaining = keys
|
|
578
|
+
if (cursor !== undefined) {
|
|
579
|
+
const { after } = decodeCursor(cursor)
|
|
580
|
+
const index = keys.findIndex(entry => entry.localId === after)
|
|
581
|
+
remaining =
|
|
582
|
+
index === -1
|
|
583
|
+
? keys.filter(entry => entry.localId > after)
|
|
584
|
+
: keys.slice(index + 1)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const hasMore = remaining.length > KEY_LIST_LIMIT
|
|
588
|
+
const page = remaining.slice(0, KEY_LIST_LIMIT)
|
|
589
|
+
// Project directly from the stored record -- NO decrypt. The description
|
|
590
|
+
// reads only public fields, which stay plaintext at rest even under the
|
|
591
|
+
// record cipher (the `encrypted` envelope replaces only the secret subset).
|
|
592
|
+
// So listing never unwraps a key: it does not bulk-materialize private
|
|
593
|
+
// material in memory, and it still enumerates keys whose KEK is rotated out
|
|
594
|
+
// or whose envelope is corrupt -- the recovery-path robustness this endpoint
|
|
595
|
+
// exists for. (Contrast `fetchKeyRecord`, which must decrypt: get/operation
|
|
596
|
+
// need the secret.)
|
|
597
|
+
// Each entry additionally carries `keyUrl`, the key's canonical invocation
|
|
598
|
+
// URL. The alias override rewrites the description's `id`, erasing the one
|
|
599
|
+
// handle (`<keystoreId>/keys/<localId>`) a recovery client needs to invoke
|
|
600
|
+
// sign -- and a lister, unlike a Get Key caller, does not already hold it.
|
|
601
|
+
// Always stamped (aliased or not), so consumers need no fallback logic; it
|
|
602
|
+
// discloses only addressing an authorized lister already has, never key
|
|
603
|
+
// material.
|
|
604
|
+
const results = page.map(({ localId, record }) => ({
|
|
605
|
+
...describeKmsKey({ key: record.key, controller: config.controller }),
|
|
606
|
+
keyUrl: `${config.id}/keys/${localId}`
|
|
607
|
+
}))
|
|
608
|
+
|
|
609
|
+
let next: string | undefined
|
|
610
|
+
if (hasMore) {
|
|
611
|
+
const lastId = page[page.length - 1]!.localId
|
|
612
|
+
next = `${kmsKeysPath({ keystoreId })}?cursor=${encodeCursor(lastId)}`
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return reply.send({ results, ...(next !== undefined && { next }) })
|
|
616
|
+
}
|
|
617
|
+
}
|