@andrewheberle/serverless-ssh-ca 0.17.5 → 0.17.7

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 (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +268 -0
  3. package/dist/api/v3/schema.js +2 -1
  4. package/dist/proof.js +1 -1
  5. package/dist/sshsig/formats.js +1 -1
  6. package/dist/utils.d.ts +2 -1
  7. package/dist/utils.js +1 -2
  8. package/package.json +9 -13
  9. package/dist/src/api/v3/index.d.ts +0 -1
  10. package/dist/src/api/v3/index.js +0 -263
  11. package/dist/src/api/v3/schema.d.ts +0 -217
  12. package/dist/src/api/v3/schema.js +0 -216
  13. package/dist/src/certificate.d.ts +0 -30
  14. package/dist/src/certificate.js +0 -154
  15. package/dist/src/db.d.ts +0 -36
  16. package/dist/src/db.js +0 -111
  17. package/dist/src/index.d.ts +0 -6
  18. package/dist/src/index.js +0 -8
  19. package/dist/src/krl.d.ts +0 -39
  20. package/dist/src/krl.js +0 -317
  21. package/dist/src/logger.d.ts +0 -2
  22. package/dist/src/logger.js +0 -3
  23. package/dist/src/proof.d.ts +0 -51
  24. package/dist/src/proof.js +0 -154
  25. package/dist/src/router.d.ts +0 -14
  26. package/dist/src/router.js +0 -22
  27. package/dist/src/sshsig/armor.d.ts +0 -7
  28. package/dist/src/sshsig/armor.js +0 -18
  29. package/dist/src/sshsig/formats.d.ts +0 -52
  30. package/dist/src/sshsig/formats.js +0 -174
  31. package/dist/src/sshsig/index.d.ts +0 -15
  32. package/dist/src/sshsig/index.js +0 -31
  33. package/dist/src/sshsig/reader.d.ts +0 -20
  34. package/dist/src/sshsig/reader.js +0 -54
  35. package/dist/src/sshsig/sig.d.ts +0 -16
  36. package/dist/src/sshsig/sig.js +0 -1
  37. package/dist/src/sshsig/sig_parser.d.ts +0 -9
  38. package/dist/src/sshsig/sig_parser.js +0 -83
  39. package/dist/src/sshsig/verifier.d.ts +0 -2
  40. package/dist/src/sshsig/verifier.js +0 -37
  41. package/dist/src/sshsig/writer.d.ts +0 -17
  42. package/dist/src/sshsig/writer.js +0 -51
  43. package/dist/src/types.d.ts +0 -42
  44. package/dist/src/types.js +0 -20
  45. package/dist/src/utils.d.ts +0 -76
  46. package/dist/src/utils.js +0 -370
  47. package/dist/src/verify.d.ts +0 -6
  48. package/dist/src/verify.js +0 -17
package/dist/src/utils.js DELETED
@@ -1,370 +0,0 @@
1
- import { env } from "cloudflare:workers";
2
- import { JWKInvalid, JWKSInvalid, JWSInvalid, JWTClaimValidationFailed, JWTInvalid } from "jose/errors";
3
- import { KeyParseError, CertificateParseError, parseCertificate, parseKey, parsePrivateKey } from "sshpk";
4
- import z from "zod";
5
- import { verifyJWT } from "./verify";
6
- import { RenewalProofOfPossession, ProofOfPossession, PossessionParseError } from "./proof";
7
- import { isRevoked } from "./db";
8
- import { logger } from "./logger";
9
- export const fatalIssue = (ctx, message, val) => {
10
- ctx.issues.push({
11
- code: "custom",
12
- message: message,
13
- input: val
14
- });
15
- return z.NEVER;
16
- };
17
- export const transformProofOfPossession = (val, ctx) => {
18
- try {
19
- const proof = new ProofOfPossession(val, { logger: logger });
20
- return proof;
21
- }
22
- catch (err) {
23
- logger.error("proof of possession parsing error", "error", err);
24
- switch (true) {
25
- case (err instanceof PossessionParseError):
26
- ctx.issues.push({
27
- code: "custom",
28
- message: err.message,
29
- input: val
30
- });
31
- break;
32
- default:
33
- ctx.issues.push({
34
- code: "custom",
35
- message: "proof of possession transform unhandled error",
36
- input: val
37
- });
38
- }
39
- return z.NEVER;
40
- }
41
- };
42
- export const transformAuthorizationHeader = async (val, ctx) => {
43
- const jwt = val.replace("Bearer ", "");
44
- if (jwt === "") {
45
- return fatalIssue(ctx, "request did not contain a JWT", val);
46
- }
47
- try {
48
- // verify jwt
49
- const { payload } = await verifyJWT(jwt);
50
- if (payload.email === undefined) {
51
- return fatalIssue(ctx, "JWT was verified but was missing required email claim", val);
52
- }
53
- return {
54
- email: payload.email,
55
- sub: payload.sub,
56
- };
57
- }
58
- catch (err) {
59
- switch (true) {
60
- case (err instanceof JWSInvalid):
61
- return fatalIssue(ctx, "the access token was invalid", val);
62
- case (err instanceof JWTInvalid):
63
- return fatalIssue(ctx, "the access token failed verification", val);
64
- case (err instanceof JWKInvalid):
65
- return fatalIssue(ctx, "the access token JWK was invalid", val);
66
- case (err instanceof JWKSInvalid):
67
- return fatalIssue(ctx, "the access token JWKS was invalid", val);
68
- case (err instanceof JWTClaimValidationFailed):
69
- return fatalIssue(ctx, "claim validtion of the JWT failed", val);
70
- default:
71
- logger.error("unhandled access token validation error", "in", "transformAuthorizationHeader", "error", err);
72
- return fatalIssue(ctx, "unhandled access token validation error", val);
73
- }
74
- }
75
- };
76
- export const transformPublicKey = (val, ctx) => {
77
- try {
78
- const key = typeof val === "string" ? parseKey(Buffer.from(val, "base64")) : parseKey(val);
79
- return key;
80
- }
81
- catch (err) {
82
- switch (true) {
83
- case (err instanceof TypeError):
84
- ctx.issues.push({
85
- code: "custom",
86
- message: err.message,
87
- input: val
88
- });
89
- break;
90
- case (err instanceof KeyParseError):
91
- ctx.issues.push({
92
- code: "custom",
93
- message: err.message,
94
- input: val
95
- });
96
- break;
97
- default:
98
- ctx.issues.push({
99
- code: "custom",
100
- message: "unhandled error parsing public_key",
101
- input: val
102
- });
103
- }
104
- return z.NEVER;
105
- }
106
- };
107
- export const identityPrincipals = (payload, claim) => {
108
- if (claim === undefined) {
109
- claim = env.JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM;
110
- }
111
- const p = payload[claim];
112
- if (p === undefined) {
113
- logger.warn("claim was missing despite being set in CA config", "claim", claim);
114
- return [];
115
- }
116
- if (p === "") {
117
- logger.warn("claim was present but was an empty string", "claim", claim);
118
- return [];
119
- }
120
- // make sure its always a string[]
121
- const principals = typeof p === "string" ? [p] : p;
122
- // replace any spaces with underscores
123
- principals.forEach((value, index, array) => {
124
- array[index] = value.replaceAll(" ", "_");
125
- });
126
- logger.info("identity token included principals", "principals", principals);
127
- return principals;
128
- };
129
- export const parseIdentity = async (jwt, claim) => {
130
- if (jwt === undefined) {
131
- throw new Error("missing identity token");
132
- }
133
- const aud = env.JWT_AUD === undefined || env.JWT_AUD === "" ? undefined : split(env.JWT_AUD);
134
- const { payload } = await verifyJWT(jwt, { aud: aud });
135
- const principals = identityPrincipals(payload, claim);
136
- return {
137
- sub: payload.sub,
138
- principals: principals,
139
- };
140
- };
141
- export const transformIdentityToken = async (val, ctx) => {
142
- try {
143
- const identity = await parseIdentity(val, env.JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM);
144
- return identity;
145
- }
146
- catch (err) {
147
- ctx.issues.push({
148
- code: "custom",
149
- message: "problem parsing identity token",
150
- input: val
151
- });
152
- return z.NEVER;
153
- }
154
- };
155
- /**
156
- *
157
- * @param val A SSH certificate either a base64 encoded "string" or a "Buffer\<ArrayBufferLike\>"
158
- * @param ctx The Zod context for any errors
159
- * @returns
160
- */
161
- export const transformCertificate = (val, ctx) => {
162
- try {
163
- const cert = typeof val === "string" ? parseCertificate(Buffer.from(val, "base64"), "openssh") : parseCertificate(val, "openssh");
164
- return cert;
165
- }
166
- catch (err) {
167
- switch (true) {
168
- case (err instanceof TypeError):
169
- ctx.issues.push({
170
- code: "custom",
171
- message: err.message,
172
- input: val
173
- });
174
- break;
175
- case (err instanceof CertificateParseError):
176
- ctx.issues.push({
177
- code: "custom",
178
- message: err.message,
179
- input: val
180
- });
181
- break;
182
- default:
183
- ctx.issues.push({
184
- code: "custom",
185
- message: "unhandled error parsing certificate",
186
- input: val
187
- });
188
- }
189
- return z.NEVER;
190
- }
191
- };
192
- export const refineCertificateRequest = async (val, ctx) => {
193
- try {
194
- // check proof of possession fingerprint matches public key
195
- if (!val.proof.matches(val.public_key)) {
196
- return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
197
- }
198
- // verify proof of possession signature
199
- const verified = await val.proof.verify();
200
- if (!verified) {
201
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
202
- }
203
- return z.NEVER;
204
- }
205
- catch (err) {
206
- logger.error("proof of possession verification unhandled error", "in", "refineCertificateRequest", "error", err);
207
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
208
- }
209
- };
210
- export const refineLegacyCertificateRequest = async (val, ctx) => {
211
- try {
212
- // check proof of possession fingerprint matches public key
213
- if (!val.nonce.matches(val.public_key)) {
214
- return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
215
- }
216
- // verify proof of possession signature
217
- const verified = await val.nonce.verify("file");
218
- if (!verified) {
219
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
220
- }
221
- return z.NEVER;
222
- }
223
- catch (err) {
224
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
225
- }
226
- };
227
- export const transformRenewalProofOfPossession = (val, ctx) => {
228
- try {
229
- return new RenewalProofOfPossession(val, { logger: logger });
230
- }
231
- catch (err) {
232
- if (err instanceof PossessionParseError) {
233
- return fatalIssue(ctx, err.message, val);
234
- }
235
- logger.error("proof of possession verification unhandled error", "in", "transformRenewalProofOfPossession", "error", err);
236
- return fatalIssue(ctx, "proof of possession transform unhandled error", val);
237
- }
238
- };
239
- export const refineHostCertificateRequest = async (val, ctx) => {
240
- const l = logger.with("in", "refineHostCertificateRequest");
241
- l.debug("started");
242
- try {
243
- // check proof of possession fingerprint matches public key
244
- l.debug("checking val.proof.matches()", "public_key", val.public_key.toString("ssh"));
245
- if (!val.proof.matches(val.public_key)) {
246
- l.warn("proof of possession fingerprint did not match public_key");
247
- return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
248
- }
249
- // verify proof of possession signature
250
- l.debug("await val.proof.verify()");
251
- const verified = await val.proof.verify();
252
- if (!verified) {
253
- l.warn("proof of possession signature validation failed");
254
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
255
- }
256
- l.debug("done");
257
- return z.NEVER;
258
- }
259
- catch (err) {
260
- l.error("proof of possession verification unhandled error", "error", err);
261
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
262
- }
263
- };
264
- export const refineLegacyHostCertificateRequest = async (val, ctx) => {
265
- try {
266
- // check proof of possession fingerprint matches public key
267
- if (!val.nonce.matches(val.public_key)) {
268
- return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
269
- }
270
- // verify proof of possession signature
271
- const verified = await val.nonce.verify("file");
272
- if (!verified) {
273
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
274
- }
275
- return z.NEVER;
276
- }
277
- catch (err) {
278
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
279
- }
280
- };
281
- export const refineHostCertificateRenewal = async (val, ctx) => {
282
- try {
283
- // ensure certificate is signed by CA
284
- const issuer = await getPublic();
285
- if (!val.certificate.isSignedByKey(issuer)) {
286
- return fatalIssue(ctx, "the provided certificate was not signed by this CA", val);
287
- }
288
- // check certificate is not expired
289
- if (val.certificate.isExpired()) {
290
- return fatalIssue(ctx, "the provided certificate is expired", val);
291
- }
292
- // ensure certificate presented for renewal is not revoked
293
- const serial = val.certificate.serial.readBigUInt64BE(0);
294
- if (await isRevoked(serial)) {
295
- return fatalIssue(ctx, "the provided certificate is revoked", val);
296
- }
297
- // check proof of possession fingerprint matches public key and certificate subject key
298
- if (!val.proof.matches(val.public_key, val.certificate.subjectKey)) {
299
- return fatalIssue(ctx, "proof of possession fingerprints did not match public_key and/or certificate", val);
300
- }
301
- // verify proof of possession signature
302
- const verified = await val.proof.verify();
303
- if (!verified) {
304
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
305
- }
306
- return z.NEVER;
307
- }
308
- catch (err) {
309
- logger.error("proof of possession verification unhandled error", "in", "refineHostCertificateRenewal", "error", err);
310
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
311
- }
312
- };
313
- export const refineRevokeCertificate = async (val, ctx) => {
314
- try {
315
- // check proof of possession fingerprint matches public key
316
- if (!val.proof.matches(val.public_key)) {
317
- return fatalIssue(ctx, "proof of possession fingerprint did not match public_key", val);
318
- }
319
- // verify proof of possession signature
320
- const verified = await val.proof.verify();
321
- if (!verified) {
322
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
323
- }
324
- return z.NEVER;
325
- }
326
- catch (err) {
327
- logger.error("proof of possession verification unhandled error", "in", "refineRevokeCertificate", "error", err);
328
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
329
- }
330
- };
331
- export const refineLegacyHostCertificateRenewal = async (val, ctx) => {
332
- try {
333
- // check certificate is not expired
334
- if (val.certificate.isExpired()) {
335
- return fatalIssue(ctx, "certificate is expired", val);
336
- }
337
- // check proof of possession fingerprint matches public key and certificate subject key
338
- if (!val.nonce.matches(val.public_key, val.certificate.subjectKey)) {
339
- return fatalIssue(ctx, "proof of possession fingerprints did not match public_key and/or certificate", val);
340
- }
341
- // verify proof of possession signature
342
- const verified = await val.nonce.verify("file");
343
- if (!verified) {
344
- return fatalIssue(ctx, "proof of possession signature validation failed", val);
345
- }
346
- return z.NEVER;
347
- }
348
- catch (err) {
349
- return fatalIssue(ctx, "proof of possession verification unhandled error", val);
350
- }
351
- };
352
- export const split = (v) => {
353
- if (v === "" || v === undefined) {
354
- return [];
355
- }
356
- return v.split(",");
357
- };
358
- export const getPublic = async (key) => {
359
- if (key === undefined) {
360
- // grab private key from secret store
361
- const secret = await env.PRIVATE_KEY.get();
362
- // parse it
363
- key = parsePrivateKey(secret);
364
- }
365
- // convert to a public key and add comment
366
- const pub = key.toPublic();
367
- pub.comment = env.ISSUER_DN;
368
- // return in ssh format trimmed of any whitespace
369
- return pub;
370
- };
@@ -1,6 +0,0 @@
1
- import { CertificateRequestJWTPayload } from "./types";
2
- type VerifyOptions = {
3
- aud?: string | string[];
4
- };
5
- export declare const verifyJWT: (jwt: string, opts?: VerifyOptions) => Promise<import("jose").JWTVerifyResult<CertificateRequestJWTPayload> & import("jose").ResolvedKey>;
6
- export {};
@@ -1,17 +0,0 @@
1
- import { createRemoteJWKSet, jwtVerify } from "jose";
2
- import { env } from "cloudflare:workers";
3
- import { split } from "./utils";
4
- export const verifyJWT = async (jwt, opts = {}) => {
5
- const JWKS = createRemoteJWKSet(new URL(env.JWT_JWKS_URL));
6
- if (opts.aud !== undefined) {
7
- return await jwtVerify(jwt, JWKS, {
8
- issuer: env.JWT_ISSUER,
9
- algorithms: split(env.JWT_ALGORITHMS),
10
- audience: opts.aud
11
- });
12
- }
13
- return await jwtVerify(jwt, JWKS, {
14
- issuer: env.JWT_ISSUER,
15
- algorithms: split(env.JWT_ALGORITHMS),
16
- });
17
- };