@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.
Files changed (122) hide show
  1. package/README.md +174 -1
  2. package/dist/api/operations.d.ts +50 -0
  3. package/dist/api/operations.d.ts.map +1 -0
  4. package/dist/api/operations.js +264 -0
  5. package/dist/api/operations.js.map +1 -0
  6. package/dist/api/schema.d.ts +72 -0
  7. package/dist/api/schema.d.ts.map +1 -0
  8. package/dist/api/schema.js +114 -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 +18 -104
  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 +186 -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 +68 -9
  30. package/dist/db/index.d.ts.map +1 -1
  31. package/dist/db/index.js +165 -49
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/db/migrations/0003_krl_revision.d.ts +3 -0
  34. package/dist/db/migrations/0003_krl_revision.d.ts.map +1 -0
  35. package/dist/db/migrations/0003_krl_revision.js +14 -0
  36. package/dist/db/migrations/0003_krl_revision.js.map +1 -0
  37. package/dist/db/migrations/index.d.ts.map +1 -1
  38. package/dist/db/migrations/index.js +2 -0
  39. package/dist/db/migrations/index.js.map +1 -1
  40. package/dist/db/types.d.ts +2 -0
  41. package/dist/db/types.d.ts.map +1 -1
  42. package/dist/index.d.ts +5 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/krl.d.ts +3 -2
  47. package/dist/krl.d.ts.map +1 -1
  48. package/dist/krl.js +12 -165
  49. package/dist/krl.js.map +1 -1
  50. package/dist/node/cache.d.ts +21 -0
  51. package/dist/node/cache.d.ts.map +1 -0
  52. package/dist/node/cache.js +49 -0
  53. package/dist/node/cache.js.map +1 -0
  54. package/dist/node/env.d.ts +6 -3
  55. package/dist/node/env.d.ts.map +1 -1
  56. package/dist/node/env.js +4 -3
  57. package/dist/node/env.js.map +1 -1
  58. package/dist/node/index.d.ts +1 -0
  59. package/dist/node/index.d.ts.map +1 -1
  60. package/dist/node/index.js +1 -0
  61. package/dist/node/index.js.map +1 -1
  62. package/dist/proof.d.ts +36 -7
  63. package/dist/proof.d.ts.map +1 -1
  64. package/dist/proof.js +53 -21
  65. package/dist/proof.js.map +1 -1
  66. package/dist/router.d.ts +25 -1
  67. package/dist/router.d.ts.map +1 -1
  68. package/dist/router.js +41 -2
  69. package/dist/router.js.map +1 -1
  70. package/dist/ssh/certificate.d.ts +93 -0
  71. package/dist/ssh/certificate.d.ts.map +1 -0
  72. package/dist/ssh/certificate.js +178 -0
  73. package/dist/ssh/certificate.js.map +1 -0
  74. package/dist/ssh/encoding.d.ts +41 -0
  75. package/dist/ssh/encoding.d.ts.map +1 -0
  76. package/dist/ssh/encoding.js +70 -0
  77. package/dist/ssh/encoding.js.map +1 -0
  78. package/dist/ssh/keys.d.ts +92 -0
  79. package/dist/ssh/keys.d.ts.map +1 -0
  80. package/dist/ssh/keys.js +277 -0
  81. package/dist/ssh/keys.js.map +1 -0
  82. package/dist/ssh/signer.d.ts +35 -0
  83. package/dist/ssh/signer.d.ts.map +1 -0
  84. package/dist/ssh/signer.js +99 -0
  85. package/dist/ssh/signer.js.map +1 -0
  86. package/dist/types.d.ts +64 -19
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/utils.d.ts +107 -21
  89. package/dist/utils.d.ts.map +1 -1
  90. package/dist/utils.js +251 -65
  91. package/dist/utils.js.map +1 -1
  92. package/dist/verify.d.ts +17 -5
  93. package/dist/verify.d.ts.map +1 -1
  94. package/dist/verify.js +47 -9
  95. package/dist/verify.js.map +1 -1
  96. package/openapi.json +1942 -1
  97. package/package.json +4 -3
  98. package/src/api/operations.ts +314 -0
  99. package/src/api/schema.ts +146 -0
  100. package/src/api/v3/index.ts +21 -248
  101. package/src/api/v3/schema.ts +36 -128
  102. package/src/api/v4/index.ts +266 -0
  103. package/src/api/v4/schema.ts +241 -0
  104. package/src/certificate.ts +62 -123
  105. package/src/db/index.ts +196 -56
  106. package/src/db/migrations/0003_krl_revision.ts +15 -0
  107. package/src/db/migrations/index.ts +2 -0
  108. package/src/db/types.ts +2 -0
  109. package/src/index.ts +6 -4
  110. package/src/krl.ts +13 -197
  111. package/src/node/cache.ts +62 -0
  112. package/src/node/env.ts +6 -5
  113. package/src/node/index.ts +1 -0
  114. package/src/proof.ts +65 -31
  115. package/src/router.ts +67 -3
  116. package/src/ssh/certificate.ts +236 -0
  117. package/src/ssh/encoding.ts +81 -0
  118. package/src/ssh/keys.ts +327 -0
  119. package/src/ssh/signer.ts +129 -0
  120. package/src/types.ts +60 -20
  121. package/src/utils.ts +285 -81
  122. package/src/verify.ts +54 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewheberle/serverless-ssh-ca",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "A serverless SSH CA built for Cloudflare Workers that can also run on Node.js",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -31,18 +31,19 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@andrewheberle/ts-slog": "^1.3.0",
34
+ "@noble/hashes": "^2.4.0",
35
+ "@scure/base": "^2.4.0",
34
36
  "chanfana": "^3.0.0",
35
37
  "hono": "^4.10.6",
36
38
  "itty-time": "^2.0.2",
37
39
  "jose": "^6.0.12",
38
- "sshpk": "^1.18.0",
40
+ "micro-packed": "^0.11.1",
39
41
  "workers-qb": "^1.14.0",
40
42
  "zod": "^4.0.0"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@eslint/js": "^10.0.1",
44
46
  "@types/node": "^24.19.1",
45
- "@types/sshpk": "^1.17.4",
46
47
  "@vitest/coverage-v8": "^4.1.7",
47
48
  "eslint": "^10.4.1",
48
49
  "tsx": "^4.21.0",
@@ -0,0 +1,314 @@
1
+ import { group, Logger } from "@andrewheberle/ts-slog"
2
+ import {
3
+ ApiException,
4
+ ConflictException,
5
+ InputValidationException,
6
+ InternalServerErrorException,
7
+ NotFoundException,
8
+ UnprocessableEntityException,
9
+ } from "chanfana"
10
+ import type { AppContext } from "../router.js"
11
+ import type { CertificateSignerResponse } from "../types.js"
12
+ import { BadIssuerError, CertificateError } from "../certificate.js"
13
+ import type { SshCertificate } from "../ssh/certificate.js"
14
+ import { KeyParseError, type PublicKey } from "../ssh/keys.js"
15
+ import { getPrivateKey, getPublic, UnsupportedKeyError } from "../utils.js"
16
+ import { CertificateType } from "../db/types.js"
17
+ import { KRLBuilder } from "../krl.js"
18
+ import { z } from "zod"
19
+ import { certificateTypeName } from "./schema.js"
20
+ import {
21
+ getKrlRevision,
22
+ getKrlVersion,
23
+ getRevocationList,
24
+ recordCertificate,
25
+ recordRenewal,
26
+ RevocationStatus,
27
+ revocationStatus,
28
+ revokeCertificate,
29
+ } from "../db/index.js"
30
+
31
+ /**
32
+ * Maps an error from an operation that uses the CA key to an API error.
33
+ */
34
+ const caKeyError = (l: Logger, err: unknown): ApiException => {
35
+ switch (true) {
36
+ case (err instanceof UnsupportedKeyError):
37
+ l.error(err.message)
38
+ return new InternalServerErrorException(err.message)
39
+ case (err instanceof KeyParseError):
40
+ l.error("error parsing private key")
41
+ return new InternalServerErrorException("error parsing private key")
42
+ case (err instanceof ApiException):
43
+ // re-throw any exisiting chanfana error
44
+ return err
45
+ default:
46
+ // otherwise throw as InternalServerErrorException
47
+ l.error("unhandled error", "error", err)
48
+ return new InternalServerErrorException("internal server error")
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Returns the public key of the CA in OpenSSH format.
54
+ *
55
+ * @param c The request context
56
+ * @param l The logger for the request
57
+ * @returns The response containing the CA public key
58
+ */
59
+ export const caPublicKey = async (c: AppContext, l: Logger): Promise<Response> => {
60
+ try {
61
+ const pub = await getPublic(c.env)
62
+
63
+ return c.text(`${pub.toString()}\n`)
64
+ } catch (err) {
65
+ throw caKeyError(l, err)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Returns the signed Key Revocation List for a type of certificate.
71
+ *
72
+ * @param c The request context
73
+ * @param l The logger for the request
74
+ * @param certificateType The type of certificate to return the KRL for
75
+ * @returns The response containing the KRL and its signature
76
+ */
77
+ export const revocationList = async (c: AppContext, l: Logger, certificateType: CertificateType): Promise<Response> => {
78
+ try {
79
+ // grab private key from secret store
80
+ const key = await getPrivateKey(c.env)
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
+
102
+ // generate KRL
103
+ const krl = new KRLBuilder(key)
104
+ .addSerials(serials)
105
+
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())
109
+
110
+ const krlBytes = krl.generate(version, "", generatedAt)
111
+
112
+ // convert to base64
113
+ const bytes = new Uint8Array(krlBytes)
114
+ let binary = ""
115
+ for (const byte of bytes) {
116
+ binary += String.fromCharCode(byte)
117
+ }
118
+ const base64Krl = btoa(binary)
119
+
120
+ const signature = await krl.signature()
121
+
122
+ const response: KrlResponse = {
123
+ krl: base64Krl,
124
+ signature: signature,
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)
137
+ } catch (err) {
138
+ throw caKeyError(l, err)
139
+ }
140
+ }
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
+
189
+ /**
190
+ * Signs a certificate, records it so it can later be revoked and returns it
191
+ * as the response to a certificate request or renewal.
192
+ *
193
+ * These are the steps shared by every API version once a request has been
194
+ * validated and authorised.
195
+ *
196
+ * @param c The request context
197
+ * @param l The logger for the request
198
+ * @param sign Signs and returns the certificate
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
201
+ * @returns The response containing the issued certificate
202
+ */
203
+ export const issueCertificate = async (c: AppContext, l: Logger, sign: () => Promise<SshCertificate>, certificateType: CertificateType, renews?: SshCertificate): Promise<Response> => {
204
+ try {
205
+ const certificate = await sign()
206
+ const response: CertificateSignerResponse = {
207
+ certificate: btoa(certificate.toString())
208
+ }
209
+
210
+ let recorded: boolean
211
+ try {
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
+ }
218
+ } catch (err) {
219
+ // a certificate that is not recorded cannot be revoked, so do not issue it
220
+ l.error("there was a problem adding issued certificate to database", "error", err)
221
+ throw new InternalServerErrorException("there was a problem recording the issued certificate")
222
+ }
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
+
230
+ l.info("completed issuing certificate",
231
+ ...group(
232
+ "certificate", "principals",
233
+ certificate.principals.join(","), "serial", certificate.serial,
234
+ ),
235
+ ...(renews === undefined ? [] : group("renewed", "serial", renews.serial)),
236
+ )
237
+
238
+ return c.json(response)
239
+ } catch (err) {
240
+ switch (true) {
241
+ case (err instanceof UnsupportedKeyError):
242
+ l.error(err.message)
243
+ throw new InternalServerErrorException(err.message)
244
+ case (err instanceof ApiException):
245
+ // re-throw any exisiting chanfana error
246
+ throw err
247
+ case (err instanceof BadIssuerError):
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)
255
+ default:
256
+ // otherwise throw as InternalServerErrorException
257
+ l.error("unhandled error", "error", err)
258
+ throw new InternalServerErrorException("internal server error")
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Revokes a certificate once the requester has proven possession of the
265
+ * private key for the public key it was issued for.
266
+ *
267
+ * @param c The request context
268
+ * @param l The logger for the request
269
+ * @param serial The serial of the certificate to revoke
270
+ * @param publicKey The public key the requester has proven possession of
271
+ * @param certificateType The type of certificate being revoked
272
+ * @returns The response containing the time of revocation
273
+ */
274
+ export const revoke = async (c: AppContext, l: Logger, serial: bigint, publicKey: PublicKey, certificateType: CertificateType): Promise<Response> => {
275
+ // ensure certificate can be revoked
276
+ const status = await revocationStatus(c.env, serial, publicKey, certificateType)
277
+ switch (status) {
278
+ case RevocationStatus.Revoked:
279
+ l.warn("certificate is already revoked")
280
+ throw new ConflictException("certificate is already revoked")
281
+ case RevocationStatus.NotFound:
282
+ l.warn("certificate not found")
283
+ throw new NotFoundException("certificate not found")
284
+ }
285
+
286
+ try {
287
+ const res = await revokeCertificate(c.env, serial)
288
+
289
+ if (res.length === 0) {
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")
293
+ }
294
+
295
+ if (res.length > 1) {
296
+ l.warn("more than one revocation result was returned", "count", res.length)
297
+ }
298
+
299
+ l.info("completed revocation")
300
+ return c.json({
301
+ revoked_at: res[0]!.revoked_at
302
+ })
303
+ } catch (err) {
304
+ switch (true) {
305
+ case (err instanceof ApiException):
306
+ // re-throw any exisiting chanfana error
307
+ throw err
308
+ default:
309
+ // otherwise throw as InternalServerErrorException
310
+ l.error("unhandled error", "error", err)
311
+ throw new InternalServerErrorException("internal server error")
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,146 @@
1
+ import z from "zod"
2
+ import type { SshCaBindings } from "../types.js"
3
+ import {
4
+ refineSubjectKey,
5
+ transformAuthorizationHeader,
6
+ transformPublicKey,
7
+ transformIdentityToken,
8
+ } from "../utils.js"
9
+ import {
10
+ contentJson,
11
+ InternalServerErrorException,
12
+ } from "chanfana"
13
+ import { CertificateType } from "../db/types.js"
14
+
15
+ /**
16
+ * Returns the name of a {@link CertificateType} as used in API paths and logs
17
+ * @param certificateType The type of certificate
18
+ * @returns "user" or "host"
19
+ */
20
+ export const certificateTypeName = (certificateType: CertificateType): "user" | "host" => {
21
+ switch (certificateType) {
22
+ case CertificateType.User:
23
+ return "user"
24
+ case CertificateType.Host:
25
+ return "host"
26
+ default: {
27
+ const unknownType: never = certificateType
28
+ throw new Error(`unknown certificate type: ${String(unknownType)}`)
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Returns the OpenAPI tag that groups the endpoints for a {@link CertificateType}
35
+ * @param certificateType The type of certificate
36
+ * @returns "User" or "Host"
37
+ */
38
+ export const certificateTypeTag = (certificateType: CertificateType): "User" | "Host" => {
39
+ switch (certificateType) {
40
+ case CertificateType.User:
41
+ return "User"
42
+ case CertificateType.Host:
43
+ return "Host"
44
+ default: {
45
+ const unknownType: never = certificateType
46
+ throw new Error(`unknown certificate type: ${String(unknownType)}`)
47
+ }
48
+ }
49
+ }
50
+
51
+ export const openapiStringByte = z.base64()
52
+ .transform((v) => {
53
+ const a = z.util.base64ToUint8Array(v)
54
+ return Buffer.from(
55
+ a.buffer,
56
+ a.byteOffset,
57
+ a.length
58
+ )
59
+ })
60
+ .openapi({ format: "byte" })
61
+
62
+ export const proofOfPossession = z.string()
63
+ .meta({
64
+ description: "Proof of possession comprising of ${timestamp}.${fingerprint}.${signature}",
65
+ example: "1765696780805.SHA256:79mq0wtpQMTvS4+Of8VzLN0qmYWNUyTXYmqKwhEgSLs.LS0tLS1CRUdJTiBTU0ggU0lHTkFUVVJFLS0tLS0KVTFOSVUwbEhBQUFBQVFBQUFHZ0FBQUFUWldOa2MyRXRjMmhoTWkxdWFYTjBjREkxTmdBQUFBaHVhWE4wY0RJMQpOZ0FBQUVFRUR6Ymk4OE10alBYSDFCb1JURU9HaS96aEc4QnhQNVZUUW5OQkFNMGZtMExtekErUUZkUlNVYVRFCkdNN2l0QjhINmZNZFAydlAxcElQcGUyOWVLQkJZd0FBQUFSbWFXeGxBQUFBQUFBQUFBWnphR0UxTVRJQUFBQmsKQUFBQUUyVmpaSE5oTFhOb1lUSXRibWx6ZEhBeU5UWUFBQUJKQUFBQUlHdmZVZGNDQkRXa3ZwL0ZHS3NkakgrRQpKZzZXN0EvWUJVZng2Z0srQ01raEFBQUFJUUQxOFd5cDNZejZ0eXNvdllXbEJHTXprRnlYbFRrSHM5blVVRXJqCkczNWVPQT09Ci0tLS0tRU5EIFNTSCBTSUdOQVRVUkUtLS0tLQo="
66
+ })
67
+
68
+ export const publicKey = openapiStringByte
69
+ .transform(transformPublicKey)
70
+ .meta({
71
+ description: "SSH public key to sign",
72
+ example: "ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkdNcFNKdHRmMEl0dE5DVmMyOFR6WEZSMUQweHZPM25wNjdWemVBUXNiZFpza3JEY1lqU2x3SjZIUERtVHBYYVUwbEVhWDlMNFloYy9jQ2YxTWU5RlRrPQo="
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
+
83
+ export const createIdentityTokenSchema = (env: SshCaBindings) => (
84
+ z.string()
85
+ .meta({
86
+ description: "Identity Token JWT from OIDC IdP",
87
+ example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNzc1NzAzMDk1fQ.ri_neAWnhMNK3LzlsrBcQYymSM4yRjmNSZZSeZiXhrEqtEz6c3cXk0Esq765umGjpUsWcosL-OFrDJlyAjTDnhrd9oV08uc_CW0rQRsJIGEuRo3ryxkLdVu9mGoZWEUb9KwjGJrwxvr-0cPWx5jaDyKwJcqMvtV_bEITUD51sDB1Vm89QfYRO_pGJo2vrRzSvMjpUenRpwPay4lYIBxl41_4YpR9Rc6VrIZuYsjV2iqEZ4eBrygMA7zPR_hN7l7s95FddLOzj5NsK57VT4uLHwYohx2oqMzw3M-B9HsZIQin_9q61pZFQXepzJth0woXiZheU27llnfHX967PhNQyg"
88
+ })
89
+ .transform((val, ctx) => transformIdentityToken(env, val, ctx))
90
+ )
91
+
92
+ export const createAccessTokenSchema = (env: SshCaBindings) => (
93
+ z.string()
94
+ .meta({
95
+ description: "Access Token JWT from OIDC IdP",
96
+ example: "Bearer eyJ..."
97
+ })
98
+ .transform((val, ctx) => transformAuthorizationHeader(env, val, ctx))
99
+ )
100
+
101
+ export const krl = openapiStringByte
102
+ .meta({ description: "Key Revocation List" })
103
+
104
+ export const createHeaderSchema = (env: SshCaBindings) => (
105
+ z.object({
106
+ "Authorization": createAccessTokenSchema(env)
107
+ })
108
+ )
109
+
110
+ export const certificate = openapiStringByte
111
+ .meta({ description: "Issued Certificate" })
112
+
113
+ export const CaPublicKeyEndpointSchema = {
114
+ tags: ["CA"],
115
+ summary: "Get the CA public key",
116
+ description: "Returns the public key of the SSH Certificate Authority in OpenSSH format, for use in `TrustedUserCAKeys` on SSH servers or `@cert-authority` entries in `known_hosts` on SSH clients.",
117
+ responses: {
118
+ "200": {
119
+ content: {
120
+ "text/plain": {
121
+ schema: z.string()
122
+ }
123
+ },
124
+ description: "Returns SSH Certificate Authority Public Key",
125
+ },
126
+ ...InternalServerErrorException.schema(),
127
+ }
128
+ }
129
+
130
+ export const createRevocationListEndpointSchema = (certificateType: CertificateType) => ({
131
+ tags: [certificateTypeTag(certificateType)],
132
+ summary: `Get the Key Revocation List for ${certificateTypeName(certificateType)} certificates`,
133
+ description: `Returns the OpenSSH Key Revocation List (KRL) of revoked ${certificateTypeName(certificateType)} certificates issued by this CA, along with an SSHSIG signature made by the CA key so the KRL can be verified before use.`,
134
+ responses: {
135
+ "200": {
136
+ description: `Returns an Open SSH Key Revocation List of revoked ${certificateTypeName(certificateType)} certificates as BASE64 and an SSHSIG signature for verification`,
137
+ ...contentJson(z.object({
138
+ krl: krl,
139
+ signature: z.string()
140
+ })
141
+ .openapi("Key Revocation List Response")
142
+ )
143
+ },
144
+ ...InternalServerErrorException.schema(),
145
+ }
146
+ })