@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.
- package/LICENSE +201 -0
- package/README.md +268 -0
- package/dist/api/v3/schema.js +2 -1
- package/dist/proof.js +1 -1
- package/dist/sshsig/formats.js +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +1 -2
- package/package.json +9 -13
- package/dist/src/api/v3/index.d.ts +0 -1
- package/dist/src/api/v3/index.js +0 -263
- package/dist/src/api/v3/schema.d.ts +0 -217
- package/dist/src/api/v3/schema.js +0 -216
- package/dist/src/certificate.d.ts +0 -30
- package/dist/src/certificate.js +0 -154
- package/dist/src/db.d.ts +0 -36
- package/dist/src/db.js +0 -111
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.js +0 -8
- package/dist/src/krl.d.ts +0 -39
- package/dist/src/krl.js +0 -317
- package/dist/src/logger.d.ts +0 -2
- package/dist/src/logger.js +0 -3
- package/dist/src/proof.d.ts +0 -51
- package/dist/src/proof.js +0 -154
- package/dist/src/router.d.ts +0 -14
- package/dist/src/router.js +0 -22
- package/dist/src/sshsig/armor.d.ts +0 -7
- package/dist/src/sshsig/armor.js +0 -18
- package/dist/src/sshsig/formats.d.ts +0 -52
- package/dist/src/sshsig/formats.js +0 -174
- package/dist/src/sshsig/index.d.ts +0 -15
- package/dist/src/sshsig/index.js +0 -31
- package/dist/src/sshsig/reader.d.ts +0 -20
- package/dist/src/sshsig/reader.js +0 -54
- package/dist/src/sshsig/sig.d.ts +0 -16
- package/dist/src/sshsig/sig.js +0 -1
- package/dist/src/sshsig/sig_parser.d.ts +0 -9
- package/dist/src/sshsig/sig_parser.js +0 -83
- package/dist/src/sshsig/verifier.d.ts +0 -2
- package/dist/src/sshsig/verifier.js +0 -37
- package/dist/src/sshsig/writer.d.ts +0 -17
- package/dist/src/sshsig/writer.js +0 -51
- package/dist/src/types.d.ts +0 -42
- package/dist/src/types.js +0 -20
- package/dist/src/utils.d.ts +0 -76
- package/dist/src/utils.js +0 -370
- package/dist/src/verify.d.ts +0 -6
- 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
|
-
};
|
package/dist/src/verify.d.ts
DELETED
|
@@ -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 {};
|
package/dist/src/verify.js
DELETED
|
@@ -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
|
-
};
|