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