@andrewheberle/serverless-ssh-ca 0.23.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.
Files changed (94) hide show
  1. package/README.md +119 -0
  2. package/dist/api/operations.d.ts +2 -1
  3. package/dist/api/operations.d.ts.map +1 -1
  4. package/dist/api/operations.js +103 -14
  5. package/dist/api/operations.js.map +1 -1
  6. package/dist/api/schema.d.ts +1 -0
  7. package/dist/api/schema.d.ts.map +1 -1
  8. package/dist/api/schema.js +8 -1
  9. package/dist/api/schema.js.map +1 -1
  10. package/dist/api/v3/index.d.ts.map +1 -1
  11. package/dist/api/v3/index.js +1 -1
  12. package/dist/api/v3/index.js.map +1 -1
  13. package/dist/api/v3/schema.d.ts.map +1 -1
  14. package/dist/api/v3/schema.js +11 -7
  15. package/dist/api/v3/schema.js.map +1 -1
  16. package/dist/api/v4/index.d.ts.map +1 -1
  17. package/dist/api/v4/index.js +1 -1
  18. package/dist/api/v4/index.js.map +1 -1
  19. package/dist/api/v4/schema.d.ts.map +1 -1
  20. package/dist/api/v4/schema.js +11 -7
  21. package/dist/api/v4/schema.js.map +1 -1
  22. package/dist/db/index.d.ts +51 -4
  23. package/dist/db/index.d.ts.map +1 -1
  24. package/dist/db/index.js +133 -33
  25. package/dist/db/index.js.map +1 -1
  26. package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
  27. package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
  28. package/dist/db/migrations/0003_krl_revision.js +14 -0
  29. package/dist/db/migrations/0003_krl_revision.js.map +1 -0
  30. package/dist/db/migrations/index.d.ts.map +1 -1
  31. package/dist/db/migrations/index.js +2 -0
  32. package/dist/db/migrations/index.js.map +1 -1
  33. package/dist/db/types.d.ts +2 -0
  34. package/dist/db/types.d.ts.map +1 -1
  35. package/dist/index.d.ts +5 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/krl.d.ts +2 -1
  40. package/dist/krl.d.ts.map +1 -1
  41. package/dist/krl.js +3 -2
  42. package/dist/krl.js.map +1 -1
  43. package/dist/node/cache.d.ts +21 -0
  44. package/dist/node/cache.d.ts.map +1 -0
  45. package/dist/node/cache.js +49 -0
  46. package/dist/node/cache.js.map +1 -0
  47. package/dist/node/env.d.ts +6 -3
  48. package/dist/node/env.d.ts.map +1 -1
  49. package/dist/node/env.js +4 -3
  50. package/dist/node/env.js.map +1 -1
  51. package/dist/node/index.d.ts +1 -0
  52. package/dist/node/index.d.ts.map +1 -1
  53. package/dist/node/index.js +1 -0
  54. package/dist/node/index.js.map +1 -1
  55. package/dist/router.d.ts +25 -1
  56. package/dist/router.d.ts.map +1 -1
  57. package/dist/router.js +38 -1
  58. package/dist/router.js.map +1 -1
  59. package/dist/ssh/keys.d.ts +12 -0
  60. package/dist/ssh/keys.d.ts.map +1 -1
  61. package/dist/ssh/keys.js +28 -0
  62. package/dist/ssh/keys.js.map +1 -1
  63. package/dist/types.d.ts +53 -3
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/utils.d.ts +68 -1
  66. package/dist/utils.d.ts.map +1 -1
  67. package/dist/utils.js +166 -13
  68. package/dist/utils.js.map +1 -1
  69. package/dist/verify.d.ts +17 -5
  70. package/dist/verify.d.ts.map +1 -1
  71. package/dist/verify.js +47 -9
  72. package/dist/verify.js.map +1 -1
  73. package/openapi.json +402 -2
  74. package/package.json +1 -1
  75. package/src/api/operations.ts +115 -12
  76. package/src/api/schema.ts +9 -0
  77. package/src/api/v3/index.ts +1 -0
  78. package/src/api/v3/schema.ts +11 -4
  79. package/src/api/v4/index.ts +1 -0
  80. package/src/api/v4/schema.ts +11 -5
  81. package/src/db/index.ts +162 -37
  82. package/src/db/migrations/0003_krl_revision.ts +15 -0
  83. package/src/db/migrations/index.ts +2 -0
  84. package/src/db/types.ts +2 -0
  85. package/src/index.ts +6 -4
  86. package/src/krl.ts +3 -2
  87. package/src/node/cache.ts +62 -0
  88. package/src/node/env.ts +6 -5
  89. package/src/node/index.ts +1 -0
  90. package/src/router.ts +64 -2
  91. package/src/ssh/keys.ts +33 -0
  92. package/src/types.ts +49 -3
  93. package/src/utils.ts +188 -14
  94. package/src/verify.ts +54 -14
@@ -2,22 +2,27 @@ import { group, Logger } from "@andrewheberle/ts-slog"
2
2
  import {
3
3
  ApiException,
4
4
  ConflictException,
5
+ InputValidationException,
5
6
  InternalServerErrorException,
6
7
  NotFoundException,
7
8
  UnprocessableEntityException,
8
9
  } from "chanfana"
9
10
  import type { AppContext } from "../router.js"
10
11
  import type { CertificateSignerResponse } from "../types.js"
11
- import { BadIssuerError } from "../certificate.js"
12
+ import { BadIssuerError, CertificateError } from "../certificate.js"
12
13
  import type { SshCertificate } from "../ssh/certificate.js"
13
14
  import { KeyParseError, type PublicKey } from "../ssh/keys.js"
14
15
  import { getPrivateKey, getPublic, UnsupportedKeyError } from "../utils.js"
15
16
  import { CertificateType } from "../db/types.js"
16
17
  import { KRLBuilder } from "../krl.js"
18
+ import { z } from "zod"
19
+ import { certificateTypeName } from "./schema.js"
17
20
  import {
21
+ getKrlRevision,
18
22
  getKrlVersion,
19
23
  getRevocationList,
20
24
  recordCertificate,
25
+ recordRenewal,
21
26
  RevocationStatus,
22
27
  revocationStatus,
23
28
  revokeCertificate,
@@ -71,20 +76,38 @@ export const caPublicKey = async (c: AppContext, l: Logger): Promise<Response> =
71
76
  */
72
77
  export const revocationList = async (c: AppContext, l: Logger, certificateType: CertificateType): Promise<Response> => {
73
78
  try {
74
- // get revoked serials
75
- const serials = (await getRevocationList(c.env, certificateType)).map(v => BigInt(v))
76
-
77
79
  // grab private key from secret store
78
80
  const key = await getPrivateKey(c.env)
79
81
 
82
+ // a cached KRL is keyed by the revision it was generated for, which
83
+ // changes whenever a certificate is revoked, and by the CA key that
84
+ // signed it, so a cached KRL is never used once it is out of date. The
85
+ // revision is read before the revoked serials, so the KRL cached for a
86
+ // revision includes at least every certificate revoked by then
87
+ const cache = c.env.CACHE
88
+ const cacheKey = cache === undefined
89
+ ? undefined
90
+ : `krl:${certificateTypeName(certificateType)}:${key.publicKey.fingerprint()}:${await getKrlRevision(c.env, certificateType)}`
91
+
92
+ if (cache !== undefined && cacheKey !== undefined) {
93
+ const cached = await readCachedKrl(cache, cacheKey, l)
94
+ if (cached !== undefined) {
95
+ return c.json(cached)
96
+ }
97
+ }
98
+
99
+ // get revoked serials
100
+ const serials = (await getRevocationList(c.env, certificateType)).map(v => BigInt(v))
101
+
80
102
  // generate KRL
81
103
  const krl = new KRLBuilder(key)
82
104
  .addSerials(serials)
83
105
 
84
- // the version only changes when the content of the KRL does
85
- const version = await getKrlVersion(c.env, certificateType, await krl.digest())
106
+ // the version only changes when the content of the KRL does, and every
107
+ // KRL with the same version has the same generation date
108
+ const { version, generatedAt } = await getKrlVersion(c.env, certificateType, await krl.digest())
86
109
 
87
- const krlBytes = krl.generate(version)
110
+ const krlBytes = krl.generate(version, "", generatedAt)
88
111
 
89
112
  // convert to base64
90
113
  const bytes = new Uint8Array(krlBytes)
@@ -96,15 +119,73 @@ export const revocationList = async (c: AppContext, l: Logger, certificateType:
96
119
 
97
120
  const signature = await krl.signature()
98
121
 
99
- return c.json({
122
+ const response: KrlResponse = {
100
123
  krl: base64Krl,
101
124
  signature: signature,
102
- })
125
+ }
126
+
127
+ if (cache !== undefined && cacheKey !== undefined) {
128
+ try {
129
+ await cache.put(cacheKey, JSON.stringify(response), { expirationTtl: KrlCacheTtl })
130
+ } catch (err) {
131
+ // the KRL is still returned, it is generated again next time
132
+ l.warn("could not cache KRL", "error", err)
133
+ }
134
+ }
135
+
136
+ return c.json(response)
103
137
  } catch (err) {
104
138
  throw caKeyError(l, err)
105
139
  }
106
140
  }
107
141
 
142
+ const KrlResponseSchema = z.object({
143
+ krl: z.string(),
144
+ signature: z.string(),
145
+ })
146
+
147
+ type KrlResponse = z.infer<typeof KrlResponseSchema>
148
+
149
+ // how long a cached KRL is kept, in seconds. A cached KRL is only used for
150
+ // the revision it was generated for, so this only removes entries for earlier
151
+ // revisions from the cache
152
+ const KrlCacheTtl = 7 * 24 * 60 * 60
153
+
154
+ /**
155
+ * Returns a cached KRL response, or undefined if there is none or it cannot be
156
+ * used, in which case the KRL is generated again.
157
+ */
158
+ const readCachedKrl = async (cache: NonNullable<AppContext["env"]["CACHE"]>, key: string, l: Logger): Promise<KrlResponse | undefined> => {
159
+ let cached: string | null
160
+ try {
161
+ cached = await cache.get(key)
162
+ } catch (err) {
163
+ l.warn("could not read cached KRL", "error", err)
164
+ return undefined
165
+ }
166
+
167
+ if (cached === null) {
168
+ return undefined
169
+ }
170
+
171
+ let json: unknown
172
+ try {
173
+ json = JSON.parse(cached)
174
+ } catch {
175
+ l.warn("cached KRL was not valid JSON")
176
+ return undefined
177
+ }
178
+
179
+ const parsed = KrlResponseSchema.safeParse(json)
180
+ if (!parsed.success) {
181
+ l.warn("cached KRL was not a valid KRL response")
182
+ return undefined
183
+ }
184
+
185
+ l.debug("using cached KRL", "key", key)
186
+ return parsed.data
187
+ }
188
+
108
189
  /**
109
190
  * Signs a certificate, records it so it can later be revoked and returns it
110
191
  * as the response to a certificate request or renewal.
@@ -116,28 +197,42 @@ export const revocationList = async (c: AppContext, l: Logger, certificateType:
116
197
  * @param l The logger for the request
117
198
  * @param sign Signs and returns the certificate
118
199
  * @param certificateType The type of certificate being issued
200
+ * @param renews The host certificate being renewed, which is revoked when the new certificate is recorded
119
201
  * @returns The response containing the issued certificate
120
202
  */
121
- export const issueCertificate = async (c: AppContext, l: Logger, sign: () => Promise<SshCertificate>, certificateType: CertificateType): Promise<Response> => {
203
+ export const issueCertificate = async (c: AppContext, l: Logger, sign: () => Promise<SshCertificate>, certificateType: CertificateType, renews?: SshCertificate): Promise<Response> => {
122
204
  try {
123
205
  const certificate = await sign()
124
206
  const response: CertificateSignerResponse = {
125
207
  certificate: btoa(certificate.toString())
126
208
  }
127
209
 
210
+ let recorded: boolean
128
211
  try {
129
- await recordCertificate(c.env, certificate, certificate.keyId, certificateType)
212
+ if (renews === undefined) {
213
+ await recordCertificate(c.env, certificate, certificate.keyId, certificateType)
214
+ recorded = true
215
+ } else {
216
+ recorded = await recordRenewal(c.env, renews, certificate)
217
+ }
130
218
  } catch (err) {
131
219
  // a certificate that is not recorded cannot be revoked, so do not issue it
132
220
  l.error("there was a problem adding issued certificate to database", "error", err)
133
221
  throw new InternalServerErrorException("there was a problem recording the issued certificate")
134
222
  }
135
223
 
224
+ if (!recorded) {
225
+ // the certificate was revoked, or renewed by another request, since it was checked
226
+ l.warn("certificate presented for renewal was revoked before the renewal was recorded")
227
+ throw new InputValidationException("the provided certificate is revoked", ["body", "certificate"])
228
+ }
229
+
136
230
  l.info("completed issuing certificate",
137
231
  ...group(
138
232
  "certificate", "principals",
139
233
  certificate.principals.join(","), "serial", certificate.serial,
140
234
  ),
235
+ ...(renews === undefined ? [] : group("renewed", "serial", renews.serial)),
141
236
  )
142
237
 
143
238
  return c.json(response)
@@ -151,6 +246,12 @@ export const issueCertificate = async (c: AppContext, l: Logger, sign: () => Pro
151
246
  throw err
152
247
  case (err instanceof BadIssuerError):
153
248
  throw new UnprocessableEntityException(err.message)
249
+ case (err instanceof CertificateError):
250
+ // the request was valid but asked for a certificate the CA's
251
+ // configuration does not allow, such as an extension that is
252
+ // not permitted or one without any principals
253
+ l.warn("certificate could not be issued", "error", err.message)
254
+ throw new UnprocessableEntityException(err.message)
154
255
  default:
155
256
  // otherwise throw as InternalServerErrorException
156
257
  l.error("unhandled error", "error", err)
@@ -186,7 +287,9 @@ export const revoke = async (c: AppContext, l: Logger, serial: bigint, publicKey
186
287
  const res = await revokeCertificate(c.env, serial)
187
288
 
188
289
  if (res.length === 0) {
189
- throw new InternalServerErrorException("no results were returned for revocation")
290
+ // another request revoked the certificate after its status was checked
291
+ l.warn("certificate was revoked by another request")
292
+ throw new ConflictException("certificate is already revoked")
190
293
  }
191
294
 
192
295
  if (res.length > 1) {
package/src/api/schema.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import z from "zod"
2
2
  import type { SshCaBindings } from "../types.js"
3
3
  import {
4
+ refineSubjectKey,
4
5
  transformAuthorizationHeader,
5
6
  transformPublicKey,
6
7
  transformIdentityToken,
@@ -71,6 +72,14 @@ export const publicKey = openapiStringByte
71
72
  example: "ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkdNcFNKdHRmMEl0dE5DVmMyOFR6WEZSMUQweHZPM25wNjdWemVBUXNiZFpza3JEY1lqU2x3SjZIUERtVHBYYVUwbEVhWDlMNFloYy9jQ2YxTWU5RlRrPQo="
72
73
  })
73
74
 
75
+ export const subjectPublicKey = openapiStringByte
76
+ .transform(transformPublicKey)
77
+ .superRefine(refineSubjectKey)
78
+ .meta({
79
+ description: "SSH public key to sign",
80
+ example: "ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkdNcFNKdHRmMEl0dE5DVmMyOFR6WEZSMUQweHZPM25wNjdWemVBUXNiZFpza3JEY1lqU2x3SjZIUERtVHBYYVUwbEVhWDlMNFloYy9jQ2YxTWU5RlRrPQo="
81
+ })
82
+
74
83
  export const createIdentityTokenSchema = (env: SshCaBindings) => (
75
84
  z.string()
76
85
  .meta({
@@ -174,6 +174,7 @@ const HostCertificateRenewEndpoint = (env: SshCaBindings) => {
174
174
  return await issueCertificate(c, l,
175
175
  () => createSignedHostCertificate(c.env, data.body.public_key, opts),
176
176
  CertificateType.Host,
177
+ data.body.certificate,
177
178
  )
178
179
  }
179
180
  }
@@ -1,6 +1,7 @@
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
7
  transformProofOfPossession,
@@ -16,6 +17,7 @@ import {
16
17
  ForbiddenException,
17
18
  InputValidationException,
18
19
  InternalServerErrorException,
20
+ TooManyRequestsException,
19
21
  UnauthorizedException,
20
22
  UnprocessableEntityException,
21
23
  } from "chanfana"
@@ -31,6 +33,7 @@ import {
31
33
  openapiStringByte,
32
34
  proofOfPossession,
33
35
  publicKey,
36
+ subjectPublicKey,
34
37
  } from "../schema.js"
35
38
 
36
39
  export {
@@ -44,7 +47,7 @@ export {
44
47
 
45
48
  export const userCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
46
49
  z.object({
47
- public_key: publicKey,
50
+ public_key: subjectPublicKey,
48
51
  proof: proofOfPossession
49
52
  .transform((val, ctx) => transformProofOfPossession(env, val, ctx)),
50
53
  identity: createIdentityTokenSchema(env),
@@ -84,16 +87,17 @@ export const createUserCertificateRequestEndpointSchema = (env: SshCaBindings) =
84
87
  ...InputValidationException.schema(),
85
88
  ...UnprocessableEntityException.schema(),
86
89
  ...InternalServerErrorException.schema(),
90
+ ...TooManyRequestsException.schema(),
87
91
  }
88
92
  })
89
93
 
90
94
  export const hostCertificateRequestEndpointBodySchema = (env: SshCaBindings) => (
91
95
  z.object({
92
- public_key: publicKey,
96
+ public_key: subjectPublicKey,
93
97
  proof: proofOfPossession
94
98
  .transform((val, ctx) => transformProofOfPossession(env, val, ctx)),
95
99
  identity: createIdentityTokenSchema(env),
96
- principals: z.array(z.string()).min(1)
100
+ principals: z.array(z.string().superRefine(refineHostPrincipal)).min(1)
97
101
  .meta({ description: "List of principals to include on the issued certificate" }),
98
102
  lifetime: z.int()
99
103
  .min(seconds("24 hours"))
@@ -128,6 +132,7 @@ export const createHostCertificateRequestEndpointSchema = (env: SshCaBindings) =
128
132
  ...InputValidationException.schema(),
129
133
  ...UnprocessableEntityException.schema(),
130
134
  ...InternalServerErrorException.schema(),
135
+ ...TooManyRequestsException.schema(),
131
136
  }
132
137
  })
133
138
 
@@ -152,7 +157,7 @@ export const hostCertificateRenewEndpointBodySchema = (env: SshCaBindings) => (
152
157
  export const createHostCertificateRenewEndpointSchema = (env: SshCaBindings) => ({
153
158
  tags: [certificateTypeTag(CertificateType.Host)],
154
159
  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.",
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.",
156
161
  request: {
157
162
  body: contentJson(hostCertificateRenewEndpointBodySchema(env))
158
163
  },
@@ -170,6 +175,7 @@ export const createHostCertificateRenewEndpointSchema = (env: SshCaBindings) =>
170
175
  ...InputValidationException.schema(),
171
176
  ...UnprocessableEntityException.schema(),
172
177
  ...InternalServerErrorException.schema(),
178
+ ...TooManyRequestsException.schema(),
173
179
  }
174
180
  })
175
181
 
@@ -211,6 +217,7 @@ export const createRevokeCertificateEndpointSchema = (env: SshCaBindings, certif
211
217
  ...InputValidationException.schema(),
212
218
  ...UnprocessableEntityException.schema(),
213
219
  ...InternalServerErrorException.schema(),
220
+ ...TooManyRequestsException.schema(),
214
221
  ...ConflictException.schema()
215
222
  }
216
223
  })
@@ -222,6 +222,7 @@ const RenewHostCertificateEndpoint = (env: SshCaBindings) => {
222
222
  return await issueCertificate(c, l,
223
223
  () => createSignedHostCertificate(c.env, data.body.certificate.publicKey, opts),
224
224
  CertificateType.Host,
225
+ data.body.certificate,
225
226
  )
226
227
  }
227
228
  }
@@ -1,6 +1,7 @@
1
1
  import z from "zod"
2
2
  import type { SshCaBindings } from "../../types.js"
3
3
  import {
4
+ refineHostPrincipal,
4
5
  refineRenewableCertificate,
5
6
  split,
6
7
  transformCertificate,
@@ -11,6 +12,7 @@ import {
11
12
  contentJson,
12
13
  InputValidationException,
13
14
  InternalServerErrorException,
15
+ TooManyRequestsException,
14
16
  NotFoundException,
15
17
  UnauthorizedException,
16
18
  ForbiddenException,
@@ -28,7 +30,7 @@ import {
28
30
  createIdentityTokenSchema,
29
31
  createRevocationListEndpointSchema,
30
32
  openapiStringByte,
31
- publicKey,
33
+ subjectPublicKey,
32
34
  } from "../schema.js"
33
35
 
34
36
  /**
@@ -96,7 +98,7 @@ export const createGetRevocationListEndpointSchema = (certificateType: Certifica
96
98
 
97
99
  export const issueUserCertificateEndpointBodySchema = (env: SshCaBindings) => (
98
100
  z.object({
99
- public_key: publicKey,
101
+ public_key: subjectPublicKey,
100
102
  identity: createIdentityTokenSchema(env),
101
103
  extensions: z.array(z.string())
102
104
  .default(split(env.SSH_CERTIFICATE_EXTENSIONS))
@@ -130,14 +132,15 @@ export const createIssueUserCertificateEndpointSchema = (env: SshCaBindings) =>
130
132
  ...InputValidationException.schema(),
131
133
  ...UnprocessableEntityException.schema(),
132
134
  ...InternalServerErrorException.schema(),
135
+ ...TooManyRequestsException.schema(),
133
136
  }
134
137
  })
135
138
 
136
139
  export const issueHostCertificateEndpointBodySchema = (env: SshCaBindings) => (
137
140
  z.object({
138
- public_key: publicKey,
141
+ public_key: subjectPublicKey,
139
142
  identity: createIdentityTokenSchema(env),
140
- principals: z.array(z.string()).min(1)
143
+ principals: z.array(z.string().superRefine(refineHostPrincipal)).min(1)
141
144
  .meta({ description: "List of principals to include on the issued certificate" }),
142
145
  lifetime: z.int()
143
146
  .min(seconds("24 hours"))
@@ -168,6 +171,7 @@ export const createIssueHostCertificateEndpointSchema = (env: SshCaBindings) =>
168
171
  ...InputValidationException.schema(),
169
172
  ...UnprocessableEntityException.schema(),
170
173
  ...InternalServerErrorException.schema(),
174
+ ...TooManyRequestsException.schema(),
171
175
  }
172
176
  })
173
177
 
@@ -190,7 +194,7 @@ export const createRenewHostCertificateEndpointSchema = (env: SshCaBindings) =>
190
194
  tags: [certificateTypeTag(CertificateType.Host)],
191
195
  operationId: "renew_host_certificate",
192
196
  summary: "Renew a host certificate",
193
- description: `Issues a new SSH host certificate for the same public key and principals as an existing, unexpired and unrevoked host certificate issued by this CA. No OIDC authentication is required; instead the \`${ProofHeader}\` header must contain a proof of possession of the private key for the existing certificate made for this request. The lifetime of the renewed certificate is no longer than that of the existing certificate.`,
197
+ description: `Issues a new SSH host certificate for the same public key and principals as an existing, unexpired and unrevoked host certificate issued by this CA. No OIDC authentication is required; instead the \`${ProofHeader}\` header must contain a proof of possession of the private key for the existing certificate made for this request. 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.`,
194
198
  request: {
195
199
  params: serialParamSchema,
196
200
  headers: createProofHeaderSchema(env),
@@ -206,6 +210,7 @@ export const createRenewHostCertificateEndpointSchema = (env: SshCaBindings) =>
206
210
  ...InputValidationException.schema(),
207
211
  ...UnprocessableEntityException.schema(),
208
212
  ...InternalServerErrorException.schema(),
213
+ ...TooManyRequestsException.schema(),
209
214
  }
210
215
  })
211
216
 
@@ -231,5 +236,6 @@ export const createRevokeCertificateEndpointSchema = (env: SshCaBindings, certif
231
236
  ...NotFoundException.schema(),
232
237
  ...ConflictException.schema(),
233
238
  ...InternalServerErrorException.schema(),
239
+ ...TooManyRequestsException.schema(),
234
240
  }
235
241
  })