@andrewheberle/serverless-ssh-ca 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +55 -1
  2. package/dist/api/operations.d.ts +49 -0
  3. package/dist/api/operations.d.ts.map +1 -0
  4. package/dist/api/operations.js +175 -0
  5. package/dist/api/operations.js.map +1 -0
  6. package/dist/api/schema.d.ts +71 -0
  7. package/dist/api/schema.d.ts.map +1 -0
  8. package/dist/api/schema.js +107 -0
  9. package/dist/api/schema.js.map +1 -0
  10. package/dist/api/v3/index.d.ts.map +1 -1
  11. package/dist/api/v3/index.js +25 -212
  12. package/dist/api/v3/index.js.map +1 -1
  13. package/dist/api/v3/schema.d.ts +30 -74
  14. package/dist/api/v3/schema.d.ts.map +1 -1
  15. package/dist/api/v3/schema.js +18 -73
  16. package/dist/api/v3/schema.js.map +1 -1
  17. package/dist/api/v4/index.d.ts +8 -0
  18. package/dist/api/v4/index.d.ts.map +1 -0
  19. package/dist/api/v4/index.js +155 -0
  20. package/dist/api/v4/index.js.map +1 -0
  21. package/dist/api/v4/schema.d.ts +268 -0
  22. package/dist/api/v4/schema.d.ts.map +1 -0
  23. package/dist/api/v4/schema.js +182 -0
  24. package/dist/api/v4/schema.js.map +1 -0
  25. package/dist/certificate.d.ts +9 -6
  26. package/dist/certificate.d.ts.map +1 -1
  27. package/dist/certificate.js +55 -99
  28. package/dist/certificate.js.map +1 -1
  29. package/dist/db/index.d.ts +32 -24
  30. package/dist/db/index.d.ts.map +1 -1
  31. package/dist/db/index.js +101 -28
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/db/migrations/0002_krl_version.d.ts +3 -0
  34. package/dist/db/migrations/0002_krl_version.d.ts.map +1 -0
  35. package/dist/db/migrations/0002_krl_version.js +15 -0
  36. package/dist/db/migrations/0002_krl_version.js.map +1 -0
  37. package/dist/db/migrations/index.d.ts.map +1 -1
  38. package/dist/db/migrations/index.js +3 -1
  39. package/dist/db/migrations/index.js.map +1 -1
  40. package/dist/db/types.d.ts +37 -0
  41. package/dist/db/types.d.ts.map +1 -0
  42. package/dist/db/types.js +13 -0
  43. package/dist/db/types.js.map +1 -0
  44. package/dist/krl.d.ts +9 -1
  45. package/dist/krl.d.ts.map +1 -1
  46. package/dist/krl.js +22 -162
  47. package/dist/krl.js.map +1 -1
  48. package/dist/proof.d.ts +36 -7
  49. package/dist/proof.d.ts.map +1 -1
  50. package/dist/proof.js +53 -21
  51. package/dist/proof.js.map +1 -1
  52. package/dist/router.d.ts.map +1 -1
  53. package/dist/router.js +3 -1
  54. package/dist/router.js.map +1 -1
  55. package/dist/ssh/certificate.d.ts +93 -0
  56. package/dist/ssh/certificate.d.ts.map +1 -0
  57. package/dist/ssh/certificate.js +178 -0
  58. package/dist/ssh/certificate.js.map +1 -0
  59. package/dist/ssh/encoding.d.ts +41 -0
  60. package/dist/ssh/encoding.d.ts.map +1 -0
  61. package/dist/ssh/encoding.js +70 -0
  62. package/dist/ssh/encoding.js.map +1 -0
  63. package/dist/ssh/keys.d.ts +80 -0
  64. package/dist/ssh/keys.d.ts.map +1 -0
  65. package/dist/ssh/keys.js +249 -0
  66. package/dist/ssh/keys.js.map +1 -0
  67. package/dist/ssh/signer.d.ts +35 -0
  68. package/dist/ssh/signer.d.ts.map +1 -0
  69. package/dist/ssh/signer.js +99 -0
  70. package/dist/ssh/signer.js.map +1 -0
  71. package/dist/types.d.ts +12 -17
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utils.d.ts +40 -21
  74. package/dist/utils.d.ts.map +1 -1
  75. package/dist/utils.js +87 -54
  76. package/dist/utils.js.map +1 -1
  77. package/openapi.json +1960 -82
  78. package/package.json +5 -17
  79. package/src/api/operations.ts +211 -0
  80. package/src/api/schema.ts +137 -0
  81. package/src/api/v3/index.ts +32 -259
  82. package/src/api/v3/schema.ts +34 -96
  83. package/src/api/v4/index.ts +265 -0
  84. package/src/api/v4/schema.ts +235 -0
  85. package/src/certificate.ts +62 -123
  86. package/src/db/index.ts +113 -49
  87. package/src/db/migrations/0002_krl_version.ts +16 -0
  88. package/src/db/migrations/index.ts +3 -1
  89. package/src/db/types.ts +40 -0
  90. package/src/krl.ts +32 -194
  91. package/src/proof.ts +65 -31
  92. package/src/router.ts +3 -1
  93. package/src/ssh/certificate.ts +236 -0
  94. package/src/ssh/encoding.ts +81 -0
  95. package/src/ssh/keys.ts +294 -0
  96. package/src/ssh/signer.ts +129 -0
  97. package/src/types.ts +12 -18
  98. package/src/utils.ts +99 -69
@@ -0,0 +1,129 @@
1
+ import * as P from "micro-packed"
2
+ import { toFixedWidth } from "../sshsig/sig_parser.js"
3
+ import { decodePublicKey, SSHBytes, SSHText, SSHUnsignedMpint } from "./encoding.js"
4
+ import type { EcdsaCurve, PrivateKey, PublicKey } from "./keys.js"
5
+
6
+ /**
7
+ * Signs data with a CA key using WebCrypto, returning signatures in the SSH
8
+ * wire format
9
+ */
10
+ export interface Signer {
11
+ /** The wire encoding of the public key of the signer */
12
+ readonly publicKey: Uint8Array
13
+ /**
14
+ * Signs data
15
+ *
16
+ * @param data The data to sign
17
+ * @returns The signature in the SSH wire format (RFC 4253 section 6.6)
18
+ */
19
+ sign(data: Uint8Array<ArrayBuffer>): Promise<Uint8Array>
20
+ }
21
+
22
+ /** An Ed25519 signature: string algorithm, string signature */
23
+ const Ed25519Signature = P.struct({
24
+ algorithm: SSHText,
25
+ signature: SSHBytes,
26
+ })
27
+
28
+ /** An ECDSA signature: string algorithm, string (mpint r, mpint s) */
29
+ const EcdsaSignature = P.struct({
30
+ algorithm: SSHText,
31
+ signature: P.prefix(P.U32BE, P.struct({
32
+ r: SSHUnsignedMpint,
33
+ s: SSHUnsignedMpint,
34
+ })),
35
+ })
36
+
37
+ const EcdsaCurves: Readonly<Record<EcdsaCurve, { namedCurve: string, hash: string, size: number }>> = {
38
+ nistp256: { namedCurve: "P-256", hash: "SHA-256", size: 32 },
39
+ nistp384: { namedCurve: "P-384", hash: "SHA-384", size: 48 },
40
+ nistp521: { namedCurve: "P-521", hash: "SHA-512", size: 66 },
41
+ }
42
+
43
+ /**
44
+ * Creates a {@link Signer} for an Ed25519 or ECDSA (P-256, P-384 or P-521)
45
+ * private key
46
+ *
47
+ * @param key The private key to sign with
48
+ * @returns The signer
49
+ */
50
+ export const createSigner = async (key: PrivateKey): Promise<Signer> => {
51
+ const publicKey = key.publicKey.blob
52
+
53
+ if (key.type === "ed25519") {
54
+ const cryptoKey = await crypto.subtle.importKey("jwk", key.jwk, { name: "Ed25519" }, false, ["sign"])
55
+
56
+ return {
57
+ publicKey,
58
+ sign: async (data) => Ed25519Signature.encode({
59
+ algorithm: "ssh-ed25519",
60
+ signature: new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, cryptoKey, data)),
61
+ }),
62
+ }
63
+ }
64
+
65
+ const curve = EcdsaCurves[key.curve]
66
+ const cryptoKey = await crypto.subtle.importKey("jwk", key.jwk, { name: "ECDSA", namedCurve: curve.namedCurve }, false, ["sign"])
67
+
68
+ return {
69
+ publicKey,
70
+ sign: async (data) => {
71
+ // WebCrypto returns r || s, each the size of the curve
72
+ const signature = new Uint8Array(await crypto.subtle.sign({ name: "ECDSA", hash: { name: curve.hash } }, cryptoKey, data))
73
+
74
+ return EcdsaSignature.encode({
75
+ // the algorithm is named for the curve of the key, never
76
+ // worked out from the signature
77
+ algorithm: `ecdsa-sha2-${key.curve}`,
78
+ signature: {
79
+ r: signature.subarray(0, curve.size),
80
+ s: signature.subarray(curve.size),
81
+ },
82
+ })
83
+ },
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Verifies a signature in the SSH wire format made with an Ed25519 or ECDSA
89
+ * key
90
+ *
91
+ * @param publicKey The key the signature must have been made with
92
+ * @param signature The signature in the SSH wire format
93
+ * @param data The data that was signed
94
+ * @returns true if the signature is valid, false if it is not or the key type is not supported
95
+ */
96
+ export const verify = async (publicKey: PublicKey, signature: Uint8Array, data: Uint8Array<ArrayBuffer>): Promise<boolean> => {
97
+ try {
98
+ const { type, fields } = decodePublicKey(publicKey.blob)
99
+
100
+ if (type === "ssh-ed25519") {
101
+ const { algorithm, signature: raw } = Ed25519Signature.decode(signature)
102
+ if (algorithm !== type) {
103
+ return false
104
+ }
105
+ const cryptoKey = await crypto.subtle.importKey("raw", new Uint8Array(fields[0]!), { name: "Ed25519" }, false, ["verify"])
106
+
107
+ return await crypto.subtle.verify({ name: "Ed25519" }, cryptoKey, new Uint8Array(raw), data)
108
+ }
109
+
110
+ const curveName = type.replace(/^ecdsa-sha2-/, "") as EcdsaCurve
111
+ const curve = EcdsaCurves[curveName]
112
+ if (curve !== undefined && type === `ecdsa-sha2-${curveName}`) {
113
+ const { algorithm, signature: { r, s } } = EcdsaSignature.decode(signature)
114
+ if (algorithm !== type || r.length > curve.size || s.length > curve.size) {
115
+ return false
116
+ }
117
+ // fields are the curve name and the public point
118
+ const cryptoKey = await crypto.subtle.importKey("raw", new Uint8Array(fields[1]!), { name: "ECDSA", namedCurve: curve.namedCurve }, false, ["verify"])
119
+ const raw = P.utils.concatBytes(toFixedWidth(r, curve.size), toFixedWidth(s, curve.size))
120
+
121
+ return await crypto.subtle.verify({ name: "ECDSA", hash: { name: curve.hash } }, cryptoKey, new Uint8Array(raw), data)
122
+ }
123
+
124
+ return false
125
+ } catch {
126
+ // a signature or key that cannot be decoded or imported is not valid
127
+ return false
128
+ }
129
+ }
package/src/types.ts CHANGED
@@ -20,12 +20,6 @@ export type CertificateRequestJWTPayload = {
20
20
  [key: string]: string | string[]
21
21
  } & JWTPayload
22
22
 
23
- export type SSHExtension = {
24
- critical: boolean;
25
- name: string;
26
- data: Buffer<ArrayBuffer>
27
- }
28
-
29
23
  /**
30
24
  * The result of executing a statement via {@link CaPreparedStatement.run} or
31
25
  * {@link CaDatabase.batch}, modelled on a Cloudflare D1 result.
@@ -99,23 +93,23 @@ export interface CaSecret {
99
93
  * {@link CaSecret} for the non-string bindings.
100
94
  */
101
95
  export interface SshCaBindings {
102
- DB: CaDatabase
96
+ CERTIFICATE_REQUEST_TIME_SKEW_MAX: string
97
+ JWT_ALGORITHMS: string | string[]
98
+ JWT_AUD?: string | string[]
99
+ JWT_ISSUER: string
100
+ JWT_JWKS_URL: string
101
+ JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: string
102
+ LOG_LEVEL?: string
103
103
  DB_CERTIFICATE_RETENTION: string
104
+ DB: CaDatabase
105
+ ISSUER_DN: string
104
106
  PRIVATE_KEY: CaSecret
105
107
  SSH_CERTIFICATE_EXTENSIONS: string
106
- SSH_CERTIFICATE_LIFETIME: string
107
- SSH_CERTIFICATE_INCLUDE_SELF?: string | boolean
108
108
  SSH_CERTIFICATE_INCLUDE_SELF_EMAIL?: string | boolean
109
+ SSH_CERTIFICATE_INCLUDE_SELF?: string | boolean
110
+ SSH_CERTIFICATE_LIFETIME: string
109
111
  SSH_CERTIFICATE_PRINCIPALS?: string | string[]
110
- SSH_HOST_CERTIFICATE_LIFETIME: string
111
112
  SSH_HOST_CERTIFICATE_ALLOWED_EMAILS?: string | string[]
112
113
  SSH_HOST_CERTIFICATE_ALLOWED_ROLES?: string | string[]
113
- ISSUER_DN: string
114
- JWT_JWKS_URL: string
115
- JWT_AUD?: string | string[]
116
- JWT_ISSUER: string
117
- JWT_ALGORITHMS: string | string[]
118
- JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: string
119
- CERTIFICATE_REQUEST_TIME_SKEW_MAX: string
120
- LOG_LEVEL?: string
114
+ SSH_HOST_CERTIFICATE_LIFETIME: string
121
115
  }
package/src/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { SshCaBindings } from "./types.js"
2
2
  import { JWKInvalid, JWKSInvalid, JWSInvalid, JWSSignatureVerificationFailed, JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "jose/errors"
3
- import sshpk from "sshpk"
4
- import type { Certificate, Key, PrivateKey } from "sshpk"
3
+ import { CertificateParseError, SshCertificate } from "./ssh/certificate.js"
4
+ import { KeyParseError, parsePrivateKey, PublicKey, UnsupportedKeyError, type PrivateKey } from "./ssh/keys.js"
5
5
  import z from "zod"
6
6
  import { verifyJWT } from "./verify.js"
7
7
  import { CertificateRequestJWTPayload } from "./types.js"
@@ -11,8 +11,7 @@ import { logger } from "./logger.js"
11
11
  import { ms } from "itty-time"
12
12
  import { InternalServerErrorException } from "chanfana"
13
13
 
14
- // sshpk is CommonJS and Node's ESM loader cannot detect most of its named exports, so values are taken from the default import
15
- const { KeyParseError, CertificateParseError, parseCertificate, parseKey, parsePrivateKey } = sshpk
14
+ export { UnsupportedKeyError }
16
15
 
17
16
  export const fatalIssue = (ctx: z.RefinementCtx, message: string, val: unknown) => {
18
17
  ctx.issues.push({
@@ -104,11 +103,20 @@ export const transformAuthorizationHeader = async (env: SshCaBindings, val: stri
104
103
  }
105
104
  }
106
105
 
107
- export const transformPublicKey = (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx): Key | never => {
108
- try {
109
- const key = typeof val === "string" ? parseKey(Buffer.from(val, "base64")) : parseKey(val)
106
+ // decodes a value sent as base64, which may already have been decoded by the schema
107
+ const fromBase64 = (val: string | Buffer<ArrayBufferLike>): string => {
108
+ return (typeof val === "string" ? Buffer.from(val, "base64") : Buffer.from(val)).toString("utf8")
109
+ }
110
110
 
111
- return key
111
+ /**
112
+ * Parses a public key in OpenSSH format, `${type} ${base64}[ ${comment}]`
113
+ *
114
+ * @param val The public key, either base64 encoded or as bytes
115
+ * @param ctx The Zod context for any errors
116
+ */
117
+ export const transformPublicKey = (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx): PublicKey | never => {
118
+ try {
119
+ return PublicKey.parse(fromBase64(val))
112
120
  } catch (err) {
113
121
  switch (true) {
114
122
  case (err instanceof TypeError):
@@ -234,11 +242,9 @@ export const transformIdentityToken = async (env: SshCaBindings, val: string, ct
234
242
  * @param ctx The Zod context for any errors
235
243
  * @returns
236
244
  */
237
- export const transformCertificate = (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx): Certificate | never => {
245
+ export const transformCertificate = (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx): SshCertificate | never => {
238
246
  try {
239
- const cert = typeof val === "string" ? parseCertificate(Buffer.from(val, "base64"), "openssh") : parseCertificate(val, "openssh")
240
-
241
- return cert
247
+ return SshCertificate.parse(fromBase64(val))
242
248
  } catch (err) {
243
249
  switch (true) {
244
250
  case (err instanceof TypeError):
@@ -269,7 +275,7 @@ export const transformCertificate = (val: string | Buffer<ArrayBufferLike>, ctx:
269
275
 
270
276
  type ParsedCertificateRequest = {
271
277
  proof: ProofOfPossession
272
- public_key: Key
278
+ public_key: PublicKey
273
279
  lifetime: number
274
280
  identity: IdentityToken
275
281
  extensions: string[]
@@ -311,7 +317,7 @@ export const transformRenewalProofOfPossession = (env: SshCaBindings, val: strin
311
317
  }
312
318
 
313
319
  type HostCertificateRequest = {
314
- public_key: Key
320
+ public_key: PublicKey
315
321
  lifetime: number
316
322
  }
317
323
 
@@ -327,7 +333,7 @@ export const refineHostCertificateRequest = async (env: SshCaBindings, val: Pars
327
333
 
328
334
  try {
329
335
  // check proof of possession fingerprint matches public key
330
- l.debug("checking val.proof.matches()", "public_key", val.public_key.toString("ssh"))
336
+ l.debug("checking val.proof.matches()", "public_key", val.public_key.toString())
331
337
  if (!val.proof.matches(val.public_key)) {
332
338
  l.warn("proof of possession fingerprint did not match public_key")
333
339
  return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val)
@@ -351,38 +357,86 @@ export const refineHostCertificateRequest = async (env: SshCaBindings, val: Pars
351
357
  }
352
358
 
353
359
  type ParsedHostCertificateRenewal = {
354
- certificate: Certificate
360
+ certificate: SshCertificate
355
361
  proof: RenewalProofOfPossession
356
362
  } & HostCertificateRequest
357
363
 
358
- export const refineHostCertificateRenewal = async (env: SshCaBindings, isRevoked: typeof IsRevokedFn, val: ParsedHostCertificateRenewal, ctx: z.RefinementCtx): Promise<never> => {
364
+ /**
365
+ * Checks a certificate presented for renewal was issued by this CA, is a host
366
+ * certificate, is within its validity period and has not been revoked.
367
+ *
368
+ * @param env The CA bindings
369
+ * @param isRevoked Returns whether a serial has been revoked
370
+ * @param certificate The certificate presented for renewal
371
+ * @returns The reason the certificate cannot be renewed, or undefined if it can be
372
+ * @throws {@link UnsupportedKeyError} when the CA key is not supported
373
+ */
374
+ export const checkRenewableCertificate = async (env: SshCaBindings, isRevoked: typeof IsRevokedFn, certificate: SshCertificate): Promise<string | undefined> => {
375
+ // ensure certificate is signed by CA
376
+ const issuer = await getPublic(env)
377
+ if (!await certificate.isSignedBy(issuer)) {
378
+ return "the provided certificate was not signed by this CA"
379
+ }
380
+
381
+ // only host certificates can be renewed, as renewal does not need the
382
+ // OIDC authentication a user certificate is issued with
383
+ if (certificate.kind !== "host") {
384
+ return "the provided certificate is not a host certificate"
385
+ }
386
+
387
+ // check certificate is within its validity period, allowing for clock
388
+ // skew at the start as a certificate issued moments ago may have a
389
+ // validity start up to a second in the future
390
+ const now = Date.now()
391
+ if (now >= certificate.validBefore.getTime()) {
392
+ return "the provided certificate is expired"
393
+ }
394
+ if (certificate.validAfter.getTime() > now + ms(env.CERTIFICATE_REQUEST_TIME_SKEW_MAX)) {
395
+ return "the provided certificate is not yet valid"
396
+ }
397
+
398
+ // ensure certificate presented for renewal is not revoked
399
+ if (await isRevoked(env, certificate.serial)) {
400
+ return "the provided certificate is revoked"
401
+ }
402
+
403
+ return undefined
404
+ }
405
+
406
+ /**
407
+ * Refines a host certificate renewal so only a certificate that can be
408
+ * renewed (see {@link checkRenewableCertificate}) is accepted.
409
+ */
410
+ export const refineRenewableCertificate = async (env: SshCaBindings, isRevoked: typeof IsRevokedFn, val: { certificate: SshCertificate }, ctx: z.RefinementCtx): Promise<never> => {
359
411
  const l = logger(env)
360
412
  try {
361
- // ensure certificate is signed by CA
362
- const issuer = await getPublic(env)
363
- if (!val.certificate.isSignedByKey(issuer)) {
364
- return fatalIssue(ctx, "the provided certificate was not signed by this CA", val)
413
+ const reason = await checkRenewableCertificate(env, isRevoked, val.certificate)
414
+ if (reason !== undefined) {
415
+ return fatalIssue(ctx, reason, val)
365
416
  }
366
417
 
367
- // check certificate is within its validity period, allowing for clock
368
- // skew at the start as a certificate issued moments ago may have a
369
- // validity start up to a second in the future
370
- const now = Date.now()
371
- if (now >= val.certificate.validUntil.getTime()) {
372
- return fatalIssue(ctx, "the provided certificate is expired", val)
373
- }
374
- if (val.certificate.validFrom.getTime() > now + ms(env.CERTIFICATE_REQUEST_TIME_SKEW_MAX)) {
375
- return fatalIssue(ctx, "the provided certificate is not yet valid", val)
418
+ return z.NEVER
419
+ } catch (err) {
420
+ // a misconfigured CA key is a server error, not a validation failure
421
+ if (err instanceof UnsupportedKeyError) {
422
+ l.error(err.message, "in", "refineRenewableCertificate")
423
+ throw new InternalServerErrorException(err.message)
376
424
  }
425
+ l.error("certificate renewal check unhandled error", "in", "refineRenewableCertificate", "error", err)
426
+ return fatalIssue(ctx, "certificate renewal check unhandled error", val)
427
+ }
428
+ }
377
429
 
378
- // ensure certificate presented for renewal is not revoked
379
- const serial = val.certificate.serial.readBigUInt64BE(0)
380
- if (await isRevoked(env, serial)) {
381
- return fatalIssue(ctx, "the provided certificate is revoked", val)
430
+ export const refineHostCertificateRenewal = async (env: SshCaBindings, isRevoked: typeof IsRevokedFn, val: ParsedHostCertificateRenewal, ctx: z.RefinementCtx): Promise<never> => {
431
+ const l = logger(env)
432
+ try {
433
+ const reason = await checkRenewableCertificate(env, isRevoked, val.certificate)
434
+ if (reason !== undefined) {
435
+ return fatalIssue(ctx, reason, val)
382
436
  }
383
437
 
384
438
  // check proof of possession fingerprint matches public key and certificate subject key
385
- if (!val.proof.matches(val.public_key, val.certificate.subjectKey)) {
439
+ if (!val.proof.matches(val.public_key, val.certificate.publicKey)) {
386
440
  return fatalIssue(ctx, "proof of possession fingerprints did not match public_key and/or certificate", val)
387
441
  }
388
442
 
@@ -406,7 +460,7 @@ export const refineHostCertificateRenewal = async (env: SshCaBindings, isRevoked
406
460
 
407
461
  type ParsedRevokeCertificate = {
408
462
  serial: bigint
409
- public_key: Key
463
+ public_key: PublicKey
410
464
  proof: ProofOfPossession
411
465
  }
412
466
 
@@ -441,53 +495,29 @@ export const split = (v?: string | string[]): string[] => {
441
495
  : v
442
496
  }
443
497
 
444
- /**
445
- * Thrown by {@link getPrivateKey} when the CA private key is of a type that
446
- * cannot be used.
447
- */
448
- export class UnsupportedKeyError extends Error {
449
- constructor(message: string) {
450
- super(message)
451
- this.name = "UnsupportedKeyError"
452
-
453
- Object.setPrototypeOf(this, UnsupportedKeyError.prototype)
454
- }
455
- }
456
-
457
498
  /**
458
499
  * Load and parse the CA private key from the `PRIVATE_KEY` secret.
459
500
  *
460
- * Only Ed25519 and ECDSA keys are supported. RSA keys are rejected because
461
- * sshpk signs RSA certificates with SHA-1 (`ssh-rsa`), which OpenSSH 8.2 and
462
- * later refuse by default, and because RSA keys in OpenSSH format are
463
- * converted with invalid CRT parameters that the Workers runtime rejects.
501
+ * Only Ed25519 and ECDSA keys are supported, as these are the key types that
502
+ * certificates and KRLs are signed with (see `createSigner` in `ssh/signer.ts`).
464
503
  *
465
- * @throws KeyParseError when the secret cannot be parsed.
504
+ * @throws {@link KeyParseError} when the secret cannot be parsed.
466
505
  * @throws {@link UnsupportedKeyError} when the key is not Ed25519 or ECDSA.
467
506
  */
468
507
  export const getPrivateKey = async (env: SshCaBindings): Promise<PrivateKey> => {
469
508
  // grab private key from secret store
470
509
  const secret = await env.PRIVATE_KEY.get()
471
510
 
472
- // parse it
473
- const key = parsePrivateKey(secret)
474
-
475
- if (key.type !== "ed25519" && key.type !== "ecdsa") {
476
- throw new UnsupportedKeyError(`CA key type ${key.type} is not supported, the CA key must be Ed25519 or ECDSA`)
477
- }
478
-
479
- return key
511
+ return parsePrivateKey(secret)
480
512
  }
481
513
 
482
- export const getPublic = async (env: SshCaBindings, key?: PrivateKey): Promise<Key> => {
514
+ /**
515
+ * Returns the public key of the CA, with `ISSUER_DN` as its comment
516
+ */
517
+ export const getPublic = async (env: SshCaBindings, key?: PrivateKey): Promise<PublicKey> => {
483
518
  if (key === undefined) {
484
519
  key = await getPrivateKey(env)
485
520
  }
486
521
 
487
- // convert to a public key and add comment
488
- const pub = key.toPublic()
489
- pub.comment = env.ISSUER_DN
490
-
491
- // return in ssh format trimmed of any whitespace
492
- return pub
522
+ return key.publicKey.withComment(env.ISSUER_DN)
493
523
  }