@andrewheberle/serverless-ssh-ca 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -1
- package/dist/api/operations.d.ts +50 -0
- package/dist/api/operations.d.ts.map +1 -0
- package/dist/api/operations.js +264 -0
- package/dist/api/operations.js.map +1 -0
- package/dist/api/schema.d.ts +72 -0
- package/dist/api/schema.d.ts.map +1 -0
- package/dist/api/schema.js +114 -0
- package/dist/api/schema.js.map +1 -0
- package/dist/api/v3/index.d.ts.map +1 -1
- package/dist/api/v3/index.js +13 -202
- package/dist/api/v3/index.js.map +1 -1
- package/dist/api/v3/schema.d.ts +23 -68
- package/dist/api/v3/schema.d.ts.map +1 -1
- package/dist/api/v3/schema.js +18 -104
- package/dist/api/v3/schema.js.map +1 -1
- package/dist/api/v4/index.d.ts +8 -0
- package/dist/api/v4/index.d.ts.map +1 -0
- package/dist/api/v4/index.js +155 -0
- package/dist/api/v4/index.js.map +1 -0
- package/dist/api/v4/schema.d.ts +268 -0
- package/dist/api/v4/schema.d.ts.map +1 -0
- package/dist/api/v4/schema.js +186 -0
- package/dist/api/v4/schema.js.map +1 -0
- package/dist/certificate.d.ts +9 -6
- package/dist/certificate.d.ts.map +1 -1
- package/dist/certificate.js +55 -99
- package/dist/certificate.js.map +1 -1
- package/dist/db/index.d.ts +68 -9
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +165 -49
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
- package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
- package/dist/db/migrations/0003_krl_revision.js +14 -0
- package/dist/db/migrations/0003_krl_revision.js.map +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/types.d.ts +2 -0
- package/dist/db/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/krl.d.ts +3 -2
- package/dist/krl.d.ts.map +1 -1
- package/dist/krl.js +12 -165
- package/dist/krl.js.map +1 -1
- package/dist/node/cache.d.ts +21 -0
- package/dist/node/cache.d.ts.map +1 -0
- package/dist/node/cache.js +49 -0
- package/dist/node/cache.js.map +1 -0
- package/dist/node/env.d.ts +6 -3
- package/dist/node/env.d.ts.map +1 -1
- package/dist/node/env.js +4 -3
- package/dist/node/env.js.map +1 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -0
- package/dist/node/index.js.map +1 -1
- package/dist/proof.d.ts +36 -7
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +53 -21
- package/dist/proof.js.map +1 -1
- package/dist/router.d.ts +25 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +41 -2
- package/dist/router.js.map +1 -1
- package/dist/ssh/certificate.d.ts +93 -0
- package/dist/ssh/certificate.d.ts.map +1 -0
- package/dist/ssh/certificate.js +178 -0
- package/dist/ssh/certificate.js.map +1 -0
- package/dist/ssh/encoding.d.ts +41 -0
- package/dist/ssh/encoding.d.ts.map +1 -0
- package/dist/ssh/encoding.js +70 -0
- package/dist/ssh/encoding.js.map +1 -0
- package/dist/ssh/keys.d.ts +92 -0
- package/dist/ssh/keys.d.ts.map +1 -0
- package/dist/ssh/keys.js +277 -0
- package/dist/ssh/keys.js.map +1 -0
- package/dist/ssh/signer.d.ts +35 -0
- package/dist/ssh/signer.d.ts.map +1 -0
- package/dist/ssh/signer.js +99 -0
- package/dist/ssh/signer.js.map +1 -0
- package/dist/types.d.ts +64 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +107 -21
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +251 -65
- package/dist/utils.js.map +1 -1
- package/dist/verify.d.ts +17 -5
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +47 -9
- package/dist/verify.js.map +1 -1
- package/openapi.json +1942 -1
- package/package.json +4 -3
- package/src/api/operations.ts +314 -0
- package/src/api/schema.ts +146 -0
- package/src/api/v3/index.ts +21 -248
- package/src/api/v3/schema.ts +36 -128
- package/src/api/v4/index.ts +266 -0
- package/src/api/v4/schema.ts +241 -0
- package/src/certificate.ts +62 -123
- package/src/db/index.ts +196 -56
- package/src/db/migrations/0003_krl_revision.ts +15 -0
- package/src/db/migrations/index.ts +2 -0
- package/src/db/types.ts +2 -0
- package/src/index.ts +6 -4
- package/src/krl.ts +13 -197
- package/src/node/cache.ts +62 -0
- package/src/node/env.ts +6 -5
- package/src/node/index.ts +1 -0
- package/src/proof.ts +65 -31
- package/src/router.ts +67 -3
- package/src/ssh/certificate.ts +236 -0
- package/src/ssh/encoding.ts +81 -0
- package/src/ssh/keys.ts +327 -0
- package/src/ssh/signer.ts +129 -0
- package/src/types.ts +60 -20
- package/src/utils.ts +285 -81
- package/src/verify.ts +54 -14
package/src/api/v3/index.ts
CHANGED
|
@@ -1,43 +1,21 @@
|
|
|
1
1
|
import { group } from "@andrewheberle/ts-slog"
|
|
2
2
|
import {
|
|
3
|
-
ApiException,
|
|
4
|
-
ConflictException,
|
|
5
3
|
ForbiddenException,
|
|
6
4
|
fromHono,
|
|
7
|
-
InternalServerErrorException,
|
|
8
|
-
NotFoundException,
|
|
9
5
|
OpenAPIRoute,
|
|
10
6
|
UnauthorizedException,
|
|
11
|
-
UnprocessableEntityException,
|
|
12
7
|
} from "chanfana"
|
|
13
8
|
import { Hono } from "hono"
|
|
14
9
|
import { AppContext } from "../../router.js"
|
|
15
|
-
import { CertificateSignerResponse } from "../../types.js"
|
|
16
10
|
import {
|
|
17
|
-
BadIssuerError,
|
|
18
11
|
CreateCertificateOptions,
|
|
19
12
|
CreateHostCertificateOptions,
|
|
20
13
|
createSignedCertificate,
|
|
21
14
|
createSignedHostCertificate,
|
|
22
|
-
|
|
23
15
|
} from "../../certificate.js"
|
|
24
|
-
import {
|
|
25
|
-
getPrivateKey,
|
|
26
|
-
getPublic,
|
|
27
|
-
split,
|
|
28
|
-
UnsupportedKeyError,
|
|
29
|
-
} from "../../utils.js"
|
|
30
|
-
import sshpk from "sshpk"
|
|
16
|
+
import { split } from "../../utils.js"
|
|
31
17
|
import { CertificateType } from "../../db/types.js"
|
|
32
|
-
import {
|
|
33
|
-
getKrlVersion,
|
|
34
|
-
getRevocationList,
|
|
35
|
-
recordCertificate,
|
|
36
|
-
RevocationStatus,
|
|
37
|
-
revocationStatus,
|
|
38
|
-
revokeCertificate,
|
|
39
|
-
} from "../../db/index.js"
|
|
40
|
-
import { KRLBuilder } from "../../krl.js"
|
|
18
|
+
import { caPublicKey, issueCertificate, revocationList, revoke } from "../operations.js"
|
|
41
19
|
import {
|
|
42
20
|
CaPublicKeyEndpointSchema,
|
|
43
21
|
createUserCertificateRequestEndpointSchema,
|
|
@@ -50,9 +28,6 @@ import {
|
|
|
50
28
|
import { logger } from "../../logger.js"
|
|
51
29
|
import type { SshCaBindings } from "../../types.js"
|
|
52
30
|
|
|
53
|
-
// sshpk is CommonJS and Node's ESM loader cannot detect most of its named exports, so values are taken from the default import
|
|
54
|
-
const { KeyParseError } = sshpk
|
|
55
|
-
|
|
56
31
|
const CaPublicKeyEndpoint = (env: SshCaBindings) => {
|
|
57
32
|
return class extends OpenAPIRoute {
|
|
58
33
|
override schema = CaPublicKeyEndpointSchema
|
|
@@ -61,27 +36,7 @@ const CaPublicKeyEndpoint = (env: SshCaBindings) => {
|
|
|
61
36
|
const l = logger(env).with(
|
|
62
37
|
...group("request", "action", "ca"),
|
|
63
38
|
)
|
|
64
|
-
|
|
65
|
-
const pub = await getPublic(c.env)
|
|
66
|
-
|
|
67
|
-
return c.text(`${pub.toString("ssh").trim()}\n`)
|
|
68
|
-
} catch (err) {
|
|
69
|
-
switch (true) {
|
|
70
|
-
case (err instanceof UnsupportedKeyError):
|
|
71
|
-
l.error(err.message)
|
|
72
|
-
throw new InternalServerErrorException(err.message)
|
|
73
|
-
case (err instanceof KeyParseError):
|
|
74
|
-
l.error("error parsing private key")
|
|
75
|
-
throw new InternalServerErrorException("error parsing private key")
|
|
76
|
-
case (err instanceof ApiException):
|
|
77
|
-
// re-throw any exisiting chanfana error
|
|
78
|
-
throw err
|
|
79
|
-
default:
|
|
80
|
-
// otherwise throw as InternalServerErrorException
|
|
81
|
-
l.error("unhandled error", "error", err)
|
|
82
|
-
throw new InternalServerErrorException("internal server error")
|
|
83
|
-
}
|
|
84
|
-
}
|
|
39
|
+
return await caPublicKey(c, l)
|
|
85
40
|
}
|
|
86
41
|
}
|
|
87
42
|
}
|
|
@@ -118,42 +73,10 @@ const UserCertificateRequestEndpoint = (env: SshCaBindings) => {
|
|
|
118
73
|
extensions: data.body.extensions,
|
|
119
74
|
}
|
|
120
75
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
await recordCertificate(c.env, certificate, data.headers.Authorization.email)
|
|
129
|
-
} catch (err) {
|
|
130
|
-
// a certificate that is not recorded cannot be revoked, so do not issue it
|
|
131
|
-
l.error("there was a problem adding issued certificate to database", "error", err)
|
|
132
|
-
throw new InternalServerErrorException("there was a problem recording the issued certificate")
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
l.info("completed issuing certificate",
|
|
136
|
-
...group(
|
|
137
|
-
"certificate", "principals",
|
|
138
|
-
certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0),
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return c.json(response)
|
|
143
|
-
} catch (err) {
|
|
144
|
-
switch (true) {
|
|
145
|
-
case (err instanceof UnsupportedKeyError):
|
|
146
|
-
l.error(err.message)
|
|
147
|
-
throw new InternalServerErrorException(err.message)
|
|
148
|
-
case (err instanceof ApiException):
|
|
149
|
-
// re-throw any exisiting chanfana error
|
|
150
|
-
throw err
|
|
151
|
-
default:
|
|
152
|
-
// otherwise throw as InternalServerErrorException
|
|
153
|
-
l.error("unhandled error", "error", err)
|
|
154
|
-
throw new InternalServerErrorException("internal server error")
|
|
155
|
-
}
|
|
156
|
-
}
|
|
76
|
+
return await issueCertificate(c, l,
|
|
77
|
+
() => createSignedCertificate(c.env, data.headers.Authorization.email, data.body.public_key, opts),
|
|
78
|
+
CertificateType.User,
|
|
79
|
+
)
|
|
157
80
|
}
|
|
158
81
|
}
|
|
159
82
|
}
|
|
@@ -170,53 +93,7 @@ const RevocationListEndpoint = (env: SshCaBindings, certificateType: Certificate
|
|
|
170
93
|
),
|
|
171
94
|
)
|
|
172
95
|
|
|
173
|
-
|
|
174
|
-
// get revoked serials
|
|
175
|
-
const serials = (await getRevocationList(c.env, certificateType)).map(v => BigInt(v))
|
|
176
|
-
|
|
177
|
-
// grab private key from secret store
|
|
178
|
-
const key = await getPrivateKey(c.env)
|
|
179
|
-
|
|
180
|
-
// generate KRL
|
|
181
|
-
const krl = new KRLBuilder(key)
|
|
182
|
-
.addSerials(serials)
|
|
183
|
-
|
|
184
|
-
// the version only changes when the content of the KRL does
|
|
185
|
-
const version = await getKrlVersion(c.env, certificateType, await krl.digest())
|
|
186
|
-
|
|
187
|
-
const krlBytes = krl.generate(version)
|
|
188
|
-
|
|
189
|
-
// convert to base64
|
|
190
|
-
const bytes = new Uint8Array(krlBytes)
|
|
191
|
-
let binary = ""
|
|
192
|
-
for (const byte of bytes) {
|
|
193
|
-
binary += String.fromCharCode(byte)
|
|
194
|
-
}
|
|
195
|
-
const base64Krl = btoa(binary)
|
|
196
|
-
|
|
197
|
-
const signature = await krl.signature()
|
|
198
|
-
|
|
199
|
-
return c.json({
|
|
200
|
-
krl: base64Krl,
|
|
201
|
-
signature: signature,
|
|
202
|
-
})
|
|
203
|
-
} catch (err) {
|
|
204
|
-
switch (true) {
|
|
205
|
-
case (err instanceof UnsupportedKeyError):
|
|
206
|
-
l.error(err.message)
|
|
207
|
-
throw new InternalServerErrorException(err.message)
|
|
208
|
-
case (err instanceof KeyParseError):
|
|
209
|
-
l.error("error parsing private key")
|
|
210
|
-
throw new InternalServerErrorException("error parsing private key")
|
|
211
|
-
case (err instanceof ApiException):
|
|
212
|
-
// re-throw any exisiting chanfana error
|
|
213
|
-
throw err
|
|
214
|
-
default:
|
|
215
|
-
// otherwise throw as InternalServerErrorException
|
|
216
|
-
l.error("unhandled error", "error", err)
|
|
217
|
-
throw new InternalServerErrorException("internal server error")
|
|
218
|
-
}
|
|
219
|
-
}
|
|
96
|
+
return await revocationList(c, l, certificateType)
|
|
220
97
|
}
|
|
221
98
|
}
|
|
222
99
|
}
|
|
@@ -259,42 +136,10 @@ const HostCertificateRequestEndpoint = (env: SshCaBindings) => {
|
|
|
259
136
|
principals: data.body.principals,
|
|
260
137
|
}
|
|
261
138
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
await recordCertificate(c.env, certificate, `host_${certificate.subjects[0]!.hostname}`, CertificateType.Host)
|
|
270
|
-
} catch (err) {
|
|
271
|
-
// a certificate that is not recorded cannot be revoked, so do not issue it
|
|
272
|
-
l.error("there was a problem adding issued certificate to database", "error", err)
|
|
273
|
-
throw new InternalServerErrorException("there was a problem recording the issued certificate")
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
l.info("completed issuing certificate",
|
|
277
|
-
...group(
|
|
278
|
-
"certificate", "principals",
|
|
279
|
-
certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0),
|
|
280
|
-
),
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
return c.json(response)
|
|
284
|
-
} catch (err) {
|
|
285
|
-
switch (true) {
|
|
286
|
-
case (err instanceof UnsupportedKeyError):
|
|
287
|
-
l.error(err.message)
|
|
288
|
-
throw new InternalServerErrorException(err.message)
|
|
289
|
-
case (err instanceof ApiException):
|
|
290
|
-
// re-throw any exisiting chanfana error
|
|
291
|
-
throw err
|
|
292
|
-
default:
|
|
293
|
-
// otherwise throw as InternalServerErrorException
|
|
294
|
-
l.error("unhandled error", "error", err)
|
|
295
|
-
throw new InternalServerErrorException("internal server error")
|
|
296
|
-
}
|
|
297
|
-
}
|
|
139
|
+
return await issueCertificate(c, l,
|
|
140
|
+
() => createSignedHostCertificate(c.env, data.body.public_key, opts),
|
|
141
|
+
CertificateType.Host,
|
|
142
|
+
)
|
|
298
143
|
}
|
|
299
144
|
}
|
|
300
145
|
}
|
|
@@ -305,7 +150,7 @@ const HostCertificateRenewEndpoint = (env: SshCaBindings) => {
|
|
|
305
150
|
|
|
306
151
|
override async handle(c: AppContext) {
|
|
307
152
|
const data = await this.getValidatedData<typeof this.schema>()
|
|
308
|
-
const serial = data.body.certificate.serial
|
|
153
|
+
const serial = data.body.certificate.serial
|
|
309
154
|
|
|
310
155
|
const l = logger(env).with(
|
|
311
156
|
...group("request",
|
|
@@ -316,54 +161,21 @@ const HostCertificateRenewEndpoint = (env: SshCaBindings) => {
|
|
|
316
161
|
)
|
|
317
162
|
|
|
318
163
|
// use smaller of the current certificate lifetime and the requested lifetime
|
|
319
|
-
const originalLifetime = (data.body.certificate.
|
|
164
|
+
const originalLifetime = (data.body.certificate.validBefore.getTime() - data.body.certificate.validAfter.getTime()) / 1000
|
|
320
165
|
const lifetime = originalLifetime > data.body.lifetime
|
|
321
166
|
? data.body.lifetime
|
|
322
167
|
: originalLifetime
|
|
323
168
|
|
|
324
169
|
const opts: CreateHostCertificateOptions = {
|
|
325
170
|
lifetime: lifetime,
|
|
326
|
-
|
|
171
|
+
principals: data.body.certificate.principals,
|
|
327
172
|
}
|
|
328
173
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
await recordCertificate(c.env, certificate, `host_${certificate.subjects[0]!.hostname}`, CertificateType.Host)
|
|
337
|
-
} catch (err) {
|
|
338
|
-
// a certificate that is not recorded cannot be revoked, so do not issue it
|
|
339
|
-
l.error("there was a problem adding issued certificate to database", "error", err)
|
|
340
|
-
throw new InternalServerErrorException("there was a problem recording the issued certificate")
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
l.info("completed issuing certificate",
|
|
344
|
-
...group(
|
|
345
|
-
"certificate", "principals",
|
|
346
|
-
certificate.subjects.map(v => v.toString()).join(","), "serial", certificate.serial.readBigUInt64BE(0),
|
|
347
|
-
),
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
return c.json(response)
|
|
351
|
-
} catch (err) {
|
|
352
|
-
switch (true) {
|
|
353
|
-
case (err instanceof UnsupportedKeyError):
|
|
354
|
-
l.error(err.message)
|
|
355
|
-
throw new InternalServerErrorException(err.message)
|
|
356
|
-
case (err instanceof ApiException):
|
|
357
|
-
// re-throw any exisiting chanfana error
|
|
358
|
-
throw err
|
|
359
|
-
case (err instanceof BadIssuerError):
|
|
360
|
-
throw new UnprocessableEntityException(err.message)
|
|
361
|
-
default:
|
|
362
|
-
// otherwise throw as InternalServerErrorException
|
|
363
|
-
l.error("unhandled error", "error", err)
|
|
364
|
-
throw new InternalServerErrorException("internal server error")
|
|
365
|
-
}
|
|
366
|
-
}
|
|
174
|
+
return await issueCertificate(c, l,
|
|
175
|
+
() => createSignedHostCertificate(c.env, data.body.public_key, opts),
|
|
176
|
+
CertificateType.Host,
|
|
177
|
+
data.body.certificate,
|
|
178
|
+
)
|
|
367
179
|
}
|
|
368
180
|
}
|
|
369
181
|
}
|
|
@@ -379,46 +191,7 @@ const RevokeCertificateEndpoint = (env: SshCaBindings, certificateType: Certific
|
|
|
379
191
|
...group("request", "type", certificateTypeName(certificateType), "action", "revoke", "serial", data.body.serial),
|
|
380
192
|
)
|
|
381
193
|
|
|
382
|
-
|
|
383
|
-
const status = await revocationStatus(c.env, data.body.serial, data.body.public_key.toString("ssh"), certificateType)
|
|
384
|
-
switch (status) {
|
|
385
|
-
case RevocationStatus.Revoked:
|
|
386
|
-
l.warn("certificate is already revoked")
|
|
387
|
-
throw new ConflictException("certificate is already revoked")
|
|
388
|
-
case RevocationStatus.Unrevokable:
|
|
389
|
-
l.warn("cannot revoke this certificate")
|
|
390
|
-
throw new ForbiddenException("cannot revoke this certificate")
|
|
391
|
-
case RevocationStatus.NotFound:
|
|
392
|
-
l.warn("certificate not found")
|
|
393
|
-
throw new NotFoundException("certificate not found")
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
try {
|
|
397
|
-
const res = await revokeCertificate(c.env, data.body.serial)
|
|
398
|
-
|
|
399
|
-
if (res.length === 0) {
|
|
400
|
-
throw new InternalServerErrorException("no results were returned for revocation")
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (res.length > 1) {
|
|
404
|
-
l.warn("more than one revocation result was returned", "count", res.length)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
l.info("completed revocation")
|
|
408
|
-
return c.json({
|
|
409
|
-
revoked_at: res[0]!.revoked_at
|
|
410
|
-
})
|
|
411
|
-
} catch (err) {
|
|
412
|
-
switch (true) {
|
|
413
|
-
case (err instanceof ApiException):
|
|
414
|
-
// re-throw any exisiting chanfana error
|
|
415
|
-
throw err
|
|
416
|
-
default:
|
|
417
|
-
// otherwise throw as InternalServerErrorException
|
|
418
|
-
l.error("unhandled error", "error", err)
|
|
419
|
-
throw new InternalServerErrorException("internal server error")
|
|
420
|
-
}
|
|
421
|
-
}
|
|
194
|
+
return await revoke(c, l, data.body.serial, data.body.public_key, certificateType)
|
|
422
195
|
}
|
|
423
196
|
}
|
|
424
197
|
}
|
package/src/api/v3/schema.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import z from "zod"
|
|
2
2
|
import type { SshCaBindings } from "../../types.js"
|
|
3
3
|
import {
|
|
4
|
+
refineHostPrincipal,
|
|
4
5
|
split,
|
|
5
6
|
refineCertificateRequest,
|
|
6
|
-
transformAuthorizationHeader,
|
|
7
7
|
transformProofOfPossession,
|
|
8
|
-
transformPublicKey,
|
|
9
8
|
transformRenewalProofOfPossession,
|
|
10
9
|
refineHostCertificateRequest,
|
|
11
10
|
transformCertificate,
|
|
12
11
|
refineHostCertificateRenewal,
|
|
13
12
|
refineRevokeCertificate,
|
|
14
|
-
transformIdentityToken,
|
|
15
13
|
} from "../../utils.js"
|
|
16
14
|
import {
|
|
17
15
|
ConflictException,
|
|
@@ -19,121 +17,37 @@ import {
|
|
|
19
17
|
ForbiddenException,
|
|
20
18
|
InputValidationException,
|
|
21
19
|
InternalServerErrorException,
|
|
20
|
+
TooManyRequestsException,
|
|
22
21
|
UnauthorizedException,
|
|
23
22
|
UnprocessableEntityException,
|
|
24
23
|
} from "chanfana"
|
|
25
24
|
import { seconds } from "itty-time"
|
|
26
25
|
import { isRevoked } from "../../db/index.js"
|
|
27
26
|
import { CertificateType } from "../../db/types.js"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* Returns the OpenAPI tag that groups the endpoints for a {@link CertificateType}
|
|
49
|
-
* @param certificateType The type of certificate
|
|
50
|
-
* @returns "User" or "Host"
|
|
51
|
-
*/
|
|
52
|
-
const certificateTypeTag = (certificateType: CertificateType): "User" | "Host" => {
|
|
53
|
-
switch (certificateType) {
|
|
54
|
-
case CertificateType.User:
|
|
55
|
-
return "User"
|
|
56
|
-
case CertificateType.Host:
|
|
57
|
-
return "Host"
|
|
58
|
-
default: {
|
|
59
|
-
const unknownType: never = certificateType
|
|
60
|
-
throw new Error(`unknown certificate type: ${String(unknownType)}`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const openapiStringByte = z.base64()
|
|
66
|
-
.transform((v) => {
|
|
67
|
-
const a = z.util.base64ToUint8Array(v)
|
|
68
|
-
return Buffer.from(
|
|
69
|
-
a.buffer,
|
|
70
|
-
a.byteOffset,
|
|
71
|
-
a.length
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
.openapi({ format: "byte" })
|
|
75
|
-
|
|
76
|
-
const proofOfPossession = z.string()
|
|
77
|
-
.meta({
|
|
78
|
-
description: "Proof of possession comprising of ${timestamp}.${fingerprint}.${signature}",
|
|
79
|
-
example: "1765696780805.SHA256:79mq0wtpQMTvS4+Of8VzLN0qmYWNUyTXYmqKwhEgSLs.LS0tLS1CRUdJTiBTU0ggU0lHTkFUVVJFLS0tLS0KVTFOSVUwbEhBQUFBQVFBQUFHZ0FBQUFUWldOa2MyRXRjMmhoTWkxdWFYTjBjREkxTmdBQUFBaHVhWE4wY0RJMQpOZ0FBQUVFRUR6Ymk4OE10alBYSDFCb1JURU9HaS96aEc4QnhQNVZUUW5OQkFNMGZtMExtekErUUZkUlNVYVRFCkdNN2l0QjhINmZNZFAydlAxcElQcGUyOWVLQkJZd0FBQUFSbWFXeGxBQUFBQUFBQUFBWnphR0UxTVRJQUFBQmsKQUFBQUUyVmpaSE5oTFhOb1lUSXRibWx6ZEhBeU5UWUFBQUJKQUFBQUlHdmZVZGNDQkRXa3ZwL0ZHS3NkakgrRQpKZzZXN0EvWUJVZng2Z0srQ01raEFBQUFJUUQxOFd5cDNZejZ0eXNvdllXbEJHTXprRnlYbFRrSHM5blVVRXJqCkczNWVPQT09Ci0tLS0tRU5EIFNTSCBTSUdOQVRVUkUtLS0tLQo="
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
const publicKey = openapiStringByte
|
|
83
|
-
.transform(transformPublicKey)
|
|
84
|
-
.meta({
|
|
85
|
-
description: "SSH public key to sign",
|
|
86
|
-
example: "ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkdNcFNKdHRmMEl0dE5DVmMyOFR6WEZSMUQweHZPM25wNjdWemVBUXNiZFpza3JEY1lqU2x3SjZIUERtVHBYYVUwbEVhWDlMNFloYy9jQ2YxTWU5RlRrPQo="
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
export const createIdentityTokenSchema = (env: SshCaBindings) => (
|
|
90
|
-
z.string()
|
|
91
|
-
.meta({
|
|
92
|
-
description: "Identity Token JWT from OIDC IdP",
|
|
93
|
-
example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNzc1NzAzMDk1fQ.ri_neAWnhMNK3LzlsrBcQYymSM4yRjmNSZZSeZiXhrEqtEz6c3cXk0Esq765umGjpUsWcosL-OFrDJlyAjTDnhrd9oV08uc_CW0rQRsJIGEuRo3ryxkLdVu9mGoZWEUb9KwjGJrwxvr-0cPWx5jaDyKwJcqMvtV_bEITUD51sDB1Vm89QfYRO_pGJo2vrRzSvMjpUenRpwPay4lYIBxl41_4YpR9Rc6VrIZuYsjV2iqEZ4eBrygMA7zPR_hN7l7s95FddLOzj5NsK57VT4uLHwYohx2oqMzw3M-B9HsZIQin_9q61pZFQXepzJth0woXiZheU27llnfHX967PhNQyg"
|
|
94
|
-
})
|
|
95
|
-
.transform((val, ctx) => transformIdentityToken(env, val, ctx))
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
export const createAccessTokenSchema = (env: SshCaBindings) => (
|
|
99
|
-
z.string()
|
|
100
|
-
.meta({
|
|
101
|
-
description: "Access Token JWT from OIDC IdP",
|
|
102
|
-
example: "Bearer eyJ..."
|
|
103
|
-
})
|
|
104
|
-
.transform((val, ctx) => transformAuthorizationHeader(env, val, ctx))
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
const krl = openapiStringByte
|
|
108
|
-
.meta({ description: "Key Revocation List" })
|
|
109
|
-
|
|
110
|
-
export const createHeaderSchema = (env: SshCaBindings) => (
|
|
111
|
-
z.object({
|
|
112
|
-
"Authorization": createAccessTokenSchema(env)
|
|
113
|
-
})
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
const certificate = openapiStringByte
|
|
117
|
-
.meta({ description: "Issued Certificate" })
|
|
118
|
-
|
|
119
|
-
export const CaPublicKeyEndpointSchema = {
|
|
120
|
-
tags: ["CA"],
|
|
121
|
-
responses: {
|
|
122
|
-
"200": {
|
|
123
|
-
content: {
|
|
124
|
-
"text/plain": {
|
|
125
|
-
schema: z.string()
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
description: "Returns SSH Certificate Authority Public Key",
|
|
129
|
-
},
|
|
130
|
-
...InternalServerErrorException.schema(),
|
|
131
|
-
}
|
|
132
|
-
}
|
|
27
|
+
import {
|
|
28
|
+
certificate,
|
|
29
|
+
certificateTypeName,
|
|
30
|
+
certificateTypeTag,
|
|
31
|
+
createHeaderSchema,
|
|
32
|
+
createIdentityTokenSchema,
|
|
33
|
+
openapiStringByte,
|
|
34
|
+
proofOfPossession,
|
|
35
|
+
publicKey,
|
|
36
|
+
subjectPublicKey,
|
|
37
|
+
} from "../schema.js"
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
CaPublicKeyEndpointSchema,
|
|
41
|
+
certificateTypeName,
|
|
42
|
+
createAccessTokenSchema,
|
|
43
|
+
createHeaderSchema,
|
|
44
|
+
createIdentityTokenSchema,
|
|
45
|
+
createRevocationListEndpointSchema,
|
|
46
|
+
} from "../schema.js"
|
|
133
47
|
|
|
134
48
|
export const userCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
|
|
135
49
|
z.object({
|
|
136
|
-
public_key:
|
|
50
|
+
public_key: subjectPublicKey,
|
|
137
51
|
proof: proofOfPossession
|
|
138
52
|
.transform((val, ctx) => transformProofOfPossession(env, val, ctx)),
|
|
139
53
|
identity: createIdentityTokenSchema(env),
|
|
@@ -152,6 +66,8 @@ export const userCertificateRequestEndpointBodySchema = (env: SshCaBindings) =>
|
|
|
152
66
|
|
|
153
67
|
export const createUserCertificateRequestEndpointSchema = (env: SshCaBindings) => ({
|
|
154
68
|
tags: [certificateTypeTag(CertificateType.User)],
|
|
69
|
+
summary: "Request a user certificate",
|
|
70
|
+
description: "Issues a signed SSH user certificate for the provided public key. The request must be authenticated with an access token and include an identity token from the OIDC IdP for the same subject, along with a proof of possession of the private key. Principals on the issued certificate are derived from the identity token.",
|
|
155
71
|
security: [{ oidcAuth: [] }],
|
|
156
72
|
request: {
|
|
157
73
|
headers: createHeaderSchema(env),
|
|
@@ -171,33 +87,17 @@ export const createUserCertificateRequestEndpointSchema = (env: SshCaBindings) =
|
|
|
171
87
|
...InputValidationException.schema(),
|
|
172
88
|
...UnprocessableEntityException.schema(),
|
|
173
89
|
...InternalServerErrorException.schema(),
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
export const createRevocationListEndpointSchema = (certificateType: CertificateType) => ({
|
|
178
|
-
tags: [certificateTypeTag(certificateType)],
|
|
179
|
-
summary: `Get the Key Revocation List for ${certificateTypeName(certificateType)} certificates`,
|
|
180
|
-
responses: {
|
|
181
|
-
"200": {
|
|
182
|
-
description: `Returns an Open SSH Key Revocation List of revoked ${certificateTypeName(certificateType)} certificates as BASE64 and an SSHSIG signature for verification`,
|
|
183
|
-
...contentJson(z.object({
|
|
184
|
-
krl: krl,
|
|
185
|
-
signature: z.string()
|
|
186
|
-
})
|
|
187
|
-
.openapi("Key Revocation List Response")
|
|
188
|
-
)
|
|
189
|
-
},
|
|
190
|
-
...InternalServerErrorException.schema(),
|
|
90
|
+
...TooManyRequestsException.schema(),
|
|
191
91
|
}
|
|
192
92
|
})
|
|
193
93
|
|
|
194
94
|
export const hostCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
|
|
195
95
|
z.object({
|
|
196
|
-
public_key:
|
|
96
|
+
public_key: subjectPublicKey,
|
|
197
97
|
proof: proofOfPossession
|
|
198
98
|
.transform((val, ctx) => transformProofOfPossession(env, val, ctx)),
|
|
199
99
|
identity: createIdentityTokenSchema(env),
|
|
200
|
-
principals: z.array(z.string()).min(1)
|
|
100
|
+
principals: z.array(z.string().superRefine(refineHostPrincipal)).min(1)
|
|
201
101
|
.meta({ description: "List of principals to include on the issued certificate" }),
|
|
202
102
|
lifetime: z.int()
|
|
203
103
|
.min(seconds("24 hours"))
|
|
@@ -211,6 +111,8 @@ export const hostCertificateRequestEndpointBodySchema = (env: SshCaBindings) =>
|
|
|
211
111
|
|
|
212
112
|
export const createHostCertificateRequestEndpointSchema = (env: SshCaBindings) => ({
|
|
213
113
|
tags: [certificateTypeTag(CertificateType.Host)],
|
|
114
|
+
summary: "Request a host certificate",
|
|
115
|
+
description: "Issues a signed SSH host certificate for the provided public key and principals. The request must be authenticated with an access token and include an identity token from the OIDC IdP for the same subject, along with a proof of possession of the private key. The user must be permitted to request host certificates by email address or role.",
|
|
214
116
|
security: [{ oidcAuth: [] }],
|
|
215
117
|
request: {
|
|
216
118
|
headers: createHeaderSchema(env),
|
|
@@ -230,6 +132,7 @@ export const createHostCertificateRequestEndpointSchema = (env: SshCaBindings) =
|
|
|
230
132
|
...InputValidationException.schema(),
|
|
231
133
|
...UnprocessableEntityException.schema(),
|
|
232
134
|
...InternalServerErrorException.schema(),
|
|
135
|
+
...TooManyRequestsException.schema(),
|
|
233
136
|
}
|
|
234
137
|
})
|
|
235
138
|
|
|
@@ -253,6 +156,8 @@ export const hostCertificateRenewEndpointBodySchema = (env: SshCaBindings) => (
|
|
|
253
156
|
|
|
254
157
|
export const createHostCertificateRenewEndpointSchema = (env: SshCaBindings) => ({
|
|
255
158
|
tags: [certificateTypeTag(CertificateType.Host)],
|
|
159
|
+
summary: "Renew a host certificate",
|
|
160
|
+
description: "Issues a new SSH host certificate with the same principals as an existing, unexpired and unrevoked host certificate issued by this CA. No OIDC authentication is required; instead the public key must match the existing certificate and the proof of possession must be made with its private key. The lifetime of the renewed certificate is no longer than that of the existing certificate. The existing certificate is revoked, so it cannot be used or renewed again once the renewed certificate is installed.",
|
|
256
161
|
request: {
|
|
257
162
|
body: contentJson(hostCertificateRenewEndpointBodySchema(env))
|
|
258
163
|
},
|
|
@@ -270,6 +175,7 @@ export const createHostCertificateRenewEndpointSchema = (env: SshCaBindings) =>
|
|
|
270
175
|
...InputValidationException.schema(),
|
|
271
176
|
...UnprocessableEntityException.schema(),
|
|
272
177
|
...InternalServerErrorException.schema(),
|
|
178
|
+
...TooManyRequestsException.schema(),
|
|
273
179
|
}
|
|
274
180
|
})
|
|
275
181
|
|
|
@@ -293,6 +199,7 @@ export const revokeCertificateEndpointBodySchema = (env: SshCaBindings) => (
|
|
|
293
199
|
export const createRevokeCertificateEndpointSchema = (env: SshCaBindings, certificateType: CertificateType) => ({
|
|
294
200
|
tags: [certificateTypeTag(certificateType)],
|
|
295
201
|
summary: `Revoke a ${certificateTypeName(certificateType)} certificate`,
|
|
202
|
+
description: `Revokes a ${certificateTypeName(certificateType)} certificate issued by this CA, identified by its serial number, and adds it to the ${certificateTypeName(certificateType)} Key Revocation List. The request must include a proof of possession of the private key for the public key that the certificate was issued for.`,
|
|
296
203
|
request: {
|
|
297
204
|
body: contentJson(revokeCertificateEndpointBodySchema(env))
|
|
298
205
|
},
|
|
@@ -310,6 +217,7 @@ export const createRevokeCertificateEndpointSchema = (env: SshCaBindings, certif
|
|
|
310
217
|
...InputValidationException.schema(),
|
|
311
218
|
...UnprocessableEntityException.schema(),
|
|
312
219
|
...InternalServerErrorException.schema(),
|
|
220
|
+
...TooManyRequestsException.schema(),
|
|
313
221
|
...ConflictException.schema()
|
|
314
222
|
}
|
|
315
223
|
})
|