@andrewheberle/serverless-ssh-ca 0.18.0 → 0.18.2

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 (77) hide show
  1. package/dist/api/v3/index.d.ts +1 -1
  2. package/dist/api/v3/index.d.ts.map +1 -1
  3. package/dist/api/v3/index.js +9 -7
  4. package/dist/api/v3/index.js.map +1 -1
  5. package/dist/api/v3/schema.d.ts +13 -13
  6. package/dist/api/v3/schema.d.ts.map +1 -1
  7. package/dist/api/v3/schema.js +2 -2
  8. package/dist/api/v3/schema.js.map +1 -1
  9. package/dist/certificate.d.ts +2 -2
  10. package/dist/certificate.d.ts.map +1 -1
  11. package/dist/certificate.js +4 -2
  12. package/dist/certificate.js.map +1 -1
  13. package/dist/db/index.d.ts +2 -2
  14. package/dist/db/index.d.ts.map +1 -1
  15. package/dist/db/index.js +2 -2
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/db/migrations/index.js +1 -1
  18. package/dist/db/migrations/index.js.map +1 -1
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/krl.d.ts.map +1 -1
  24. package/dist/krl.js +46 -16
  25. package/dist/krl.js.map +1 -1
  26. package/dist/logger.d.ts +1 -1
  27. package/dist/logger.d.ts.map +1 -1
  28. package/dist/proof.d.ts +2 -2
  29. package/dist/proof.d.ts.map +1 -1
  30. package/dist/proof.js +5 -3
  31. package/dist/proof.js.map +1 -1
  32. package/dist/router.d.ts +1 -1
  33. package/dist/router.d.ts.map +1 -1
  34. package/dist/router.js +2 -2
  35. package/dist/router.js.map +1 -1
  36. package/dist/sshsig/formats.d.ts +1 -1
  37. package/dist/sshsig/formats.d.ts.map +1 -1
  38. package/dist/sshsig/index.d.ts +1 -1
  39. package/dist/sshsig/index.d.ts.map +1 -1
  40. package/dist/sshsig/index.js +2 -2
  41. package/dist/sshsig/index.js.map +1 -1
  42. package/dist/sshsig/sig.d.ts +1 -1
  43. package/dist/sshsig/sig.d.ts.map +1 -1
  44. package/dist/sshsig/sig_parser.d.ts +1 -1
  45. package/dist/sshsig/sig_parser.d.ts.map +1 -1
  46. package/dist/sshsig/sig_parser.js +3 -3
  47. package/dist/sshsig/sig_parser.js.map +1 -1
  48. package/dist/sshsig/verifier.d.ts +1 -1
  49. package/dist/sshsig/verifier.d.ts.map +1 -1
  50. package/dist/sshsig/verifier.js +2 -2
  51. package/dist/sshsig/verifier.js.map +1 -1
  52. package/dist/utils.d.ts +5 -5
  53. package/dist/utils.d.ts.map +1 -1
  54. package/dist/utils.js +6 -4
  55. package/dist/utils.js.map +1 -1
  56. package/dist/verify.d.ts +2 -2
  57. package/dist/verify.d.ts.map +1 -1
  58. package/dist/verify.js +1 -1
  59. package/dist/verify.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/api/v3/index.ts +13 -12
  62. package/src/api/v3/schema.ts +3 -3
  63. package/src/certificate.ts +8 -12
  64. package/src/db/index.ts +4 -4
  65. package/src/db/migrations/index.ts +1 -1
  66. package/src/index.ts +4 -4
  67. package/src/krl.ts +50 -19
  68. package/src/logger.ts +1 -1
  69. package/src/proof.ts +8 -4
  70. package/src/router.ts +3 -3
  71. package/src/sshsig/formats.ts +1 -1
  72. package/src/sshsig/index.ts +3 -3
  73. package/src/sshsig/sig.ts +1 -1
  74. package/src/sshsig/sig_parser.ts +4 -4
  75. package/src/sshsig/verifier.ts +3 -3
  76. package/src/utils.ts +11 -7
  77. package/src/verify.ts +3 -3
package/src/krl.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { PrivateKey } from "sshpk"
1
+ import type { AlgorithmPart, PrivateKey } from "sshpk"
2
+ import { toFixedWidth } from "./sshsig/sig_parser.js"
2
3
 
3
4
  // ── Constants ────────────────────────────────────────────────────────────────
4
5
 
@@ -94,20 +95,25 @@ const HASH_ALGORITHM = "sha512"
94
95
  // ── Helpers ──────────────────────────────────────────────────────────────────
95
96
 
96
97
  /**
97
- * PEM string → raw DER bytes (strips header/footer and decodes base64).
98
- * Works for both PKCS8 ("-----BEGIN PRIVATE KEY-----") and any other PEM type.
98
+ * Get the raw bytes of a named part of an sshpk key.
99
99
  */
100
- function pemToDer(pem: string): Uint8Array<ArrayBuffer> {
101
- const lines = pem
102
- .trim()
103
- .split("\n")
104
- .filter((l) => !l.startsWith("-----"))
105
- const binary = atob(lines.join(""))
106
- const bytes = new Uint8Array(binary.length)
107
- for (let i = 0; i < binary.length; i++) {
108
- bytes[i] = binary.charCodeAt(i)
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`)
109
104
  }
110
- return bytes
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(/=+$/, "")
111
117
  }
112
118
 
113
119
  /**
@@ -119,15 +125,21 @@ async function importPrivateKey(caKey: PrivateKey): Promise<{
119
125
  sigAlgo: string // SSH signature algorithm name
120
126
  webCryptoAlgo: SubtleCryptoSignAlgorithm
121
127
  }> {
122
- const pkcs8Pem = caKey.toString("pkcs8")
123
- const der = pemToDer(pkcs8Pem)
124
128
  const type = caKey.type // "ed25519" | "ecdsa" | etc.
125
129
  const curve = (caKey as unknown as { curve?: string }).curve
126
130
 
127
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
+ }
128
140
  const cryptoKey = await crypto.subtle.importKey(
129
- "pkcs8",
130
- der,
141
+ "jwk",
142
+ { kty: "OKP", crv: "Ed25519", d: base64url(seed), x: base64url(publicKey) },
131
143
  { name: "Ed25519" },
132
144
  false,
133
145
  ["sign"],
@@ -141,26 +153,45 @@ async function importPrivateKey(caKey: PrivateKey): Promise<{
141
153
  let namedCurve: string
142
154
  let sigAlgo: string
143
155
  let hash: string
156
+ let size: number // bytes in each coordinate and the private key
144
157
 
145
158
  if (curve === "nistp256") {
146
159
  namedCurve = "P-256"
147
160
  sigAlgo = "ecdsa-sha2-nistp256"
148
161
  hash = "SHA-256"
162
+ size = 32
149
163
  } else if (curve === "nistp384") {
150
164
  namedCurve = "P-384"
151
165
  sigAlgo = "ecdsa-sha2-nistp384"
152
166
  hash = "SHA-384"
167
+ size = 48
153
168
  } else if (curve === "nistp521") {
154
169
  namedCurve = "P-521"
155
170
  sigAlgo = "ecdsa-sha2-nistp521"
156
171
  hash = "SHA-512"
172
+ size = 66
157
173
  } else {
158
174
  throw new Error(`Unsupported ECDSA curve: ${curve}`)
159
175
  }
160
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)
161
186
  const cryptoKey = await crypto.subtle.importKey(
162
- "pkcs8",
163
- der,
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
+ },
164
195
  { name: "ECDSA", namedCurve },
165
196
  false,
166
197
  ["sign"],
package/src/logger.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SshCaBindings } from "./types"
1
+ import type { SshCaBindings } from "./types.js"
2
2
  import { Logger, LogLevel } from "@andrewheberle/ts-slog"
3
3
 
4
4
  export const logger = (env: SshCaBindings): Logger => {
package/src/proof.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { ms } from "itty-time"
2
- import { Fingerprint, FingerprintFormatError, Key, parseFingerprint, parseKey } from "sshpk"
3
- import { verify } from "./sshsig"
4
- import { parse } from "./sshsig/sig_parser"
5
- import { Sig } from "./sshsig/sig"
2
+ import sshpk from "sshpk"
3
+ import type { Fingerprint, Key } from "sshpk"
4
+ import { verify } from "./sshsig/index.js"
5
+ import { parse } from "./sshsig/sig_parser.js"
6
+ import { Sig } from "./sshsig/sig.js"
6
7
  import { group, Logger, LogLevel } from "@andrewheberle/ts-slog"
7
8
 
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
11
+
8
12
  export const Namespace = "proof-of-possession@com.github.serverless-ssh-ca.andrewheberle"
9
13
 
10
14
  export class PossessionParseError extends Error {
package/src/router.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { fromHono } from "chanfana"
2
2
  import { Hono, type Context } from "hono"
3
- import { createApi as apiv3 } from "./api/v3"
3
+ import { createApi as apiv3 } from "./api/v3/index.js"
4
4
  import { HTTPException } from "hono/http-exception"
5
- import { logger } from "./logger"
6
- import type { SshCaBindings } from "./types"
5
+ import { logger } from "./logger.js"
6
+ import type { SshCaBindings } from "./types.js"
7
7
 
8
8
  export type CFArgs = [SshCaBindings, ExecutionContext]
9
9
  export type AppContext = Context<{ Bindings: SshCaBindings }>
@@ -1,4 +1,4 @@
1
- import { Reader } from "./reader";
1
+ import { Reader } from "./reader.js";
2
2
 
3
3
  /**
4
4
  * Represents an SSH public key.
@@ -1,6 +1,6 @@
1
- import { Sig } from "./sig";
2
- import { verify as rawVerify } from "./verifier";
3
- import { parse } from "./sig_parser";
1
+ import { Sig } from "./sig.js";
2
+ import { verify as rawVerify } from "./verifier.js";
3
+ import { parse } from "./sig_parser.js";
4
4
 
5
5
  /**
6
6
  * Verifies SSH signature against provided data.
package/src/sshsig/sig.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Pubkey } from "./formats";
1
+ import { Pubkey } from "./formats.js";
2
2
 
3
3
  /**
4
4
  * Represents a parsed SSH signature.
@@ -1,7 +1,7 @@
1
- import { Sig } from "./sig";
2
- import { Reader } from "./reader";
3
- import { parsePubkey } from "./formats";
4
- import { dearmor } from "./armor";
1
+ import { Sig } from "./sig.js";
2
+ import { Reader } from "./reader.js";
3
+ import { parsePubkey } from "./formats.js";
4
+ import { dearmor } from "./armor.js";
5
5
 
6
6
  /**
7
7
  * ECDSA signature algorithms whose signatures need converting from SSH
@@ -1,6 +1,6 @@
1
- import { Sig } from "./sig";
2
- import { convertAlgorithm, convertHash, convertPublicKey } from "./formats";
3
- import { Writer } from "./writer";
1
+ import { Sig } from "./sig.js";
2
+ import { convertAlgorithm, convertHash, convertPublicKey } from "./formats.js";
3
+ import { Writer } from "./writer.js";
4
4
 
5
5
  export async function verify(
6
6
  subtle: SubtleCrypto,
package/src/utils.ts CHANGED
@@ -1,15 +1,19 @@
1
- import type { SshCaBindings } from "./types"
1
+ import type { SshCaBindings } from "./types.js"
2
2
  import { JWKInvalid, JWKSInvalid, JWSInvalid, JWSSignatureVerificationFailed, JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "jose/errors"
3
- import { Certificate, Key, KeyParseError, CertificateParseError, parseCertificate, parseKey, parsePrivateKey, PrivateKey } from "sshpk"
3
+ import sshpk from "sshpk"
4
+ import type { Certificate, Key, PrivateKey } from "sshpk"
4
5
  import z from "zod"
5
- import { verifyJWT } from "./verify"
6
- import { CertificateRequestJWTPayload } from "./types"
7
- import { RenewalProofOfPossession, ProofOfPossession, PossessionParseError } from "./proof"
8
- import type { isRevoked as IsRevokedFn } from "./db"
9
- import { logger } from "./logger"
6
+ import { verifyJWT } from "./verify.js"
7
+ import { CertificateRequestJWTPayload } from "./types.js"
8
+ import { RenewalProofOfPossession, ProofOfPossession, PossessionParseError } from "./proof.js"
9
+ import type { isRevoked as IsRevokedFn } from "./db/index.js"
10
+ import { logger } from "./logger.js"
10
11
  import { ms } from "itty-time"
11
12
  import { InternalServerErrorException } from "chanfana"
12
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
16
+
13
17
  export const fatalIssue = (ctx: z.RefinementCtx, message: string, val: unknown) => {
14
18
  ctx.issues.push({
15
19
  code: "custom",
package/src/verify.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRemoteJWKSet, jwtVerify } from "jose"
2
- import { CertificateRequestJWTPayload } from "./types"
3
- import type { SshCaBindings } from "./types"
4
- import { split } from "./utils"
2
+ import { CertificateRequestJWTPayload } from "./types.js"
3
+ import type { SshCaBindings } from "./types.js"
4
+ import { split } from "./utils.js"
5
5
 
6
6
  type VerifyOptions = {
7
7
  aud?: string | string[]