@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
package/src/krl.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { AlgorithmPart, PrivateKey } from "sshpk"
2
- import { toFixedWidth } from "./sshsig/sig_parser.js"
1
+ import type { PrivateKey } from "./ssh/keys.js"
2
+ import { createSigner } from "./ssh/signer.js"
3
3
 
4
4
  // ── Constants ────────────────────────────────────────────────────────────────
5
5
 
@@ -59,32 +59,6 @@ function encodeSection(sectionType: number, data: Uint8Array): Uint8Array {
59
59
  return concat(new Uint8Array([sectionType]), encodeString(data))
60
60
  }
61
61
 
62
- // ── CA public-key extraction ─────────────────────────────────────────────────
63
-
64
- /**
65
- * Extract the SSH wire-format public key bytes from an sshpk PrivateKey.
66
- *
67
- * sshpk's "ssh" format produces a string like:
68
- * ssh-ed25519 AAAA...base64... [comment]
69
- *
70
- * The base64 blob *is* the SSH wire encoding of the public key, so we just
71
- * decode it. This works for both Ed25519 and ECDSA keys.
72
- */
73
- function caKeyWireBytes(caKey: PrivateKey): Uint8Array {
74
- const sshString = caKey.toPublic().toString("ssh")
75
- // Format: "<type> <base64> [<comment>]" — we want the second token.
76
- const base64 = sshString.trim().split(/\s+/)[1]
77
- if (!base64) {
78
- throw new Error("Unexpected sshpk 'ssh' format output: " + sshString)
79
- }
80
- const binary = atob(base64)
81
- const bytes = new Uint8Array(binary.length)
82
- for (let i = 0; i < binary.length; i++) {
83
- bytes[i] = binary.charCodeAt(i)
84
- }
85
- return bytes
86
- }
87
-
88
62
  // ── Constants ────────────────────────────────────────────────────────────────
89
63
 
90
64
  const SSHSIG_MAGIC = new TextEncoder().encode("SSHSIG")
@@ -92,156 +66,6 @@ const SIG_VERSION = 1
92
66
  const NAMESPACE = "krl@com.github.serverless-ssh-ca.andrewheberle"
93
67
  const HASH_ALGORITHM = "sha512"
94
68
 
95
- // ── Helpers ──────────────────────────────────────────────────────────────────
96
-
97
- /**
98
- * Get the raw bytes of a named part of an sshpk key.
99
- */
100
- function keyPart(key: PrivateKey, name: AlgorithmPart): Uint8Array {
101
- const part = key.parts.find((p) => p.name === name)
102
- if (part === undefined) {
103
- throw new Error(`Missing ${name} part in ${key.type} key`)
104
- }
105
- return part.data
106
- }
107
-
108
- /**
109
- * Base64url-encode bytes without padding, as used for JWK values.
110
- */
111
- function base64url(bytes: Uint8Array): string {
112
- let binary = ""
113
- for (const byte of bytes) {
114
- binary += String.fromCharCode(byte)
115
- }
116
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
117
- }
118
-
119
- /**
120
- * Import an sshpk PrivateKey into a WebCrypto CryptoKey.
121
- * Supports Ed25519 and ECDSA (P-256 / P-384 / P-521).
122
- */
123
- async function importPrivateKey(caKey: PrivateKey): Promise<{
124
- cryptoKey: CryptoKey
125
- sigAlgo: string // SSH signature algorithm name
126
- webCryptoAlgo: Parameters<SubtleCrypto["sign"]>[0]
127
- }> {
128
- const type = caKey.type // "ed25519" | "ecdsa" | etc.
129
- const curve = (caKey as unknown as { curve?: string }).curve
130
-
131
- if (type === "ed25519") {
132
- // import from the raw key parts as a JWK rather than via sshpk's PKCS#8
133
- // output, which drops a leading zero byte from the seed (about 1 key in
134
- // 512) and is then rejected by WebCrypto
135
- const seed = keyPart(caKey, "k")
136
- const publicKey = keyPart(caKey, "A")
137
- if (seed.length !== 32 || publicKey.length !== 32) {
138
- throw new Error(`Unexpected Ed25519 key part lengths: k=${seed.length} A=${publicKey.length}`)
139
- }
140
- const cryptoKey = await crypto.subtle.importKey(
141
- "jwk",
142
- { kty: "OKP", crv: "Ed25519", d: base64url(seed), x: base64url(publicKey) },
143
- { name: "Ed25519" },
144
- false,
145
- ["sign"],
146
- )
147
- return {
148
- cryptoKey,
149
- sigAlgo: "ssh-ed25519",
150
- webCryptoAlgo: { name: "Ed25519" },
151
- }
152
- } else if (type === "ecdsa") {
153
- let namedCurve: string
154
- let sigAlgo: string
155
- let hash: string
156
- let size: number // bytes in each coordinate and the private key
157
-
158
- if (curve === "nistp256") {
159
- namedCurve = "P-256"
160
- sigAlgo = "ecdsa-sha2-nistp256"
161
- hash = "SHA-256"
162
- size = 32
163
- } else if (curve === "nistp384") {
164
- namedCurve = "P-384"
165
- sigAlgo = "ecdsa-sha2-nistp384"
166
- hash = "SHA-384"
167
- size = 48
168
- } else if (curve === "nistp521") {
169
- namedCurve = "P-521"
170
- sigAlgo = "ecdsa-sha2-nistp521"
171
- hash = "SHA-512"
172
- size = 66
173
- } else {
174
- throw new Error(`Unsupported ECDSA curve: ${curve}`)
175
- }
176
-
177
- // import from the raw key parts as a JWK rather than via sshpk's PKCS#8
178
- // output, which drops leading zero bytes from the private key so it is
179
- // shorter than RFC 5915 requires (about half of P-521 keys)
180
- const point = keyPart(caKey, "Q")
181
- if (point.length !== 1 + size * 2 || point[0] !== 0x04) {
182
- throw new Error(`Unexpected ECDSA public key encoding for ${curve}`)
183
- }
184
- // d is an SSH mpint, so may be short or have a leading sign byte
185
- const d = toFixedWidth(keyPart(caKey, "d"), size)
186
- const cryptoKey = await crypto.subtle.importKey(
187
- "jwk",
188
- {
189
- kty: "EC",
190
- crv: namedCurve,
191
- d: base64url(d),
192
- x: base64url(point.subarray(1, 1 + size)),
193
- y: base64url(point.subarray(1 + size)),
194
- },
195
- { name: "ECDSA", namedCurve },
196
- false,
197
- ["sign"],
198
- )
199
- return {
200
- cryptoKey,
201
- sigAlgo,
202
- webCryptoAlgo: { name: "ECDSA", hash: { name: hash } },
203
- }
204
- } else {
205
- throw new Error(`Unsupported key type: ${type}`)
206
- }
207
- }
208
-
209
- /**
210
- * Convert a WebCrypto ECDSA signature (IEEE P1363: raw r||s) to the SSH wire
211
- * encoding: string(algo) + string(mpint(r) + mpint(s)).
212
- *
213
- * SSH mpints are big-endian, with a leading 0x00 byte if the high bit is set.
214
- */
215
- function p1363ToSshEcdsaSig(
216
- p1363: Uint8Array,
217
- sigAlgo: string,
218
- ): Uint8Array {
219
- const half = p1363.length / 2
220
- const r = p1363.slice(0, half)
221
- const s = p1363.slice(half)
222
-
223
- function encodeMpint(n: Uint8Array): Uint8Array {
224
- // Strip leading zero bytes, then re-pad if high bit set.
225
- let start = 0
226
- while (start < n.length - 1 && n[start] === 0) start++
227
- const trimmed = n.slice(start)
228
- const padded =
229
- trimmed[0]! & 0x80 ? concat(new Uint8Array([0x00]), trimmed) : trimmed
230
- return encodeString(padded)
231
- }
232
-
233
- const innerSig = concat(encodeMpint(r), encodeMpint(s))
234
- return concat(encodeString(sigAlgo), encodeString(innerSig))
235
- }
236
-
237
- /**
238
- * Wrap an Ed25519 WebCrypto signature into SSH wire encoding:
239
- * string(algo) + string(sig_bytes)
240
- */
241
- function wrapEd25519Sig(sig: Uint8Array): Uint8Array {
242
- return concat(encodeString("ssh-ed25519"), encodeString(sig))
243
- }
244
-
245
69
  /**
246
70
  * Base64-encode bytes and wrap in SSH signature armor at 76 chars per line.
247
71
  */
@@ -298,6 +122,29 @@ export class KRLBuilder {
298
122
  return this
299
123
  }
300
124
 
125
+ /**
126
+ * Return a hex encoded SHA-256 digest of the content of the KRL.
127
+ *
128
+ * The digest covers the CA public key and the revoked serials and key IDs,
129
+ * but not the order they were added in or the header, so it only changes
130
+ * when the set of revoked certificates or the CA does.
131
+ */
132
+ async digest(): Promise<string> {
133
+ const serials = [...this._serials].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
134
+ const keyIds = [...this._keyIds].sort()
135
+
136
+ const content = concat(
137
+ encodeString(this._caKey.publicKey.blob),
138
+ encodeUint32(serials.length),
139
+ ...serials.map(encodeUint64),
140
+ encodeUint32(keyIds.length),
141
+ ...keyIds.map((id) => encodeString(id)),
142
+ )
143
+
144
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", content))
145
+ return Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("")
146
+ }
147
+
301
148
  /**
302
149
  * Generate the binary KRL
303
150
  *
@@ -352,7 +199,7 @@ export class KRLBuilder {
352
199
  // <one or more cert sub-sections>
353
200
  //
354
201
  const certSectionData = concat(
355
- encodeString(caKeyWireBytes(this._caKey)), // ca_key
202
+ encodeString(this._caKey.publicKey.blob), // ca_key
356
203
  encodeString(""), // reserved
357
204
  ...certSubSections
358
205
  )
@@ -386,7 +233,7 @@ export class KRLBuilder {
386
233
  } else {
387
234
  krl = this._krl
388
235
  }
389
- const { cryptoKey, sigAlgo, webCryptoAlgo } = await importPrivateKey(this._caKey)
236
+ const signer = await createSigner(this._caKey)
390
237
 
391
238
  // 1. Hash the KRL bytes with SHA-512.
392
239
  const krlHash = new Uint8Array(
@@ -408,18 +255,10 @@ export class KRLBuilder {
408
255
  encodeString(krlHash),
409
256
  )
410
257
 
411
- // 3. Sign the blob.
412
- const rawSig = new Uint8Array(
413
- await crypto.subtle.sign(webCryptoAlgo, cryptoKey, signedData),
414
- )
415
-
416
- // 4. Encode the signature into SSH wire format.
417
- const sshSig =
418
- sigAlgo === "ssh-ed25519"
419
- ? wrapEd25519Sig(rawSig)
420
- : p1363ToSshEcdsaSig(rawSig, sigAlgo)
258
+ // 3. Sign the blob, which returns the signature in SSH wire format.
259
+ const sshSig = await signer.sign(signedData)
421
260
 
422
- // 5. Assemble the outer SSHSIG blob.
261
+ // 4. Assemble the outer SSHSIG blob.
423
262
  //
424
263
  // byte[6] "SSHSIG"
425
264
  // uint32 SIG_VERSION (1)
@@ -428,18 +267,17 @@ export class KRLBuilder {
428
267
  // string reserved (empty)
429
268
  // string hash_algorithm
430
269
  // string signature
431
- const publicKeyWire = caKeyWireBytes(this._caKey)
432
270
  const sigBlob = concat(
433
271
  SSHSIG_MAGIC,
434
272
  encodeUint32(SIG_VERSION),
435
- encodeString(publicKeyWire),
273
+ encodeString(signer.publicKey),
436
274
  encodeString(NAMESPACE),
437
275
  encodeString(""), // reserved
438
276
  encodeString(HASH_ALGORITHM),
439
277
  encodeString(sshSig),
440
278
  )
441
279
 
442
- // 6. Armor and return.
280
+ // 5. Armor and return.
443
281
  return armorSignature(sigBlob)
444
282
  }
445
283
  }
package/src/proof.ts CHANGED
@@ -1,16 +1,34 @@
1
1
  import { ms } from "itty-time"
2
- import sshpk from "sshpk"
3
- import type { Fingerprint, Key } from "sshpk"
2
+ import { KeyParseError, PublicKey } from "./ssh/keys.js"
4
3
  import { verify } from "./sshsig/index.js"
5
4
  import { parse } from "./sshsig/sig_parser.js"
6
5
  import { Sig } from "./sshsig/sig.js"
7
6
  import { group, Logger, LogLevel } from "@andrewheberle/ts-slog"
8
7
 
9
- // sshpk is CommonJS and Node's ESM loader cannot detect most of its named exports, so values are taken from the default import
10
- const { FingerprintFormatError, parseFingerprint, parseKey } = sshpk
8
+ // a SHA256 fingerprint as shown by ssh-keygen -l, which is unpadded base64
9
+ const FingerprintFormat = /^SHA256:[A-Za-z0-9+/]{43}$/
11
10
 
12
11
  export const Namespace = "proof-of-possession@com.github.serverless-ssh-ca.andrewheberle"
13
12
 
13
+ /**
14
+ * The namespace used for proofs of possession that are bound to a request,
15
+ * kept separate from {@link Namespace} so a proof made for one form can never
16
+ * be accepted as the other.
17
+ */
18
+ export const RequestNamespace = "request-proof-of-possession@com.github.serverless-ssh-ca.andrewheberle"
19
+
20
+ /**
21
+ * Returns the request a proof of possession is bound to, which is appended to
22
+ * the signed data as `${timestamp}.${fingerprint}.${binding}`.
23
+ *
24
+ * @param method The HTTP method of the request, for example "DELETE"
25
+ * @param path The path of the request with any parameters filled in, for example "/api/v4/user/certificates/123"
26
+ * @returns The request binding, for example "DELETE /api/v4/user/certificates/123"
27
+ */
28
+ export const requestBinding = (method: string, path: string): string => {
29
+ return `${method.toUpperCase()} ${path}`
30
+ }
31
+
14
32
  export class PossessionParseError extends Error {
15
33
  constructor(message: string, cause?: unknown) {
16
34
  super(message)
@@ -38,9 +56,10 @@ export class PossessionParseError extends Error {
38
56
  */
39
57
  export class ProofOfPossession {
40
58
  readonly timestamp: number
41
- readonly fingerprint: Fingerprint
59
+ /** The SHA256 fingerprint of the key, for example "SHA256:79mq0wtpQMTvS4+Of8VzLN0qmYWNUyTXYmqKwhEgSLs" */
60
+ readonly fingerprint: string
42
61
  readonly signature: Sig
43
- readonly signaturePubkey: Key
62
+ readonly signaturePubkey: PublicKey
44
63
  readonly logger: Logger
45
64
  private readonly data: string
46
65
 
@@ -69,10 +88,9 @@ constructor(proof: string, options?: { from?: number, logger?: Logger, skew?: nu
69
88
  throw new PossessionParseError("timestamp was from the future")
70
89
  }
71
90
  try {
72
- // parse fingerprint
73
- const fingerprint = parseFingerprint(fingerprintString!)
74
- if (fingerprint === undefined) {
75
- throw new PossessionParseError("fingerprint did not parse")
91
+ // check fingerprint
92
+ if (!FingerprintFormat.test(fingerprintString!)) {
93
+ throw new PossessionParseError("proof of possession fingerprint was an invalid format")
76
94
  }
77
95
 
78
96
  // convert siganture from base64
@@ -86,26 +104,26 @@ constructor(proof: string, options?: { from?: number, logger?: Logger, skew?: nu
86
104
  throw new PossessionParseError("proof of possession signature could not be parsed", err)
87
105
  }
88
106
 
89
- this.fingerprint = fingerprint
107
+ this.fingerprint = fingerprintString!
90
108
  this.data = `${timestamp}.${fingerprintString}`
91
- this.signaturePubkey = parseKey(this.signature.publickey.toString())
109
+ this.signaturePubkey = PublicKey.parse(this.signature.publickey.toString())
92
110
 
93
111
  // ensure the provided fingerprint matches the signature public key
94
- if (!this.fingerprint.matches(this.signaturePubkey)) {
112
+ if (this.fingerprint !== this.signaturePubkey.fingerprint()) {
95
113
  throw new PossessionParseError("proof of possession fingerprint does not match signature public key")
96
114
  }
97
115
 
98
116
  this.logger.debug("constructor completed",
99
117
  ...group("this",
100
118
  "timestamp", this.timestamp,
101
- "fingerprint", this.fingerprint.toString("base64"),
102
- "signaturePubkey.fingerprint()", this.signaturePubkey.fingerprint().toString("base64"),
119
+ "fingerprint", this.fingerprint,
120
+ "signaturePubkey.fingerprint()", this.signaturePubkey.fingerprint(),
103
121
  ),
104
122
  )
105
123
  } catch (err) {
106
124
  switch (true) {
107
- case (err instanceof FingerprintFormatError):
108
- throw new PossessionParseError("proof of possession fingerprint was an invalid format", err)
125
+ case (err instanceof KeyParseError):
126
+ throw new PossessionParseError(`proof of possession signature key could not be used: ${err.message}`, err)
109
127
  default:
110
128
  throw err
111
129
  }
@@ -120,26 +138,42 @@ constructor(proof: string, options?: { from?: number, logger?: Logger, skew?: nu
120
138
  return await verify(this.signature, this.data, { namespace: namespace || Namespace })
121
139
  }
122
140
 
141
+ /**
142
+ * Verifies the proof was made for a specific request, so it cannot be
143
+ * replayed against another endpoint or with other path parameters (such
144
+ * as a different certificate serial).
145
+ *
146
+ * The signed data is `${timestamp}.${fingerprint}.${METHOD} ${path}` using
147
+ * the {@link RequestNamespace} namespace.
148
+ *
149
+ * @param method The HTTP method of the request
150
+ * @param path The path of the request with any parameters filled in
151
+ * @returns true if signature is valid for the request, false otherwise
152
+ */
153
+ async verifyRequest(method: string, path: string): Promise<boolean> {
154
+ return await verify(this.signature, `${this.data}.${requestBinding(method, path)}`, { namespace: RequestNamespace })
155
+ }
156
+
123
157
  /**
124
158
  *
125
159
  * @param keys array of keys to verify finterprint against
126
160
  * @returns true if the proof of possession fingerprint matches any of the provided keys, false otherwise
127
161
  */
128
- matches(key: Key): boolean
129
- matches(...keys: Key[]): boolean
162
+ matches(key: PublicKey): boolean
163
+ matches(...keys: PublicKey[]): boolean
130
164
 
131
- matches(...keys: Key[]): boolean {
165
+ matches(...keys: PublicKey[]): boolean {
132
166
  this.logger.debug("matches() called", "in", "ProofOfPossession", "keys", keys.length)
133
167
  try {
134
168
  for (const key of keys) {
135
169
  // confirm proof of possession fingerprint matches keys
136
- this.logger.debug("this.fingerprint.matches()", "in", "ProofOfPossession", "fingerprint", this.fingerprint.toString("base64"), "key", key.toString("ssh"))
137
- if (!this.fingerprint.matches(key))
170
+ this.logger.debug("comparing fingerprint", "in", "ProofOfPossession", "fingerprint", this.fingerprint, "key", key.toString())
171
+ if (this.fingerprint !== key.fingerprint())
138
172
  return false
139
173
 
140
174
  // also confirm key used to sign proof of possession matches
141
- this.logger.debug("this.signaturePubkey.fingerprint().matches()", "in", "ProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint().toString("base64"), "key", key.toString("ssh"))
142
- if (!this.signaturePubkey.fingerprint().matches(key))
175
+ this.logger.debug("comparing signature public key", "in", "ProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint(), "key", key.toString())
176
+ if (!this.signaturePubkey.equals(key))
143
177
  return false
144
178
  }
145
179
  this.logger.debug("matches() completed", "in", "ProofOfPossession", "keys", keys.length)
@@ -163,10 +197,10 @@ export class PossessionMatchError extends Error {
163
197
  }
164
198
 
165
199
  export class RenewalProofOfPossession extends ProofOfPossession {
166
- override matches(key: Key): never
167
- override matches(...keys: Key[]): boolean
200
+ override matches(key: PublicKey): never
201
+ override matches(...keys: PublicKey[]): boolean
168
202
 
169
- override matches(...keys: Key[]): boolean {
203
+ override matches(...keys: PublicKey[]): boolean {
170
204
  this.logger.debug("matches() called", "in", "RenewalProofOfPossession", "keys", keys.length)
171
205
  if (keys.length === 1) {
172
206
  throw new PossessionMatchError("must verify both public key and certificate")
@@ -174,11 +208,11 @@ export class RenewalProofOfPossession extends ProofOfPossession {
174
208
 
175
209
  try {
176
210
  for (const key of keys) {
177
- this.logger.debug("this.fingerprint.matches()", "in", "RenewalProofOfPossession", "fingerprint", this.fingerprint.toString("base64"), "key", key.toString("ssh"))
178
- if (!this.fingerprint.matches(key))
211
+ this.logger.debug("comparing fingerprint", "in", "RenewalProofOfPossession", "fingerprint", this.fingerprint, "key", key.toString())
212
+ if (this.fingerprint !== key.fingerprint())
179
213
  return false
180
- this.logger.debug("this.signaturePubkey.fingerprint().matches()", "in", "RenewalProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint().toString("base64"), "key", key.toString("ssh"))
181
- if (!this.signaturePubkey.fingerprint().matches(key))
214
+ this.logger.debug("comparing signature public key", "in", "RenewalProofOfPossession", "signaturePubkey", this.signaturePubkey.fingerprint(), "key", key.toString())
215
+ if (!this.signaturePubkey.equals(key))
182
216
  return false
183
217
  }
184
218
  this.logger.debug("matches() completed", "in", "RenewalProofOfPossession", "keys", keys.length)
package/src/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { fromHono } from "chanfana"
2
2
  import { Hono, type Context } from "hono"
3
3
  import { createApi as apiv3 } from "./api/v3/index.js"
4
+ import { BasePath as v4BasePath, createApi as apiv4 } from "./api/v4/index.js"
4
5
  import { HTTPException } from "hono/http-exception"
5
6
  import { logger } from "./logger.js"
6
7
  import type { SshCaBindings } from "./types.js"
@@ -24,7 +25,7 @@ export const createApp = (env: SshCaBindings) => {
24
25
  schema: {
25
26
  info: {
26
27
  title: "Serverless SSH CA API",
27
- version: "3.0.0"
28
+ version: "4.0.0"
28
29
  },
29
30
  }
30
31
  })
@@ -36,6 +37,7 @@ export const createApp = (env: SshCaBindings) => {
36
37
  })
37
38
 
38
39
  app.route("/api/v3", apiv3(env))
40
+ app.route(v4BasePath, apiv4(env))
39
41
 
40
42
  return app
41
43
  }