@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,34 @@
1
+ import { verify as rawVerify } from "./verifier";
2
+ import { parse } from "./sig_parser";
3
+ /**
4
+ * Verifies SSH signature against provided data.
5
+ *
6
+ * Returns `true` if the `signature` is a valid signature over `signed_data`, `false` otherwise.
7
+ *
8
+ * @param {Sig | string} signature SSH signature.
9
+ * @param {Uint8Array | string} signed_data Data that has been signed.
10
+ * @param {object} options Pass-in subtle crypto if required or a specific namespace.
11
+ * @returns {Promise<boolean>} Resolves to true if the signature is valid, to false otherwise.
12
+ */
13
+ export async function verify(signature, signed_data, options) {
14
+ if (typeof options === "undefined") {
15
+ options = {};
16
+ }
17
+ const subtle = options.subtle || crypto.subtle;
18
+ if (typeof signature === "string") {
19
+ signature = parse(signature);
20
+ }
21
+ // check namespace matches if provided in options, otherwise default to
22
+ // signature's namespace. Reject if they don't match.
23
+ const namespace = options.namespace || signature.namespace;
24
+ if (namespace !== signature.namespace) {
25
+ return false;
26
+ }
27
+ if (typeof signed_data === "string") {
28
+ signed_data = new TextEncoder().encode(signed_data);
29
+ }
30
+ else {
31
+ signed_data = new Uint8Array(signed_data.buffer.slice(signed_data.byteOffset, signed_data.byteOffset + signed_data.byteLength));
32
+ }
33
+ return await rawVerify(subtle, signature, signed_data);
34
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Byte stream reader helper.
3
+ *
4
+ * This class exposes several helper functions for reading SSH structured data.
5
+ */
6
+ export declare class Reader {
7
+ private view;
8
+ private pos;
9
+ constructor(view: DataView);
10
+ readUint8(): number;
11
+ readUint32(): number;
12
+ peekUint32(): number;
13
+ peekBytes(pos: number, num: number): Reader;
14
+ readBytes(num: number): Reader;
15
+ readString(): Reader;
16
+ peekString(): Reader;
17
+ toString(): string;
18
+ bytes(): ArrayBuffer;
19
+ get isAtEnd(): boolean;
20
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Byte stream reader helper.
3
+ *
4
+ * This class exposes several helper functions for reading SSH structured data.
5
+ */
6
+ export class Reader {
7
+ view;
8
+ pos = 0;
9
+ constructor(view) {
10
+ this.view = view;
11
+ this.view = view;
12
+ }
13
+ readUint8() {
14
+ return this.view.getUint8(this.pos++);
15
+ }
16
+ readUint32() {
17
+ const v = this.peekUint32();
18
+ this.pos += 4;
19
+ return v;
20
+ }
21
+ peekUint32() {
22
+ return this.view.getUint32(this.pos);
23
+ }
24
+ peekBytes(pos, num) {
25
+ const dv = new DataView(this.view.buffer, pos + this.view.byteOffset, num);
26
+ return new Reader(dv);
27
+ }
28
+ readBytes(num) {
29
+ const reader = this.peekBytes(this.pos, num);
30
+ this.pos += num;
31
+ return reader;
32
+ }
33
+ readString() {
34
+ const len = this.readUint32();
35
+ return this.readBytes(len);
36
+ }
37
+ peekString() {
38
+ const len = this.peekUint32();
39
+ return this.peekBytes(this.pos + 4, len);
40
+ }
41
+ toString() {
42
+ let s = "";
43
+ for (let i = 0; i < this.view.byteLength; i++) {
44
+ s += String.fromCharCode(this.view.getUint8(i));
45
+ }
46
+ return s;
47
+ }
48
+ bytes() {
49
+ return this.view.buffer.slice(this.view.byteOffset + this.pos, this.view.byteOffset + this.view.byteLength);
50
+ }
51
+ get isAtEnd() {
52
+ return this.pos == this.view.byteLength;
53
+ }
54
+ }
@@ -0,0 +1,16 @@
1
+ import { Pubkey } from "./formats";
2
+ /**
3
+ * Represents a parsed SSH signature.
4
+ */
5
+ export interface Sig {
6
+ publickey: Pubkey;
7
+ namespace: string;
8
+ reserved: ArrayBuffer;
9
+ hash_algorithm: string;
10
+ signature: {
11
+ sig_algo: string;
12
+ raw_signature: ArrayBuffer;
13
+ flags?: number;
14
+ counter?: number;
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { Sig } from "./sig";
2
+ /**
3
+ * Parse bytes into an SSH signature object.
4
+ *
5
+ * This function throws an Error when the signature is invalid or unsupported.
6
+ *
7
+ * @param {DataView | string} signature Raw bytes to parse.
8
+ */
9
+ export declare function parse(signature: DataView | string): Sig;
@@ -0,0 +1,83 @@
1
+ import { Reader } from "./reader";
2
+ import { parsePubkey } from "./formats";
3
+ import { dearmor } from "./armor";
4
+ /**
5
+ * Parse bytes into an SSH signature object.
6
+ *
7
+ * This function throws an Error when the signature is invalid or unsupported.
8
+ *
9
+ * @param {DataView | string} signature Raw bytes to parse.
10
+ */
11
+ export function parse(signature) {
12
+ let view;
13
+ if (typeof signature === "string") {
14
+ const bytes = dearmor(signature);
15
+ view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
16
+ }
17
+ else {
18
+ view = signature;
19
+ }
20
+ const reader = new Reader(view);
21
+ const magic = reader.readBytes(6).toString();
22
+ if (magic !== "SSHSIG") {
23
+ throw new Error("Expected SSHSIG magic value but got unexpected bytes");
24
+ }
25
+ const version = reader.readUint32();
26
+ if (version !== 1) {
27
+ throw new Error("Expected version 1 but got unexpected version");
28
+ }
29
+ const raw_publickey = reader.peekString().bytes();
30
+ const publickey = reader.readString();
31
+ const pk_algo = publickey.readString().toString();
32
+ const pubkey = parsePubkey(pk_algo, publickey, raw_publickey);
33
+ const namespace = reader.readString().toString();
34
+ const reserved = reader.readString().bytes();
35
+ const hash_algorithm = reader.readString().toString();
36
+ const raw_signature = reader.readString();
37
+ const sig_algo = raw_signature.readString().toString();
38
+ const sig_bytes = raw_signature.readString();
39
+ let bytes;
40
+ if (sig_algo === "ecdsa-sha2-nistp256" || sig_algo === "ecdsa-sha2-nistp384" ||
41
+ sig_algo === "ecdsa-sha2-nistp512" ||
42
+ sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
43
+ let r = new Uint8Array(sig_bytes.readString().bytes());
44
+ if (r.length === 0) {
45
+ throw new Error("invalid ECDSA signature component");
46
+ }
47
+ if (r[0] === 0x00 && r.length % 2 == 1) {
48
+ r = r.slice(1);
49
+ }
50
+ let s = new Uint8Array(sig_bytes.readString().bytes());
51
+ if (s.length === 0) {
52
+ throw new Error("invalid ECDSA signature component");
53
+ }
54
+ if (s[0] === 0x00 && s.length % 2 == 1) {
55
+ s = s.slice(1);
56
+ }
57
+ bytes = new Uint8Array([...r, ...s]).buffer;
58
+ }
59
+ else {
60
+ bytes = sig_bytes.bytes();
61
+ }
62
+ let flags, counter;
63
+ if (sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com" ||
64
+ sig_algo == "sk-ssh-ed25519@openssh.com") {
65
+ flags = new Uint8Array(raw_signature.readBytes(1).bytes())[0];
66
+ counter = raw_signature.readUint32();
67
+ }
68
+ if (!reader.isAtEnd) {
69
+ throw new Error("Signature was parsed but there were still bytes in the stream.");
70
+ }
71
+ return {
72
+ publickey: pubkey,
73
+ namespace,
74
+ reserved,
75
+ hash_algorithm,
76
+ signature: {
77
+ sig_algo,
78
+ raw_signature: bytes,
79
+ flags,
80
+ counter,
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,2 @@
1
+ import { Sig } from "./sig";
2
+ export declare function verify(subtle: SubtleCrypto, signature: Sig, signed_data: Uint8Array<ArrayBuffer>): Promise<boolean>;
@@ -0,0 +1,37 @@
1
+ import { convertAlgorithm, convertHash, convertPublicKey } from "./formats";
2
+ import { Writer } from "./writer";
3
+ export async function verify(subtle, signature, signed_data) {
4
+ //https://nodejs.org/api/webcrypto.html#subtleimportkeyformat-keydata-algorithm-extractable-keyusages
5
+ // for 'RSASSA-PKCS1-v1_5' only spki, pkcs8 and jwk are supported
6
+ // https://stackoverflow.com/questions/46232571/webcrypto-importing-rsa-public-key-with-modulus-and-exponent-using-crypto-subtl
7
+ const { keyData, format } = convertPublicKey(signature.publickey);
8
+ const algorithm = convertAlgorithm(signature.signature.sig_algo);
9
+ const key = await subtle.importKey(format, keyData, algorithm, false, [
10
+ "verify",
11
+ ]);
12
+ const writer = new Writer(96 + signature.namespace.length);
13
+ // https://github.com/openssh/openssh-portable/blob/d575cf44895104e0fcb0629920fb645207218129/PROTOCOL.sshsig
14
+ // MAGIC_PREAMBLE
15
+ writer.writeBytes("SSHSIG");
16
+ // namespace
17
+ writer.writeString(signature.namespace);
18
+ // reserved
19
+ writer.writeUint32(0);
20
+ // hash_algorithm
21
+ const hash = signature.hash_algorithm;
22
+ writer.writeString(hash);
23
+ const digest = new Uint8Array(await subtle.digest(convertHash(hash), signed_data));
24
+ writer.writeString(digest);
25
+ const data = writer.bytes();
26
+ if (signature.publickey.pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com" ||
27
+ signature.publickey.pk_algo === "sk-ssh-ed25519@openssh.com") {
28
+ // https://fuchsia.googlesource.com/third_party/openssh-portable/+/refs/heads/main/PROTOCOL.u2f#176
29
+ const u2f_data = new Writer(100);
30
+ u2f_data.writeBytes(await subtle.digest("SHA-256", Uint8Array.from(Array.prototype.map.call(signature.publickey.application, (x) => x.charCodeAt(0)))));
31
+ u2f_data.writeByte(signature.signature.flags || 0);
32
+ u2f_data.writeUint32(signature.signature.counter || 0);
33
+ u2f_data.writeBytes(await subtle.digest("SHA-256", data));
34
+ return await subtle.verify(algorithm, key, signature.signature.raw_signature, u2f_data.bytes());
35
+ }
36
+ return await subtle.verify(algorithm, key, signature.signature.raw_signature, data);
37
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Byte stream writer helper.
3
+ *
4
+ * This class exposes several helper functions for writing bytes into a buffer.
5
+ */
6
+ export declare class Writer {
7
+ private pos;
8
+ private buffer;
9
+ private view;
10
+ constructor(maxByteLength: number);
11
+ private addResize;
12
+ writeUint32(num: number): void;
13
+ writeString(bytes: ArrayLike<number> | string): void;
14
+ writeBytes(bytes: ArrayLike<number> | string | ArrayBuffer): void;
15
+ writeByte(byte: number): void;
16
+ bytes(): Uint8Array<ArrayBuffer>;
17
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Byte stream writer helper.
3
+ *
4
+ * This class exposes several helper functions for writing bytes into a buffer.
5
+ */
6
+ export class Writer {
7
+ pos = 0;
8
+ buffer;
9
+ view;
10
+ constructor(maxByteLength) {
11
+ this.buffer = new ArrayBuffer(0, { maxByteLength });
12
+ this.view = new DataView(this.buffer);
13
+ }
14
+ addResize(num) {
15
+ this.buffer.resize(this.buffer.byteLength + num);
16
+ }
17
+ writeUint32(num) {
18
+ this.addResize(4);
19
+ this.view.setUint32(this.pos, num);
20
+ this.pos += 4;
21
+ }
22
+ writeString(bytes) {
23
+ this.writeUint32(bytes.length);
24
+ this.writeBytes(bytes);
25
+ }
26
+ writeBytes(bytes) {
27
+ let uint8bytes;
28
+ if (typeof bytes === "string") {
29
+ uint8bytes = Array.prototype.map.call(bytes, (x) => x.charCodeAt(0));
30
+ }
31
+ else if (bytes instanceof ArrayBuffer) {
32
+ uint8bytes = new Uint8Array(bytes);
33
+ }
34
+ else {
35
+ uint8bytes = bytes;
36
+ }
37
+ // it's more efficient to resize once and write all bytes
38
+ // instead of calling `writeByte` in each iteration
39
+ this.addResize(uint8bytes.length);
40
+ for (let i = 0; i < uint8bytes.length; i++) {
41
+ this.view.setUint8(this.pos++, uint8bytes[i]);
42
+ }
43
+ }
44
+ writeByte(byte) {
45
+ this.addResize(1);
46
+ this.view.setUint8(this.pos++, byte);
47
+ }
48
+ bytes() {
49
+ return new Uint8Array(this.buffer);
50
+ }
51
+ }
@@ -0,0 +1,42 @@
1
+ import { JWTPayload } from "jose";
2
+ export type CertificateSignerPayload = {
3
+ public_key: string;
4
+ identity: string;
5
+ extensions?: string[];
6
+ lifetime?: number;
7
+ nonce?: string;
8
+ };
9
+ export type CertificateSignerResponse = {
10
+ certificate: string;
11
+ };
12
+ export type CertificateRequestJWTPayload = {
13
+ email: string;
14
+ sub: string;
15
+ [key: string]: string | string[];
16
+ } & JWTPayload;
17
+ export type SSHExtension = {
18
+ critical: boolean;
19
+ name: string;
20
+ data: Buffer<ArrayBuffer>;
21
+ };
22
+ export interface SshCaBindings {
23
+ DB: D1Database;
24
+ DB_CERTIFICATE_RETENTION: string;
25
+ PRIVATE_KEY: SecretsStoreSecret;
26
+ SSH_CERTIFICATE_EXTENSIONS: string;
27
+ SSH_CERTIFICATE_LIFETIME: string;
28
+ SSH_CERTIFICATE_INCLUDE_SELF: string;
29
+ SSH_CERTIFICATE_PRINCIPALS: string;
30
+ SSH_HOST_CERTIFICATE_LIFETIME: string;
31
+ SSH_HOST_CERTIFICATE_ALLOWED_EMAILS: string;
32
+ SSH_HOST_CERTIFICATE_ALLOWED_ROLES: string;
33
+ ISSUER_DN: string;
34
+ JWT_JWKS_URL: string;
35
+ JWT_AUD: string;
36
+ JWT_ISSUER: string;
37
+ JWT_ALGORITHMS: string;
38
+ JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: string;
39
+ SSH_HOST_CERTIFICATE_ALLOWED_ROLES_CLAIM: string;
40
+ CERTIFICATE_REQUEST_TIME_SKEW_MAX: string;
41
+ LOG_LEVEL: string;
42
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import type { SshCaBindings } from "./types";
2
+ import { Certificate, Key, PrivateKey } from "sshpk";
3
+ import z from "zod";
4
+ import { CertificateRequestJWTPayload } from "./types";
5
+ import { RenewalProofOfPossession, ProofOfPossession } from "./proof";
6
+ export declare const fatalIssue: (ctx: z.RefinementCtx, message: string, val: unknown) => never;
7
+ export declare const transformProofOfPossession: (env: SshCaBindings, val: string, ctx: z.core.$RefinementCtx<string>) => ProofOfPossession | never;
8
+ type AccessToken = {
9
+ email: string;
10
+ sub: string;
11
+ };
12
+ export declare const transformAuthorizationHeader: (env: SshCaBindings, val: string, ctx: z.RefinementCtx) => Promise<AccessToken | never>;
13
+ export declare const transformPublicKey: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => Key | never;
14
+ export declare const identityPrincipals: (env: SshCaBindings, payload: CertificateRequestJWTPayload, claim?: string) => string[];
15
+ type IdentityToken = {
16
+ sub: string;
17
+ principals: string[];
18
+ };
19
+ export declare const parseIdentity: (env: SshCaBindings, jwt: string | undefined, claim?: string) => Promise<IdentityToken>;
20
+ export declare const transformIdentityToken: (env: SshCaBindings, val: string, ctx: z.RefinementCtx) => Promise<IdentityToken | never>;
21
+ /**
22
+ *
23
+ * @param val A SSH certificate either a base64 encoded "string" or a "Buffer\<ArrayBufferLike\>"
24
+ * @param ctx The Zod context for any errors
25
+ * @returns
26
+ */
27
+ export declare const transformCertificate: (val: string | Buffer<ArrayBufferLike>, ctx: z.RefinementCtx) => Certificate | never;
28
+ type ParsedCertificateRequest = {
29
+ proof: ProofOfPossession;
30
+ public_key: Key;
31
+ lifetime: number;
32
+ identity: IdentityToken;
33
+ extensions: string[];
34
+ };
35
+ export declare const refineCertificateRequest: (env: SshCaBindings, val: ParsedCertificateRequest, ctx: z.RefinementCtx) => Promise<never>;
36
+ export declare const transformRenewalProofOfPossession: (env: SshCaBindings, val: string, ctx: z.RefinementCtx) => RenewalProofOfPossession | never;
37
+ type HostCertificateRequest = {
38
+ public_key: Key;
39
+ lifetime: number;
40
+ };
41
+ type ParsedHostCertificateRequest = {
42
+ principals: string[];
43
+ proof: ProofOfPossession;
44
+ } & HostCertificateRequest;
45
+ export declare const refineHostCertificateRequest: (env: SshCaBindings, val: ParsedHostCertificateRequest, ctx: z.RefinementCtx) => Promise<never>;
46
+ type ParsedHostCertificateRenewal = {
47
+ certificate: Certificate;
48
+ proof: RenewalProofOfPossession;
49
+ } & HostCertificateRequest;
50
+ export declare const refineHostCertificateRenewal: (env: SshCaBindings, val: ParsedHostCertificateRenewal, ctx: z.RefinementCtx) => Promise<never>;
51
+ type ParsedRevokeCertificate = {
52
+ serial: bigint;
53
+ public_key: Key;
54
+ proof: ProofOfPossession;
55
+ };
56
+ export declare const refineRevokeCertificate: (env: SshCaBindings, val: ParsedRevokeCertificate, ctx: z.RefinementCtx) => Promise<never>;
57
+ export declare const split: (v: string) => string[];
58
+ export declare const getPublic: (env: SshCaBindings, key?: PrivateKey) => Promise<Key>;
59
+ export {};