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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single in-process WebKMS module this server hard-wires (`local-v1`),
|
|
3
|
+
* collecting the module's parts in one file: a pure-crypto core per key type,
|
|
4
|
+
* an operation dispatch table keyed by `(key type, operation type)`, and the
|
|
5
|
+
* generate/describe orchestration (record assembly, `publicAlias` /
|
|
6
|
+
* `publicAliasTemplate` expansion). The module never touches storage -- the
|
|
7
|
+
* request layer loads/inserts `KmsKeyRecord`s through the storage backend and
|
|
8
|
+
* hands this module the (secret-bearing) stored key.
|
|
9
|
+
*
|
|
10
|
+
* Custody draws the line on what is served (see the roadmap's wire contract):
|
|
11
|
+
* every operation here requires the custodial secret -- Ed25519 sign, X25519
|
|
12
|
+
* deriveSecret, HMAC sign/verify, AES-KW wrap/unwrap. Asymmetric verify needs
|
|
13
|
+
* only the public key and is deliberately not served (client-local instead);
|
|
14
|
+
* requesting it is a clean 400.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createCipheriv,
|
|
18
|
+
createDecipheriv,
|
|
19
|
+
createHmac,
|
|
20
|
+
randomBytes,
|
|
21
|
+
timingSafeEqual
|
|
22
|
+
} from 'node:crypto'
|
|
23
|
+
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key'
|
|
24
|
+
import { X25519KeyAgreementKey2020 } from '@interop/x25519-key-agreement-key'
|
|
25
|
+
import {
|
|
26
|
+
InvalidRequestBodyError,
|
|
27
|
+
UnsupportedKeyOperationError
|
|
28
|
+
} from '../errors.js'
|
|
29
|
+
import type { IDID, KmsKeyDescription, KmsStoredKey } from '../types.js'
|
|
30
|
+
|
|
31
|
+
/** The JSON-LD context each supported key type's description carries. */
|
|
32
|
+
const KEY_TYPE_CONTEXTS: Record<string, string> = {
|
|
33
|
+
Ed25519VerificationKey2020:
|
|
34
|
+
'https://w3id.org/security/suites/ed25519-2020/v1',
|
|
35
|
+
X25519KeyAgreementKey2020: 'https://w3id.org/security/suites/x25519-2020/v1',
|
|
36
|
+
Sha256HmacKey2019: 'https://w3id.org/security/suites/hmac-2019/v1',
|
|
37
|
+
AesKeyWrappingKey2019: 'https://w3id.org/security/suites/aes-2019/v1'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* AES Key Wrap (RFC 3394) parameters: AES-256 in key-wrap mode with the RFC's
|
|
42
|
+
* default initial value. The IV is the integrity check -- `decipher.final()`
|
|
43
|
+
* throws when the unwrapped output would not reproduce it (i.e. the wrong KEK
|
|
44
|
+
* or corrupted ciphertext).
|
|
45
|
+
*/
|
|
46
|
+
const AES_KW_ALGORITHM = 'id-aes256-wrap'
|
|
47
|
+
const AES_KW_DEFAULT_IV = Buffer.alloc(8, 0xa6)
|
|
48
|
+
|
|
49
|
+
/** Decodes a base64url (no padding) operation field into bytes. */
|
|
50
|
+
function decodeBase64url({
|
|
51
|
+
value,
|
|
52
|
+
field,
|
|
53
|
+
requestName
|
|
54
|
+
}: {
|
|
55
|
+
value: unknown
|
|
56
|
+
field: string
|
|
57
|
+
requestName: string
|
|
58
|
+
}): Buffer {
|
|
59
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
60
|
+
throw new InvalidRequestBodyError({
|
|
61
|
+
requestName,
|
|
62
|
+
detail: `Operation "${field}" must be a non-empty base64url string.`,
|
|
63
|
+
pointer: `#/${field}`
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(value, 'base64url')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Expands a `publicAliasTemplate` against the key description, using the
|
|
71
|
+
* `url-template` package (RFC 6570): `{var}` substitutes the
|
|
72
|
+
* percent-encoded description field, `{+var}` substitutes it raw (a typical
|
|
73
|
+
* template is `{+controller}#{publicKeyMultibase}`). An unknown
|
|
74
|
+
* variable expands to the empty string, as in RFC 6570.
|
|
75
|
+
*
|
|
76
|
+
* @param options {object}
|
|
77
|
+
* @param options.template {string}
|
|
78
|
+
* @param options.description {KmsKeyDescription} expansion variables
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function expandAliasTemplate({
|
|
82
|
+
template,
|
|
83
|
+
description
|
|
84
|
+
}: {
|
|
85
|
+
template: string
|
|
86
|
+
description: KmsKeyDescription
|
|
87
|
+
}): string {
|
|
88
|
+
const variables = description as unknown as Record<string, unknown>
|
|
89
|
+
return template.replace(
|
|
90
|
+
/\{(\+?)([A-Za-z0-9_]+)\}/g,
|
|
91
|
+
(_match, rawModifier: string, name: string) => {
|
|
92
|
+
const value = variables[name]
|
|
93
|
+
const expanded = typeof value === 'string' ? value : ''
|
|
94
|
+
return rawModifier === '+' ? expanded : encodeURIComponent(expanded)
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds the public key-description projection of a stored key: the
|
|
101
|
+
* allowlisted public fields plus the live keystore `controller` (never any
|
|
102
|
+
* secret field), with the `publicAlias` / expanded `publicAliasTemplate`
|
|
103
|
+
* override applied to `id`. Applied identically at generate time and on every
|
|
104
|
+
* subsequent read, so descriptions are stable (the client caches them).
|
|
105
|
+
*
|
|
106
|
+
* @param options {object}
|
|
107
|
+
* @param options.key {KmsStoredKey} the stored (secret-bearing) key
|
|
108
|
+
* @param options.controller {IDID} the keystore's current controller
|
|
109
|
+
* @returns {KmsKeyDescription}
|
|
110
|
+
*/
|
|
111
|
+
export function describeKmsKey({
|
|
112
|
+
key,
|
|
113
|
+
controller
|
|
114
|
+
}: {
|
|
115
|
+
key: KmsStoredKey
|
|
116
|
+
controller: IDID
|
|
117
|
+
}): KmsKeyDescription {
|
|
118
|
+
const description: KmsKeyDescription = {
|
|
119
|
+
'@context': key['@context'],
|
|
120
|
+
id: key.id,
|
|
121
|
+
type: key.type,
|
|
122
|
+
...(key.publicKeyMultibase !== undefined && {
|
|
123
|
+
publicKeyMultibase: key.publicKeyMultibase
|
|
124
|
+
}),
|
|
125
|
+
controller
|
|
126
|
+
}
|
|
127
|
+
if (key.publicAlias !== undefined) {
|
|
128
|
+
description.id = key.publicAlias
|
|
129
|
+
} else if (key.publicAliasTemplate !== undefined) {
|
|
130
|
+
description.id = expandAliasTemplate({
|
|
131
|
+
template: key.publicAliasTemplate,
|
|
132
|
+
description
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
return description
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generates key material for a supported key type and assembles the stored
|
|
140
|
+
* key (full serialized form, secret material included) plus its public
|
|
141
|
+
* description. Field names are protocol-fixed: the asymmetric pairs carry
|
|
142
|
+
* `publicKeyMultibase` / `privateKeyMultibase`; the symmetric keys carry a
|
|
143
|
+
* base64url 256-bit `secret` (the webkms per-type key generators).
|
|
144
|
+
*
|
|
145
|
+
* @param options {object}
|
|
146
|
+
* @param options.keyId {string} the full key URL (`<keystoreId>/keys/<localId>`)
|
|
147
|
+
* @param options.type {string} the webkms key type
|
|
148
|
+
* @param options.controller {IDID} the keystore's current controller (for
|
|
149
|
+
* the returned description only -- never stored on the key)
|
|
150
|
+
* @param [options.maxCapabilityChainLength] {number}
|
|
151
|
+
* @param [options.publicAlias] {string}
|
|
152
|
+
* @param [options.publicAliasTemplate] {string}
|
|
153
|
+
* @returns {Promise<{ key: KmsStoredKey, keyDescription: KmsKeyDescription }>}
|
|
154
|
+
*/
|
|
155
|
+
export async function generateKmsKey({
|
|
156
|
+
keyId,
|
|
157
|
+
type,
|
|
158
|
+
controller,
|
|
159
|
+
maxCapabilityChainLength,
|
|
160
|
+
publicAlias,
|
|
161
|
+
publicAliasTemplate
|
|
162
|
+
}: {
|
|
163
|
+
keyId: string
|
|
164
|
+
type: string
|
|
165
|
+
controller: IDID
|
|
166
|
+
maxCapabilityChainLength?: number
|
|
167
|
+
publicAlias?: string
|
|
168
|
+
publicAliasTemplate?: string
|
|
169
|
+
}): Promise<{ key: KmsStoredKey; keyDescription: KmsKeyDescription }> {
|
|
170
|
+
// `Object.hasOwn`, not `KEY_TYPE_CONTEXTS[type] === undefined`: `type` is
|
|
171
|
+
// client-supplied, so a prototype-chain name (`constructor`, `toString`,
|
|
172
|
+
// `hasOwnProperty`, ...) would otherwise resolve to an inherited `Object`
|
|
173
|
+
// member and slip past the fail-closed rejection into a stored key record.
|
|
174
|
+
if (!Object.hasOwn(KEY_TYPE_CONTEXTS, type)) {
|
|
175
|
+
throw new InvalidRequestBodyError({
|
|
176
|
+
requestName: 'Generate Key',
|
|
177
|
+
detail: `Unsupported key type "${type}".`,
|
|
178
|
+
pointer: '#/invocationTarget/type'
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
const context = KEY_TYPE_CONTEXTS[type]!
|
|
182
|
+
const key: KmsStoredKey = { '@context': context, id: keyId, type }
|
|
183
|
+
if (type === 'Ed25519VerificationKey2020') {
|
|
184
|
+
const keyPair = await Ed25519VerificationKey.generate()
|
|
185
|
+
key.publicKeyMultibase = keyPair.publicKeyMultibase
|
|
186
|
+
key.privateKeyMultibase = keyPair.privateKeyMultibase
|
|
187
|
+
} else if (type === 'X25519KeyAgreementKey2020') {
|
|
188
|
+
const keyPair = await X25519KeyAgreementKey2020.generate()
|
|
189
|
+
key.publicKeyMultibase = keyPair.publicKeyMultibase
|
|
190
|
+
key.privateKeyMultibase = keyPair.privateKeyMultibase
|
|
191
|
+
} else {
|
|
192
|
+
// Sha256HmacKey2019 / AesKeyWrappingKey2019: a 256-bit symmetric secret.
|
|
193
|
+
key.secret = randomBytes(32).toString('base64url')
|
|
194
|
+
}
|
|
195
|
+
if (maxCapabilityChainLength !== undefined) {
|
|
196
|
+
key.maxCapabilityChainLength = maxCapabilityChainLength
|
|
197
|
+
}
|
|
198
|
+
if (publicAlias !== undefined) {
|
|
199
|
+
key.publicAlias = publicAlias
|
|
200
|
+
}
|
|
201
|
+
if (publicAliasTemplate !== undefined) {
|
|
202
|
+
key.publicAliasTemplate = publicAliasTemplate
|
|
203
|
+
}
|
|
204
|
+
return { key, keyDescription: describeKmsKey({ key, controller }) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* SignOperation on an Ed25519 key: signs the `verifyData` bytes with the
|
|
209
|
+
* custodial private key. The signature is not a secret, so serving it is the
|
|
210
|
+
* custody model WebKMS is built for.
|
|
211
|
+
*/
|
|
212
|
+
async function ed25519Sign({
|
|
213
|
+
key,
|
|
214
|
+
operation
|
|
215
|
+
}: {
|
|
216
|
+
key: KmsStoredKey
|
|
217
|
+
operation: Record<string, unknown>
|
|
218
|
+
}): Promise<{ signatureValue: string }> {
|
|
219
|
+
const data = decodeBase64url({
|
|
220
|
+
value: operation.verifyData,
|
|
221
|
+
field: 'verifyData',
|
|
222
|
+
requestName: 'Sign Operation'
|
|
223
|
+
})
|
|
224
|
+
const keyPair = await Ed25519VerificationKey.from({ ...key })
|
|
225
|
+
const signature = await keyPair.signer().sign({ data })
|
|
226
|
+
return { signatureValue: Buffer.from(signature).toString('base64url') }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* DeriveSecretOperation on an X25519 key: raw ECDH against the submitted peer
|
|
231
|
+
* public key -- no KDF is applied; the caller runs the shared secret through
|
|
232
|
+
* one. A peer whose `type` does not match the key's is a clean 400 (the client
|
|
233
|
+
* pre-checks, so only non-client callers hit this).
|
|
234
|
+
*/
|
|
235
|
+
async function x25519DeriveSecret({
|
|
236
|
+
key,
|
|
237
|
+
operation
|
|
238
|
+
}: {
|
|
239
|
+
key: KmsStoredKey
|
|
240
|
+
operation: Record<string, unknown>
|
|
241
|
+
}): Promise<{ secret: string }> {
|
|
242
|
+
const requestName = 'Derive Secret Operation'
|
|
243
|
+
const publicKey = operation.publicKey as
|
|
244
|
+
{ type?: string; publicKeyMultibase?: string } | undefined
|
|
245
|
+
if (typeof publicKey !== 'object' || publicKey === null) {
|
|
246
|
+
throw new InvalidRequestBodyError({
|
|
247
|
+
requestName,
|
|
248
|
+
detail: 'Operation "publicKey" must be a public key object.',
|
|
249
|
+
pointer: '#/publicKey'
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
if (publicKey.type !== key.type) {
|
|
253
|
+
throw new InvalidRequestBodyError({
|
|
254
|
+
requestName,
|
|
255
|
+
detail:
|
|
256
|
+
`The given public key type "${publicKey.type}" does not match the` +
|
|
257
|
+
` key agreement key's type "${key.type}".`,
|
|
258
|
+
pointer: '#/publicKey/type'
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
const keyPair = await X25519KeyAgreementKey2020.from({ ...key })
|
|
262
|
+
let secret: Uint8Array
|
|
263
|
+
try {
|
|
264
|
+
secret = await keyPair.deriveSecret({ publicKey })
|
|
265
|
+
} catch {
|
|
266
|
+
// A malformed / wrong-header `publicKeyMultibase` is a client error.
|
|
267
|
+
throw new InvalidRequestBodyError({
|
|
268
|
+
requestName,
|
|
269
|
+
detail: 'Operation "publicKey.publicKeyMultibase" is not a valid key.',
|
|
270
|
+
pointer: '#/publicKey/publicKeyMultibase'
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
return { secret: Buffer.from(secret).toString('base64url') }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Computes the HMAC-SHA-256 of `verifyData` under the key's secret. */
|
|
277
|
+
function hmacDigest({
|
|
278
|
+
key,
|
|
279
|
+
operation,
|
|
280
|
+
requestName
|
|
281
|
+
}: {
|
|
282
|
+
key: KmsStoredKey
|
|
283
|
+
operation: Record<string, unknown>
|
|
284
|
+
requestName: string
|
|
285
|
+
}): Buffer {
|
|
286
|
+
const data = decodeBase64url({
|
|
287
|
+
value: operation.verifyData,
|
|
288
|
+
field: 'verifyData',
|
|
289
|
+
requestName
|
|
290
|
+
})
|
|
291
|
+
const secret = Buffer.from(key.secret as string, 'base64url')
|
|
292
|
+
return createHmac('sha256', secret).update(data).digest()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** SignOperation on an HMAC key. */
|
|
296
|
+
async function hmacSign({
|
|
297
|
+
key,
|
|
298
|
+
operation
|
|
299
|
+
}: {
|
|
300
|
+
key: KmsStoredKey
|
|
301
|
+
operation: Record<string, unknown>
|
|
302
|
+
}): Promise<{ signatureValue: string }> {
|
|
303
|
+
const digest = hmacDigest({ key, operation, requestName: 'Sign Operation' })
|
|
304
|
+
return { signatureValue: digest.toString('base64url') }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* VerifyOperation on an HMAC key -- the one served verify (a client cannot
|
|
309
|
+
* check an HMAC without the symmetric secret). A wrong-*length*
|
|
310
|
+
* `signatureValue` is an ordinary `verified: false`, checked before
|
|
311
|
+
* `timingSafeEqual` (which throws on mismatched lengths).
|
|
312
|
+
*/
|
|
313
|
+
async function hmacVerify({
|
|
314
|
+
key,
|
|
315
|
+
operation
|
|
316
|
+
}: {
|
|
317
|
+
key: KmsStoredKey
|
|
318
|
+
operation: Record<string, unknown>
|
|
319
|
+
}): Promise<{ verified: boolean }> {
|
|
320
|
+
const requestName = 'Verify Operation'
|
|
321
|
+
const signature = decodeBase64url({
|
|
322
|
+
value: operation.signatureValue,
|
|
323
|
+
field: 'signatureValue',
|
|
324
|
+
requestName
|
|
325
|
+
})
|
|
326
|
+
const expected = hmacDigest({ key, operation, requestName })
|
|
327
|
+
const verified =
|
|
328
|
+
signature.length === expected.length && timingSafeEqual(signature, expected)
|
|
329
|
+
return { verified }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** WrapKeyOperation on an AES-KW key encryption key. */
|
|
333
|
+
async function aesWrapKey({
|
|
334
|
+
key,
|
|
335
|
+
operation
|
|
336
|
+
}: {
|
|
337
|
+
key: KmsStoredKey
|
|
338
|
+
operation: Record<string, unknown>
|
|
339
|
+
}): Promise<{ wrappedKey: string }> {
|
|
340
|
+
const unwrapped = decodeBase64url({
|
|
341
|
+
value: operation.unwrappedKey,
|
|
342
|
+
field: 'unwrappedKey',
|
|
343
|
+
requestName: 'Wrap Key Operation'
|
|
344
|
+
})
|
|
345
|
+
const secret = Buffer.from(key.secret as string, 'base64url')
|
|
346
|
+
const cipher = createCipheriv(AES_KW_ALGORITHM, secret, AES_KW_DEFAULT_IV)
|
|
347
|
+
const wrapped = Buffer.concat([cipher.update(unwrapped), cipher.final()])
|
|
348
|
+
return { wrappedKey: wrapped.toString('base64url') }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* UnwrapKeyOperation on an AES-KW key encryption key. A failed unwrap -- the
|
|
353
|
+
* RFC 3394 integrity check rejecting the ciphertext (wrong KEK, corrupted
|
|
354
|
+
* `wrappedKey`) -- resolves `unwrappedKey: null` rather than erroring: that is
|
|
355
|
+
* the client's documented contract (`Kek.unwrapKey` resolves null when the key
|
|
356
|
+
* does not match).
|
|
357
|
+
*/
|
|
358
|
+
async function aesUnwrapKey({
|
|
359
|
+
key,
|
|
360
|
+
operation
|
|
361
|
+
}: {
|
|
362
|
+
key: KmsStoredKey
|
|
363
|
+
operation: Record<string, unknown>
|
|
364
|
+
}): Promise<{ unwrappedKey: string | null }> {
|
|
365
|
+
const wrapped = decodeBase64url({
|
|
366
|
+
value: operation.wrappedKey,
|
|
367
|
+
field: 'wrappedKey',
|
|
368
|
+
requestName: 'Unwrap Key Operation'
|
|
369
|
+
})
|
|
370
|
+
const secret = Buffer.from(key.secret as string, 'base64url')
|
|
371
|
+
try {
|
|
372
|
+
const decipher = createDecipheriv(
|
|
373
|
+
AES_KW_ALGORITHM,
|
|
374
|
+
secret,
|
|
375
|
+
AES_KW_DEFAULT_IV
|
|
376
|
+
)
|
|
377
|
+
const unwrapped = Buffer.concat([
|
|
378
|
+
decipher.update(wrapped),
|
|
379
|
+
decipher.final()
|
|
380
|
+
])
|
|
381
|
+
return { unwrappedKey: unwrapped.toString('base64url') }
|
|
382
|
+
} catch {
|
|
383
|
+
return { unwrappedKey: null }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The operation dispatch table: which operation envelope `type`s this module
|
|
389
|
+
* serves for each key type, and the pure-crypto implementation for each pair.
|
|
390
|
+
* Deliberate omissions (custody is the criterion): `VerifyOperation` on the
|
|
391
|
+
* asymmetric and key-agreement types (verify client-locally against the
|
|
392
|
+
* description's `publicKeyMultibase`), and every cross-type combination.
|
|
393
|
+
*/
|
|
394
|
+
const KEY_OPERATIONS: Record<
|
|
395
|
+
string,
|
|
396
|
+
Record<
|
|
397
|
+
string,
|
|
398
|
+
(options: {
|
|
399
|
+
key: KmsStoredKey
|
|
400
|
+
operation: Record<string, unknown>
|
|
401
|
+
}) => Promise<object>
|
|
402
|
+
>
|
|
403
|
+
> = {
|
|
404
|
+
Ed25519VerificationKey2020: { SignOperation: ed25519Sign },
|
|
405
|
+
X25519KeyAgreementKey2020: { DeriveSecretOperation: x25519DeriveSecret },
|
|
406
|
+
Sha256HmacKey2019: { SignOperation: hmacSign, VerifyOperation: hmacVerify },
|
|
407
|
+
AesKeyWrappingKey2019: {
|
|
408
|
+
WrapKeyOperation: aesWrapKey,
|
|
409
|
+
UnwrapKeyOperation: aesUnwrapKey
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Runs a key operation envelope against a stored key, dispatching on
|
|
415
|
+
* `(key.type, operation.type)`. An operation this module does not serve for
|
|
416
|
+
* the key's type -- or does not recognize at all -- is a clean 400
|
|
417
|
+
* `UnsupportedKeyOperationError`.
|
|
418
|
+
*
|
|
419
|
+
* @param options {object}
|
|
420
|
+
* @param options.key {KmsStoredKey} the stored (secret-bearing) key
|
|
421
|
+
* @param options.operation {Record<string, unknown>} the validated envelope
|
|
422
|
+
* @returns {Promise<object>} the operation's wire result (`signatureValue` /
|
|
423
|
+
* `verified` / `secret` / `wrappedKey` / `unwrappedKey`)
|
|
424
|
+
*/
|
|
425
|
+
export async function runKeyOperation({
|
|
426
|
+
key,
|
|
427
|
+
operation
|
|
428
|
+
}: {
|
|
429
|
+
key: KmsStoredKey
|
|
430
|
+
operation: Record<string, unknown>
|
|
431
|
+
}): Promise<object> {
|
|
432
|
+
const operationType = operation.type as string
|
|
433
|
+
// `Object.hasOwn` at both levels: `key.type` and `operationType` are both
|
|
434
|
+
// client-influenced, so plain-object indexing could otherwise resolve an
|
|
435
|
+
// inherited `Object` member (`constructor`, `toString`, ...) to a truthy
|
|
436
|
+
// function and bypass this fail-closed gate.
|
|
437
|
+
const operations = Object.hasOwn(KEY_OPERATIONS, key.type)
|
|
438
|
+
? KEY_OPERATIONS[key.type]
|
|
439
|
+
: undefined
|
|
440
|
+
const runOperation =
|
|
441
|
+
operations && Object.hasOwn(operations, operationType)
|
|
442
|
+
? operations[operationType]
|
|
443
|
+
: undefined
|
|
444
|
+
if (runOperation === undefined) {
|
|
445
|
+
throw new UnsupportedKeyOperationError({
|
|
446
|
+
operationType,
|
|
447
|
+
keyType: key.type
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
return await runOperation({ key, operation })
|
|
451
|
+
}
|