@andrewheberle/serverless-ssh-ca 0.17.5
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/dist/api/v3/index.d.ts +2 -0
- package/dist/api/v3/index.js +278 -0
- package/dist/api/v3/schema.d.ts +218 -0
- package/dist/api/v3/schema.js +207 -0
- package/dist/certificate.d.ts +30 -0
- package/dist/certificate.js +154 -0
- package/dist/db.d.ts +36 -0
- package/dist/db.js +111 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/krl.d.ts +39 -0
- package/dist/krl.js +317 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +2 -0
- package/dist/proof.d.ts +52 -0
- package/dist/proof.js +153 -0
- package/dist/router.d.ts +7 -0
- package/dist/router.js +24 -0
- package/dist/src/api/v3/index.d.ts +1 -0
- package/dist/src/api/v3/index.js +263 -0
- package/dist/src/api/v3/schema.d.ts +217 -0
- package/dist/src/api/v3/schema.js +216 -0
- package/dist/src/certificate.d.ts +30 -0
- package/dist/src/certificate.js +154 -0
- package/dist/src/db.d.ts +36 -0
- package/dist/src/db.js +111 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +8 -0
- package/dist/src/krl.d.ts +39 -0
- package/dist/src/krl.js +317 -0
- package/dist/src/logger.d.ts +2 -0
- package/dist/src/logger.js +3 -0
- package/dist/src/proof.d.ts +51 -0
- package/dist/src/proof.js +154 -0
- package/dist/src/router.d.ts +14 -0
- package/dist/src/router.js +22 -0
- package/dist/src/sshsig/armor.d.ts +7 -0
- package/dist/src/sshsig/armor.js +18 -0
- package/dist/src/sshsig/formats.d.ts +52 -0
- package/dist/src/sshsig/formats.js +174 -0
- package/dist/src/sshsig/index.d.ts +15 -0
- package/dist/src/sshsig/index.js +31 -0
- package/dist/src/sshsig/reader.d.ts +20 -0
- package/dist/src/sshsig/reader.js +54 -0
- package/dist/src/sshsig/sig.d.ts +16 -0
- package/dist/src/sshsig/sig.js +1 -0
- package/dist/src/sshsig/sig_parser.d.ts +9 -0
- package/dist/src/sshsig/sig_parser.js +83 -0
- package/dist/src/sshsig/verifier.d.ts +2 -0
- package/dist/src/sshsig/verifier.js +37 -0
- package/dist/src/sshsig/writer.d.ts +17 -0
- package/dist/src/sshsig/writer.js +51 -0
- package/dist/src/types.d.ts +42 -0
- package/dist/src/types.js +20 -0
- package/dist/src/utils.d.ts +76 -0
- package/dist/src/utils.js +370 -0
- package/dist/src/verify.d.ts +6 -0
- package/dist/src/verify.js +17 -0
- package/dist/sshsig/armor.d.ts +7 -0
- package/dist/sshsig/armor.js +18 -0
- package/dist/sshsig/formats.d.ts +52 -0
- package/dist/sshsig/formats.js +174 -0
- package/dist/sshsig/index.d.ts +15 -0
- package/dist/sshsig/index.js +34 -0
- package/dist/sshsig/reader.d.ts +20 -0
- package/dist/sshsig/reader.js +54 -0
- package/dist/sshsig/sig.d.ts +16 -0
- package/dist/sshsig/sig.js +1 -0
- package/dist/sshsig/sig_parser.d.ts +9 -0
- package/dist/sshsig/sig_parser.js +83 -0
- package/dist/sshsig/verifier.d.ts +2 -0
- package/dist/sshsig/verifier.js +37 -0
- package/dist/sshsig/writer.d.ts +17 -0
- package/dist/sshsig/writer.js +51 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +59 -0
- package/dist/utils.js +323 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +16 -0
- package/package.json +58 -0
|
@@ -0,0 +1,370 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes armoring from text SSH signatures and decodes it to raw bytes.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} text SSH signature in armored format.
|
|
5
|
+
* @returns {Uint8Array} Raw bytes of the signature.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dearmor(text: string): Uint8Array<ArrayBuffer>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes armoring from text SSH signatures and decodes it to raw bytes.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} text SSH signature in armored format.
|
|
5
|
+
* @returns {Uint8Array} Raw bytes of the signature.
|
|
6
|
+
*/
|
|
7
|
+
export function dearmor(text) {
|
|
8
|
+
const lines = text.trim().split("\n");
|
|
9
|
+
const first = lines.shift();
|
|
10
|
+
if (first !== "-----BEGIN SSH SIGNATURE-----") {
|
|
11
|
+
throw new Error("Bad header line, expected -----BEGIN SSH SIGNATURE----- got: " + first);
|
|
12
|
+
}
|
|
13
|
+
const last = lines.pop();
|
|
14
|
+
if (last !== "-----END SSH SIGNATURE-----") {
|
|
15
|
+
throw new Error("Bad trailer line, expected -----END SSH SIGNATURE----- got: " + last);
|
|
16
|
+
}
|
|
17
|
+
return Uint8Array.from(atob(lines.join("")).split("").map((x) => x.charCodeAt(0)));
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Reader } from "./reader";
|
|
2
|
+
/**
|
|
3
|
+
* Represents an SSH public key.
|
|
4
|
+
*/
|
|
5
|
+
export type Pubkey = {
|
|
6
|
+
pk_algo: "ssh-rsa";
|
|
7
|
+
e: Uint8Array;
|
|
8
|
+
n: Uint8Array;
|
|
9
|
+
toString(): string;
|
|
10
|
+
} | {
|
|
11
|
+
pk_algo: "ssh-ed25519";
|
|
12
|
+
key: Uint8Array;
|
|
13
|
+
toString(): string;
|
|
14
|
+
} | {
|
|
15
|
+
pk_algo: "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521";
|
|
16
|
+
curve: string;
|
|
17
|
+
point: Uint8Array;
|
|
18
|
+
toString(): string;
|
|
19
|
+
} | {
|
|
20
|
+
pk_algo: "sk-ecdsa-sha2-nistp256@openssh.com";
|
|
21
|
+
curve: string;
|
|
22
|
+
point: Uint8Array;
|
|
23
|
+
application: string;
|
|
24
|
+
toString(): string;
|
|
25
|
+
} | {
|
|
26
|
+
pk_algo: "sk-ssh-ed25519@openssh.com";
|
|
27
|
+
key: Uint8Array;
|
|
28
|
+
application: string;
|
|
29
|
+
toString(): string;
|
|
30
|
+
};
|
|
31
|
+
export declare function parsePubkey(pk_algo: string, publickey: Reader, raw_publickey: ArrayBuffer): Pubkey;
|
|
32
|
+
export declare function convertPublicKey(publickey: Pubkey): {
|
|
33
|
+
format: "jwk";
|
|
34
|
+
keyData: JsonWebKey;
|
|
35
|
+
} | {
|
|
36
|
+
format: "raw";
|
|
37
|
+
keyData: ArrayBuffer;
|
|
38
|
+
};
|
|
39
|
+
export declare function convertAlgorithm(sig_algo: string): {
|
|
40
|
+
name: string;
|
|
41
|
+
hash: {
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
namedCurve?: undefined;
|
|
45
|
+
} | {
|
|
46
|
+
name: string;
|
|
47
|
+
namedCurve: string;
|
|
48
|
+
hash: {
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export declare function convertHash(hashName: string): string;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export function parsePubkey(pk_algo, publickey, raw_publickey) {
|
|
2
|
+
let pubkey;
|
|
3
|
+
if (pk_algo === "ssh-rsa") {
|
|
4
|
+
pubkey = {
|
|
5
|
+
pk_algo,
|
|
6
|
+
e: new Uint8Array(publickey.readString().bytes()),
|
|
7
|
+
n: new Uint8Array(publickey.readString().bytes()),
|
|
8
|
+
toString() {
|
|
9
|
+
return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
else if (pk_algo === "ssh-ed25519") {
|
|
14
|
+
pubkey = {
|
|
15
|
+
pk_algo,
|
|
16
|
+
key: new Uint8Array(publickey.readString().bytes()),
|
|
17
|
+
toString() {
|
|
18
|
+
return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
else if (pk_algo === "sk-ssh-ed25519@openssh.com") {
|
|
23
|
+
const key = new Uint8Array(publickey.readString().bytes());
|
|
24
|
+
const application = publickey.readString().toString();
|
|
25
|
+
pubkey = {
|
|
26
|
+
pk_algo,
|
|
27
|
+
key,
|
|
28
|
+
application,
|
|
29
|
+
toString() {
|
|
30
|
+
return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else if (pk_algo === "ecdsa-sha2-nistp256" || pk_algo === "ecdsa-sha2-nistp384" ||
|
|
35
|
+
pk_algo === "ecdsa-sha2-nistp521") {
|
|
36
|
+
const curve = publickey.readString().toString();
|
|
37
|
+
const point = new Uint8Array(publickey.readString().bytes());
|
|
38
|
+
pubkey = {
|
|
39
|
+
pk_algo,
|
|
40
|
+
curve,
|
|
41
|
+
point,
|
|
42
|
+
toString() {
|
|
43
|
+
return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else if (pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
|
|
48
|
+
const curve = publickey.readString().toString();
|
|
49
|
+
const point = new Uint8Array(publickey.readString().bytes());
|
|
50
|
+
const application = publickey.readString().toString();
|
|
51
|
+
pubkey = {
|
|
52
|
+
pk_algo,
|
|
53
|
+
curve,
|
|
54
|
+
point,
|
|
55
|
+
application,
|
|
56
|
+
toString() {
|
|
57
|
+
return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`Unsupported pk_algo: ${pk_algo}`);
|
|
63
|
+
}
|
|
64
|
+
return pubkey;
|
|
65
|
+
}
|
|
66
|
+
function base64Encode(bytes) {
|
|
67
|
+
return btoa(String.fromCharCode.apply(null, bytes));
|
|
68
|
+
}
|
|
69
|
+
function base64UrlEncode(bytes) {
|
|
70
|
+
return base64Encode(bytes)
|
|
71
|
+
.replace(/\+/g, "-")
|
|
72
|
+
.replace(/\//g, "_")
|
|
73
|
+
.replace(/=/g, "");
|
|
74
|
+
}
|
|
75
|
+
export function convertPublicKey(publickey) {
|
|
76
|
+
const pk_algo = publickey.pk_algo;
|
|
77
|
+
if (pk_algo === "ssh-rsa") {
|
|
78
|
+
return {
|
|
79
|
+
keyData: {
|
|
80
|
+
kty: "RSA",
|
|
81
|
+
e: base64UrlEncode(publickey.e),
|
|
82
|
+
n: base64UrlEncode(publickey.n),
|
|
83
|
+
},
|
|
84
|
+
format: "jwk",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
else if (pk_algo === "ssh-ed25519" || pk_algo === "sk-ssh-ed25519@openssh.com") {
|
|
88
|
+
return {
|
|
89
|
+
keyData: publickey.key.buffer.slice(publickey.key.byteOffset, publickey.key.byteOffset + publickey.key.byteLength),
|
|
90
|
+
format: "raw",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else if (pk_algo === "ecdsa-sha2-nistp256" || pk_algo === "ecdsa-sha2-nistp384" ||
|
|
94
|
+
pk_algo === "ecdsa-sha2-nistp521" ||
|
|
95
|
+
pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
|
|
96
|
+
if (publickey.point[0] !== 0x04) {
|
|
97
|
+
throw new Error("Only uncompressed (0x04) format is supported");
|
|
98
|
+
}
|
|
99
|
+
const point = publickey.point.slice(1);
|
|
100
|
+
let crv;
|
|
101
|
+
if (pk_algo === "ecdsa-sha2-nistp256" ||
|
|
102
|
+
pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
|
|
103
|
+
crv = "P-256";
|
|
104
|
+
}
|
|
105
|
+
else if (pk_algo === "ecdsa-sha2-nistp384") {
|
|
106
|
+
crv = "P-384";
|
|
107
|
+
}
|
|
108
|
+
{
|
|
109
|
+
crv = "P-521";
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
keyData: {
|
|
113
|
+
kty: "EC",
|
|
114
|
+
crv,
|
|
115
|
+
x: base64UrlEncode(point.slice(0, point.length / 2)),
|
|
116
|
+
y: base64UrlEncode(point.slice(point.length / 2)),
|
|
117
|
+
},
|
|
118
|
+
format: "jwk",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error(`Unsupported algo: ${pk_algo}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function convertAlgorithm(sig_algo) {
|
|
126
|
+
if (sig_algo === "rsa-sha2-512") {
|
|
127
|
+
return {
|
|
128
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
129
|
+
hash: { name: "SHA-512" },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else if (sig_algo === "ssh-ed25519" || sig_algo === "sk-ssh-ed25519@openssh.com") {
|
|
133
|
+
return {
|
|
134
|
+
name: "Ed25519",
|
|
135
|
+
hash: { name: "SHA-512" },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
else if (sig_algo === "ecdsa-sha2-nistp256" ||
|
|
139
|
+
sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
|
|
140
|
+
return {
|
|
141
|
+
name: "ECDSA",
|
|
142
|
+
namedCurve: "P-256",
|
|
143
|
+
hash: { name: "SHA-256" },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else if (sig_algo === "ecdsa-sha2-nistp384") {
|
|
147
|
+
return {
|
|
148
|
+
name: "ECDSA",
|
|
149
|
+
namedCurve: "P-384",
|
|
150
|
+
hash: { name: "SHA-384" },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else if (sig_algo === "ecdsa-sha2-nistp521") {
|
|
154
|
+
return {
|
|
155
|
+
name: "ECDSA",
|
|
156
|
+
namedCurve: "P-521",
|
|
157
|
+
hash: { name: "SHA-512" },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
throw new Error(`Unsupported algo: ${sig_algo}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function convertHash(hashName) {
|
|
165
|
+
if (hashName === "sha256") {
|
|
166
|
+
return "SHA-256";
|
|
167
|
+
}
|
|
168
|
+
else if (hashName === "sha512") {
|
|
169
|
+
return "SHA-512";
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Error(`Unknown hash: ${hashName}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Sig } from "./sig";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies SSH signature against provided data.
|
|
4
|
+
*
|
|
5
|
+
* Returns `true` if the `signature` is a valid signature over `signed_data`, `false` otherwise.
|
|
6
|
+
*
|
|
7
|
+
* @param {Sig | string} signature SSH signature.
|
|
8
|
+
* @param {Uint8Array | string} signed_data Data that has been signed.
|
|
9
|
+
* @param {object} options Pass-in subtle crypto if required or a specific namespace.
|
|
10
|
+
* @returns {Promise<boolean>} Resolves to true if the signature is valid, to false otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export declare function verify(signature: Sig | string, signed_data: Uint8Array | string, options?: {
|
|
13
|
+
subtle?: SubtleCrypto;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
}): Promise<boolean>;
|