@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.
- package/README.md +55 -1
- package/dist/api/operations.d.ts +49 -0
- package/dist/api/operations.d.ts.map +1 -0
- package/dist/api/operations.js +175 -0
- package/dist/api/operations.js.map +1 -0
- package/dist/api/schema.d.ts +71 -0
- package/dist/api/schema.d.ts.map +1 -0
- package/dist/api/schema.js +107 -0
- package/dist/api/schema.js.map +1 -0
- package/dist/api/v3/index.d.ts.map +1 -1
- package/dist/api/v3/index.js +25 -212
- package/dist/api/v3/index.js.map +1 -1
- package/dist/api/v3/schema.d.ts +30 -74
- package/dist/api/v3/schema.d.ts.map +1 -1
- package/dist/api/v3/schema.js +18 -73
- package/dist/api/v3/schema.js.map +1 -1
- package/dist/api/v4/index.d.ts +8 -0
- package/dist/api/v4/index.d.ts.map +1 -0
- package/dist/api/v4/index.js +155 -0
- package/dist/api/v4/index.js.map +1 -0
- package/dist/api/v4/schema.d.ts +268 -0
- package/dist/api/v4/schema.d.ts.map +1 -0
- package/dist/api/v4/schema.js +182 -0
- package/dist/api/v4/schema.js.map +1 -0
- package/dist/certificate.d.ts +9 -6
- package/dist/certificate.d.ts.map +1 -1
- package/dist/certificate.js +55 -99
- package/dist/certificate.js.map +1 -1
- package/dist/db/index.d.ts +32 -24
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +101 -28
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations/0002_krl_version.d.ts +3 -0
- package/dist/db/migrations/0002_krl_version.d.ts.map +1 -0
- package/dist/db/migrations/0002_krl_version.js +15 -0
- package/dist/db/migrations/0002_krl_version.js.map +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +3 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/types.d.ts +37 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +13 -0
- package/dist/db/types.js.map +1 -0
- package/dist/krl.d.ts +9 -1
- package/dist/krl.d.ts.map +1 -1
- package/dist/krl.js +22 -162
- package/dist/krl.js.map +1 -1
- package/dist/proof.d.ts +36 -7
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +53 -21
- package/dist/proof.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3 -1
- package/dist/router.js.map +1 -1
- package/dist/ssh/certificate.d.ts +93 -0
- package/dist/ssh/certificate.d.ts.map +1 -0
- package/dist/ssh/certificate.js +178 -0
- package/dist/ssh/certificate.js.map +1 -0
- package/dist/ssh/encoding.d.ts +41 -0
- package/dist/ssh/encoding.d.ts.map +1 -0
- package/dist/ssh/encoding.js +70 -0
- package/dist/ssh/encoding.js.map +1 -0
- package/dist/ssh/keys.d.ts +80 -0
- package/dist/ssh/keys.d.ts.map +1 -0
- package/dist/ssh/keys.js +249 -0
- package/dist/ssh/keys.js.map +1 -0
- package/dist/ssh/signer.d.ts +35 -0
- package/dist/ssh/signer.d.ts.map +1 -0
- package/dist/ssh/signer.js +99 -0
- package/dist/ssh/signer.js.map +1 -0
- package/dist/types.d.ts +12 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +40 -21
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +87 -54
- package/dist/utils.js.map +1 -1
- package/openapi.json +1960 -82
- package/package.json +5 -17
- package/src/api/operations.ts +211 -0
- package/src/api/schema.ts +137 -0
- package/src/api/v3/index.ts +32 -259
- package/src/api/v3/schema.ts +34 -96
- package/src/api/v4/index.ts +265 -0
- package/src/api/v4/schema.ts +235 -0
- package/src/certificate.ts +62 -123
- package/src/db/index.ts +113 -49
- package/src/db/migrations/0002_krl_version.ts +16 -0
- package/src/db/migrations/index.ts +3 -1
- package/src/db/types.ts +40 -0
- package/src/krl.ts +32 -194
- package/src/proof.ts +65 -31
- package/src/router.ts +3 -1
- package/src/ssh/certificate.ts +236 -0
- package/src/ssh/encoding.ts +81 -0
- package/src/ssh/keys.ts +294 -0
- package/src/ssh/signer.ts +129 -0
- package/src/types.ts +12 -18
- package/src/utils.ts +99 -69
package/src/api/v3/index.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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",
|
|
91
|
+
"type", certificateTypeName(certificateType),
|
|
169
92
|
"action", "krl",
|
|
170
93
|
),
|
|
171
94
|
)
|
|
172
95
|
|
|
173
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
171
|
+
principals: data.body.certificate.principals,
|
|
326
172
|
}
|
|
327
173
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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",
|
|
190
|
+
...group("request", "type", certificateTypeName(certificateType), "action", "revoke", "serial", data.body.serial),
|
|
379
191
|
)
|
|
380
192
|
|
|
381
|
-
|
|
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("
|
|
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.
|
|
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
|
}
|
package/src/api/v3/schema.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
202
|
+
description: `SSH ${certificateTypeName(certificateType)} certificate revoked successfully`,
|
|
265
203
|
...contentJson(z.object({
|
|
266
204
|
revoked_at: z.iso.datetime(),
|
|
267
205
|
})
|