@andrewheberle/serverless-ssh-ca 0.22.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 (84) 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 +13 -202
  12. package/dist/api/v3/index.js.map +1 -1
  13. package/dist/api/v3/schema.d.ts +23 -68
  14. package/dist/api/v3/schema.d.ts.map +1 -1
  15. package/dist/api/v3/schema.js +10 -100
  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 +17 -5
  30. package/dist/db/index.d.ts.map +1 -1
  31. package/dist/db/index.js +39 -23
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/krl.d.ts +1 -1
  34. package/dist/krl.d.ts.map +1 -1
  35. package/dist/krl.js +9 -163
  36. package/dist/krl.js.map +1 -1
  37. package/dist/proof.d.ts +36 -7
  38. package/dist/proof.d.ts.map +1 -1
  39. package/dist/proof.js +53 -21
  40. package/dist/proof.js.map +1 -1
  41. package/dist/router.d.ts.map +1 -1
  42. package/dist/router.js +3 -1
  43. package/dist/router.js.map +1 -1
  44. package/dist/ssh/certificate.d.ts +93 -0
  45. package/dist/ssh/certificate.d.ts.map +1 -0
  46. package/dist/ssh/certificate.js +178 -0
  47. package/dist/ssh/certificate.js.map +1 -0
  48. package/dist/ssh/encoding.d.ts +41 -0
  49. package/dist/ssh/encoding.d.ts.map +1 -0
  50. package/dist/ssh/encoding.js +70 -0
  51. package/dist/ssh/encoding.js.map +1 -0
  52. package/dist/ssh/keys.d.ts +80 -0
  53. package/dist/ssh/keys.d.ts.map +1 -0
  54. package/dist/ssh/keys.js +249 -0
  55. package/dist/ssh/keys.js.map +1 -0
  56. package/dist/ssh/signer.d.ts +35 -0
  57. package/dist/ssh/signer.d.ts.map +1 -0
  58. package/dist/ssh/signer.js +99 -0
  59. package/dist/ssh/signer.js.map +1 -0
  60. package/dist/types.d.ts +12 -17
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/utils.d.ts +40 -21
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +87 -54
  65. package/dist/utils.js.map +1 -1
  66. package/openapi.json +1542 -1
  67. package/package.json +4 -3
  68. package/src/api/operations.ts +211 -0
  69. package/src/api/schema.ts +137 -0
  70. package/src/api/v3/index.ts +20 -248
  71. package/src/api/v3/schema.ts +26 -125
  72. package/src/api/v4/index.ts +265 -0
  73. package/src/api/v4/schema.ts +235 -0
  74. package/src/certificate.ts +62 -123
  75. package/src/db/index.ts +41 -26
  76. package/src/krl.ts +10 -195
  77. package/src/proof.ts +65 -31
  78. package/src/router.ts +3 -1
  79. package/src/ssh/certificate.ts +236 -0
  80. package/src/ssh/encoding.ts +81 -0
  81. package/src/ssh/keys.ts +294 -0
  82. package/src/ssh/signer.ts +129 -0
  83. package/src/types.ts +12 -18
  84. package/src/utils.ts +99 -69
@@ -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
- try {
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
- try {
122
- const certificate = await createSignedCertificate(c.env, data.headers.Authorization.email, data.body.public_key, opts)
123
- const response: CertificateSignerResponse = {
124
- certificate: btoa(certificate.toString("openssh"))
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
- try {
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
- try {
263
- const certificate = await createSignedHostCertificate(c.env, data.body.public_key, opts)
264
- const response: CertificateSignerResponse = {
265
- certificate: btoa(certificate.toString("openssh"))
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.readBigUInt64BE(0)
153
+ const serial = data.body.certificate.serial
309
154
 
310
155
  const l = logger(env).with(
311
156
  ...group("request",
@@ -316,54 +161,20 @@ 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.validUntil.getTime() - data.body.certificate.validFrom.getTime()) / 1000
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
- subjects: data.body.certificate.subjects
171
+ principals: data.body.certificate.principals,
327
172
  }
328
173
 
329
- try {
330
- const certificate = await createSignedHostCertificate(c.env, data.body.public_key, opts)
331
- const response: CertificateSignerResponse = {
332
- certificate: btoa(certificate.toString("openssh"))
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
+ )
367
178
  }
368
179
  }
369
180
  }
@@ -379,46 +190,7 @@ const RevokeCertificateEndpoint = (env: SshCaBindings, certificateType: Certific
379
190
  ...group("request", "type", certificateTypeName(certificateType), "action", "revoke", "serial", data.body.serial),
380
191
  )
381
192
 
382
- // ensure certificate can be revoked
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
- }
193
+ return await revoke(c, l, data.body.serial, data.body.public_key, certificateType)
422
194
  }
423
195
  }
424
196
  }
@@ -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,
@@ -25,111 +22,25 @@ import {
25
22
  import { seconds } from "itty-time"
26
23
  import { isRevoked } from "../../db/index.js"
27
24
  import { CertificateType } from "../../db/types.js"
28
-
29
- /**
30
- * Returns the name of a {@link CertificateType} as used in API paths and logs
31
- * @param certificateType The type of certificate
32
- * @returns "user" or "host"
33
- */
34
- export const certificateTypeName = (certificateType: CertificateType): "user" | "host" => {
35
- switch (certificateType) {
36
- case CertificateType.User:
37
- return "user"
38
- case CertificateType.Host:
39
- return "host"
40
- default: {
41
- const unknownType: never = certificateType
42
- throw new Error(`unknown certificate type: ${String(unknownType)}`)
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
- }
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"
133
44
 
134
45
  export const userCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
135
46
  z.object({
@@ -152,6 +63,8 @@ export const userCertificateRequestEndpointBodySchema = (env: SshCaBindings) =>
152
63
 
153
64
  export const createUserCertificateRequestEndpointSchema = (env: SshCaBindings) => ({
154
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.",
155
68
  security: [{ oidcAuth: [] }],
156
69
  request: {
157
70
  headers: createHeaderSchema(env),
@@ -174,23 +87,6 @@ export const createUserCertificateRequestEndpointSchema = (env: SshCaBindings) =
174
87
  }
175
88
  })
176
89
 
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(),
191
- }
192
- })
193
-
194
90
  export const hostCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
195
91
  z.object({
196
92
  public_key: publicKey,
@@ -211,6 +107,8 @@ export const hostCertificateRequestEndpointBodySchema = (env: SshCaBindings) =>
211
107
 
212
108
  export const createHostCertificateRequestEndpointSchema = (env: SshCaBindings) => ({
213
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.",
214
112
  security: [{ oidcAuth: [] }],
215
113
  request: {
216
114
  headers: createHeaderSchema(env),
@@ -253,6 +151,8 @@ export const hostCertificateRenewEndpointBodySchema = (env: SshCaBindings) => (
253
151
 
254
152
  export const createHostCertificateRenewEndpointSchema = (env: SshCaBindings) => ({
255
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.",
256
156
  request: {
257
157
  body: contentJson(hostCertificateRenewEndpointBodySchema(env))
258
158
  },
@@ -293,6 +193,7 @@ export const revokeCertificateEndpointBodySchema = (env: SshCaBindings) => (
293
193
  export const createRevokeCertificateEndpointSchema = (env: SshCaBindings, certificateType: CertificateType) => ({
294
194
  tags: [certificateTypeTag(certificateType)],
295
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.`,
296
197
  request: {
297
198
  body: contentJson(revokeCertificateEndpointBodySchema(env))
298
199
  },