@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.
Files changed (81) hide show
  1. package/dist/api/v3/index.d.ts +2 -0
  2. package/dist/api/v3/index.js +278 -0
  3. package/dist/api/v3/schema.d.ts +218 -0
  4. package/dist/api/v3/schema.js +207 -0
  5. package/dist/certificate.d.ts +30 -0
  6. package/dist/certificate.js +154 -0
  7. package/dist/db.d.ts +36 -0
  8. package/dist/db.js +111 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +14 -0
  11. package/dist/krl.d.ts +39 -0
  12. package/dist/krl.js +317 -0
  13. package/dist/logger.d.ts +3 -0
  14. package/dist/logger.js +2 -0
  15. package/dist/proof.d.ts +52 -0
  16. package/dist/proof.js +153 -0
  17. package/dist/router.d.ts +7 -0
  18. package/dist/router.js +24 -0
  19. package/dist/src/api/v3/index.d.ts +1 -0
  20. package/dist/src/api/v3/index.js +263 -0
  21. package/dist/src/api/v3/schema.d.ts +217 -0
  22. package/dist/src/api/v3/schema.js +216 -0
  23. package/dist/src/certificate.d.ts +30 -0
  24. package/dist/src/certificate.js +154 -0
  25. package/dist/src/db.d.ts +36 -0
  26. package/dist/src/db.js +111 -0
  27. package/dist/src/index.d.ts +6 -0
  28. package/dist/src/index.js +8 -0
  29. package/dist/src/krl.d.ts +39 -0
  30. package/dist/src/krl.js +317 -0
  31. package/dist/src/logger.d.ts +2 -0
  32. package/dist/src/logger.js +3 -0
  33. package/dist/src/proof.d.ts +51 -0
  34. package/dist/src/proof.js +154 -0
  35. package/dist/src/router.d.ts +14 -0
  36. package/dist/src/router.js +22 -0
  37. package/dist/src/sshsig/armor.d.ts +7 -0
  38. package/dist/src/sshsig/armor.js +18 -0
  39. package/dist/src/sshsig/formats.d.ts +52 -0
  40. package/dist/src/sshsig/formats.js +174 -0
  41. package/dist/src/sshsig/index.d.ts +15 -0
  42. package/dist/src/sshsig/index.js +31 -0
  43. package/dist/src/sshsig/reader.d.ts +20 -0
  44. package/dist/src/sshsig/reader.js +54 -0
  45. package/dist/src/sshsig/sig.d.ts +16 -0
  46. package/dist/src/sshsig/sig.js +1 -0
  47. package/dist/src/sshsig/sig_parser.d.ts +9 -0
  48. package/dist/src/sshsig/sig_parser.js +83 -0
  49. package/dist/src/sshsig/verifier.d.ts +2 -0
  50. package/dist/src/sshsig/verifier.js +37 -0
  51. package/dist/src/sshsig/writer.d.ts +17 -0
  52. package/dist/src/sshsig/writer.js +51 -0
  53. package/dist/src/types.d.ts +42 -0
  54. package/dist/src/types.js +20 -0
  55. package/dist/src/utils.d.ts +76 -0
  56. package/dist/src/utils.js +370 -0
  57. package/dist/src/verify.d.ts +6 -0
  58. package/dist/src/verify.js +17 -0
  59. package/dist/sshsig/armor.d.ts +7 -0
  60. package/dist/sshsig/armor.js +18 -0
  61. package/dist/sshsig/formats.d.ts +52 -0
  62. package/dist/sshsig/formats.js +174 -0
  63. package/dist/sshsig/index.d.ts +15 -0
  64. package/dist/sshsig/index.js +34 -0
  65. package/dist/sshsig/reader.d.ts +20 -0
  66. package/dist/sshsig/reader.js +54 -0
  67. package/dist/sshsig/sig.d.ts +16 -0
  68. package/dist/sshsig/sig.js +1 -0
  69. package/dist/sshsig/sig_parser.d.ts +9 -0
  70. package/dist/sshsig/sig_parser.js +83 -0
  71. package/dist/sshsig/verifier.d.ts +2 -0
  72. package/dist/sshsig/verifier.js +37 -0
  73. package/dist/sshsig/writer.d.ts +17 -0
  74. package/dist/sshsig/writer.js +51 -0
  75. package/dist/types.d.ts +42 -0
  76. package/dist/types.js +1 -0
  77. package/dist/utils.d.ts +59 -0
  78. package/dist/utils.js +323 -0
  79. package/dist/verify.d.ts +7 -0
  80. package/dist/verify.js +16 -0
  81. 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>;