@andrewheberle/serverless-ssh-ca 0.17.13 → 0.17.15

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 (114) hide show
  1. package/README.md +6 -17
  2. package/dist/api/v3/index.d.ts +1 -0
  3. package/dist/api/v3/index.d.ts.map +1 -0
  4. package/dist/api/v3/index.js +1 -0
  5. package/dist/api/v3/index.js.map +1 -0
  6. package/dist/api/v3/schema.d.ts +9 -0
  7. package/dist/api/v3/schema.d.ts.map +1 -0
  8. package/dist/api/v3/schema.js +4 -4
  9. package/dist/api/v3/schema.js.map +1 -0
  10. package/dist/certificate.d.ts +1 -0
  11. package/dist/certificate.d.ts.map +1 -0
  12. package/dist/certificate.js +1 -0
  13. package/dist/certificate.js.map +1 -0
  14. package/dist/{db.d.ts → db/index.d.ts} +6 -6
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +151 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/migrations/0001_initial_schema.d.ts +3 -0
  19. package/dist/db/migrations/0001_initial_schema.d.ts.map +1 -0
  20. package/dist/db/migrations/0001_initial_schema.js +24 -0
  21. package/dist/db/migrations/0001_initial_schema.js.map +1 -0
  22. package/dist/db/migrations/index.d.ts +3 -0
  23. package/dist/db/migrations/index.d.ts.map +1 -0
  24. package/dist/db/migrations/index.js +5 -0
  25. package/dist/db/migrations/index.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/krl.d.ts +1 -0
  31. package/dist/krl.d.ts.map +1 -0
  32. package/dist/krl.js +1 -0
  33. package/dist/krl.js.map +1 -0
  34. package/dist/logger.d.ts +1 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +1 -0
  37. package/dist/logger.js.map +1 -0
  38. package/dist/proof.d.ts +2 -0
  39. package/dist/proof.d.ts.map +1 -0
  40. package/dist/proof.js +2 -1
  41. package/dist/proof.js.map +1 -0
  42. package/dist/router.d.ts +1 -0
  43. package/dist/router.d.ts.map +1 -0
  44. package/dist/router.js +1 -0
  45. package/dist/router.js.map +1 -0
  46. package/dist/sshsig/armor.d.ts +1 -0
  47. package/dist/sshsig/armor.d.ts.map +1 -0
  48. package/dist/sshsig/armor.js +1 -0
  49. package/dist/sshsig/armor.js.map +1 -0
  50. package/dist/sshsig/formats.d.ts +1 -0
  51. package/dist/sshsig/formats.d.ts.map +1 -0
  52. package/dist/sshsig/formats.js +1 -0
  53. package/dist/sshsig/formats.js.map +1 -0
  54. package/dist/sshsig/index.d.ts +1 -0
  55. package/dist/sshsig/index.d.ts.map +1 -0
  56. package/dist/sshsig/index.js +1 -0
  57. package/dist/sshsig/index.js.map +1 -0
  58. package/dist/sshsig/reader.d.ts +1 -0
  59. package/dist/sshsig/reader.d.ts.map +1 -0
  60. package/dist/sshsig/reader.js +1 -0
  61. package/dist/sshsig/reader.js.map +1 -0
  62. package/dist/sshsig/sig.d.ts +1 -0
  63. package/dist/sshsig/sig.d.ts.map +1 -0
  64. package/dist/sshsig/sig.js +1 -0
  65. package/dist/sshsig/sig.js.map +1 -0
  66. package/dist/sshsig/sig_parser.d.ts +1 -0
  67. package/dist/sshsig/sig_parser.d.ts.map +1 -0
  68. package/dist/sshsig/sig_parser.js +1 -0
  69. package/dist/sshsig/sig_parser.js.map +1 -0
  70. package/dist/sshsig/verifier.d.ts +1 -0
  71. package/dist/sshsig/verifier.d.ts.map +1 -0
  72. package/dist/sshsig/verifier.js +1 -0
  73. package/dist/sshsig/verifier.js.map +1 -0
  74. package/dist/sshsig/writer.d.ts +1 -0
  75. package/dist/sshsig/writer.d.ts.map +1 -0
  76. package/dist/sshsig/writer.js +1 -0
  77. package/dist/sshsig/writer.js.map +1 -0
  78. package/dist/types.d.ts +1 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +1 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/utils.d.ts +1 -0
  83. package/dist/utils.d.ts.map +1 -0
  84. package/dist/utils.js +4 -0
  85. package/dist/utils.js.map +1 -0
  86. package/dist/verify.d.ts +1 -0
  87. package/dist/verify.d.ts.map +1 -0
  88. package/dist/verify.js +1 -0
  89. package/dist/verify.js.map +1 -0
  90. package/package.json +16 -8
  91. package/src/api/v3/index.ts +419 -0
  92. package/src/api/v3/schema.ts +265 -0
  93. package/src/certificate.ts +214 -0
  94. package/src/db/index.ts +194 -0
  95. package/src/db/migrations/0001_initial_schema.ts +25 -0
  96. package/src/db/migrations/index.ts +6 -0
  97. package/src/index.ts +18 -0
  98. package/src/krl.ts +414 -0
  99. package/src/logger.ts +6 -0
  100. package/src/proof.ts +187 -0
  101. package/src/router.ts +35 -0
  102. package/src/sshsig/armor.ts +24 -0
  103. package/src/sshsig/formats.ts +226 -0
  104. package/src/sshsig/index.ts +47 -0
  105. package/src/sshsig/reader.ts +59 -0
  106. package/src/sshsig/sig.ts +17 -0
  107. package/src/sshsig/sig_parser.ts +90 -0
  108. package/src/sshsig/verifier.ts +85 -0
  109. package/src/sshsig/writer.ts +56 -0
  110. package/src/types.ts +49 -0
  111. package/src/utils.ts +414 -0
  112. package/src/verify.ts +25 -0
  113. package/LICENSE +0 -201
  114. package/dist/db.js +0 -111
@@ -0,0 +1,226 @@
1
+ import { Reader } from "./reader";
2
+
3
+ /**
4
+ * Represents an SSH public key.
5
+ */
6
+ export type Pubkey = {
7
+ pk_algo: "ssh-rsa";
8
+ e: Uint8Array;
9
+ n: Uint8Array;
10
+ toString(): string;
11
+ } | {
12
+ pk_algo: "ssh-ed25519";
13
+ key: Uint8Array;
14
+ toString(): string;
15
+ } | {
16
+ pk_algo:
17
+ | "ecdsa-sha2-nistp256"
18
+ | "ecdsa-sha2-nistp384"
19
+ | "ecdsa-sha2-nistp521";
20
+ curve: string;
21
+ point: Uint8Array;
22
+ toString(): string;
23
+ } | {
24
+ pk_algo: "sk-ecdsa-sha2-nistp256@openssh.com";
25
+ curve: string;
26
+ point: Uint8Array;
27
+ // typically "ssh:"
28
+ application: string;
29
+ toString(): string;
30
+ } | {
31
+ pk_algo: "sk-ssh-ed25519@openssh.com";
32
+ key: Uint8Array;
33
+ // typically "ssh:"
34
+ application: string;
35
+ toString(): string;
36
+ };
37
+
38
+ export function parsePubkey(
39
+ pk_algo: string,
40
+ publickey: Reader,
41
+ raw_publickey: ArrayBuffer,
42
+ ): Pubkey {
43
+ let pubkey: Pubkey;
44
+ if (pk_algo === "ssh-rsa") {
45
+ pubkey = {
46
+ pk_algo,
47
+ e: new Uint8Array(publickey.readString().bytes()),
48
+ n: new Uint8Array(publickey.readString().bytes()),
49
+ toString() {
50
+ return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
51
+ },
52
+ };
53
+ } else if (pk_algo === "ssh-ed25519") {
54
+ pubkey = {
55
+ pk_algo,
56
+ key: new Uint8Array(publickey.readString().bytes()),
57
+ toString() {
58
+ return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
59
+ },
60
+ };
61
+ } else if (pk_algo === "sk-ssh-ed25519@openssh.com") {
62
+ const key = new Uint8Array(publickey.readString().bytes());
63
+ const application = publickey.readString().toString();
64
+ pubkey = {
65
+ pk_algo,
66
+ key,
67
+ application,
68
+ toString() {
69
+ return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
70
+ },
71
+ };
72
+ } else if (
73
+ pk_algo === "ecdsa-sha2-nistp256" || pk_algo === "ecdsa-sha2-nistp384" ||
74
+ pk_algo === "ecdsa-sha2-nistp521"
75
+ ) {
76
+ const curve = publickey.readString().toString();
77
+ const point = new Uint8Array(publickey.readString().bytes());
78
+ pubkey = {
79
+ pk_algo,
80
+ curve,
81
+ point,
82
+ toString() {
83
+ return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
84
+ },
85
+ };
86
+ } else if (pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com") {
87
+ const curve = publickey.readString().toString();
88
+ const point = new Uint8Array(publickey.readString().bytes());
89
+ const application = publickey.readString().toString();
90
+ pubkey = {
91
+ pk_algo,
92
+ curve,
93
+ point,
94
+ application,
95
+ toString() {
96
+ return `${pk_algo} ${base64Encode(new Uint8Array(raw_publickey))}`;
97
+ },
98
+ };
99
+ } else {
100
+ throw new Error(`Unsupported pk_algo: ${pk_algo}`);
101
+ }
102
+ return pubkey;
103
+ }
104
+
105
+ function base64Encode(bytes: Uint8Array) {
106
+ return btoa(String.fromCharCode.apply(null, bytes as unknown as number[]));
107
+ }
108
+
109
+ function base64UrlEncode(bytes: Uint8Array) {
110
+ return base64Encode(bytes)
111
+ .replace(
112
+ /\+/g,
113
+ "-",
114
+ )
115
+ .replace(/\//g, "_")
116
+ .replace(/=/g, "");
117
+ }
118
+
119
+ export function convertPublicKey(publickey: Pubkey): {
120
+ format: "jwk";
121
+ keyData: JsonWebKey;
122
+ } | { format: "raw"; keyData: ArrayBuffer } {
123
+ const pk_algo = publickey.pk_algo;
124
+ if (pk_algo === "ssh-rsa") {
125
+ return {
126
+ keyData: {
127
+ kty: "RSA",
128
+ e: base64UrlEncode(publickey.e),
129
+ n: base64UrlEncode(publickey.n),
130
+ },
131
+ format: "jwk",
132
+ };
133
+ } else if (
134
+ pk_algo === "ssh-ed25519" || pk_algo === "sk-ssh-ed25519@openssh.com"
135
+ ) {
136
+ return {
137
+ keyData: publickey.key.buffer.slice(
138
+ publickey.key.byteOffset,
139
+ publickey.key.byteOffset + publickey.key.byteLength
140
+ ) as ArrayBuffer,
141
+ format: "raw",
142
+ };
143
+ } else if (
144
+ pk_algo === "ecdsa-sha2-nistp256" || pk_algo === "ecdsa-sha2-nistp384" ||
145
+ pk_algo === "ecdsa-sha2-nistp521" ||
146
+ pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com"
147
+ ) {
148
+ if (publickey.point[0] !== 0x04) {
149
+ throw new Error("Only uncompressed (0x04) format is supported");
150
+ }
151
+
152
+ const point = publickey.point.slice(1);
153
+
154
+ let crv;
155
+ if (
156
+ pk_algo === "ecdsa-sha2-nistp256" ||
157
+ pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com"
158
+ ) {
159
+ crv = "P-256";
160
+ } else if (pk_algo === "ecdsa-sha2-nistp384") {
161
+ crv = "P-384";
162
+ } else {
163
+ crv = "P-521";
164
+ }
165
+ return {
166
+ keyData: {
167
+ kty: "EC",
168
+ crv,
169
+ x: base64UrlEncode(point.slice(0, point.length / 2)),
170
+ y: base64UrlEncode(point.slice(point.length / 2)),
171
+ },
172
+ format: "jwk",
173
+ };
174
+ } else {
175
+ throw new Error(`Unsupported algo: ${pk_algo}`);
176
+ }
177
+ }
178
+
179
+ export function convertAlgorithm(sig_algo: string) {
180
+ if (sig_algo === "rsa-sha2-512") {
181
+ return {
182
+ name: "RSASSA-PKCS1-v1_5",
183
+ hash: { name: "SHA-512" },
184
+ };
185
+ } else if (
186
+ sig_algo === "ssh-ed25519" || sig_algo === "sk-ssh-ed25519@openssh.com"
187
+ ) {
188
+ return {
189
+ name: "Ed25519",
190
+ hash: { name: "SHA-512" },
191
+ };
192
+ } else if (
193
+ sig_algo === "ecdsa-sha2-nistp256" ||
194
+ sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com"
195
+ ) {
196
+ return {
197
+ name: "ECDSA",
198
+ namedCurve: "P-256",
199
+ hash: { name: "SHA-256" },
200
+ };
201
+ } else if (sig_algo === "ecdsa-sha2-nistp384") {
202
+ return {
203
+ name: "ECDSA",
204
+ namedCurve: "P-384",
205
+ hash: { name: "SHA-384" },
206
+ };
207
+ } else if (sig_algo === "ecdsa-sha2-nistp521") {
208
+ return {
209
+ name: "ECDSA",
210
+ namedCurve: "P-521",
211
+ hash: { name: "SHA-512" },
212
+ };
213
+ } else {
214
+ throw new Error(`Unsupported algo: ${sig_algo}`);
215
+ }
216
+ }
217
+
218
+ export function convertHash(hashName: string): string {
219
+ if (hashName === "sha256") {
220
+ return "SHA-256";
221
+ } else if (hashName === "sha512") {
222
+ return "SHA-512";
223
+ } else {
224
+ throw new Error(`Unknown hash: ${hashName}`);
225
+ }
226
+ }
@@ -0,0 +1,47 @@
1
+ import { Sig } from "./sig";
2
+ import { verify as rawVerify } from "./verifier";
3
+ import { parse } from "./sig_parser";
4
+
5
+ /**
6
+ * Verifies SSH signature against provided data.
7
+ *
8
+ * Returns `true` if the `signature` is a valid signature over `signed_data`, `false` otherwise.
9
+ *
10
+ * @param {Sig | string} signature SSH signature.
11
+ * @param {Uint8Array | string} signed_data Data that has been signed.
12
+ * @param {object} options Pass-in subtle crypto if required or a specific namespace.
13
+ * @returns {Promise<boolean>} Resolves to true if the signature is valid, to false otherwise.
14
+ */
15
+ export async function verify(
16
+ signature: Sig | string,
17
+ signed_data: Uint8Array | string,
18
+ options?: {
19
+ subtle?: SubtleCrypto;
20
+ namespace?: string;
21
+ },
22
+ ): Promise<boolean> {
23
+ if (typeof options === "undefined") {
24
+ options = {};
25
+ }
26
+ const subtle = options.subtle || crypto.subtle;
27
+ if (typeof signature === "string") {
28
+ signature = parse(signature);
29
+ }
30
+
31
+ // check namespace matches if provided in options, otherwise default to
32
+ // signature's namespace. Reject if they don't match.
33
+ const namespace = options.namespace || signature.namespace;
34
+ if (namespace !== signature.namespace) {
35
+ return false;
36
+ }
37
+
38
+ if (typeof signed_data === "string") {
39
+ signed_data = new TextEncoder().encode(signed_data);
40
+ } else {
41
+ signed_data = new Uint8Array(signed_data.buffer.slice(
42
+ signed_data.byteOffset,
43
+ signed_data.byteOffset + signed_data.byteLength
44
+ ) as ArrayBuffer)
45
+ }
46
+ return await rawVerify(subtle, signature, signed_data as Uint8Array<ArrayBuffer>);
47
+ }
@@ -0,0 +1,59 @@
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
+ private pos = 0;
8
+ constructor(private view: DataView) {
9
+ this.view = view;
10
+ }
11
+ readUint8() {
12
+ return this.view.getUint8(this.pos++);
13
+ }
14
+ readUint32() {
15
+ const v = this.peekUint32();
16
+ this.pos += 4;
17
+ return v;
18
+ }
19
+ peekUint32() {
20
+ return this.view.getUint32(this.pos);
21
+ }
22
+ peekBytes(pos: number, num: number) {
23
+ const dv = new DataView(
24
+ this.view.buffer,
25
+ pos + this.view.byteOffset,
26
+ num,
27
+ );
28
+ return new Reader(dv);
29
+ }
30
+ readBytes(num: number) {
31
+ const reader = this.peekBytes(this.pos, num);
32
+ this.pos += num;
33
+ return reader;
34
+ }
35
+ readString() {
36
+ const len = this.readUint32();
37
+ return this.readBytes(len);
38
+ }
39
+ peekString() {
40
+ const len = this.peekUint32();
41
+ return this.peekBytes(this.pos + 4, len);
42
+ }
43
+ toString() {
44
+ let s = "";
45
+ for (let i = 0; i < this.view.byteLength; i++) {
46
+ s += String.fromCharCode(this.view.getUint8(i));
47
+ }
48
+ return s;
49
+ }
50
+ bytes(): ArrayBuffer {
51
+ return (this.view.buffer as ArrayBuffer).slice(
52
+ this.view.byteOffset + this.pos,
53
+ this.view.byteOffset + this.view.byteLength,
54
+ );
55
+ }
56
+ get isAtEnd() {
57
+ return this.pos == this.view.byteLength;
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import { Pubkey } from "./formats";
2
+
3
+ /**
4
+ * Represents a parsed SSH signature.
5
+ */
6
+ export interface Sig {
7
+ publickey: Pubkey;
8
+ namespace: string;
9
+ reserved: ArrayBuffer;
10
+ hash_algorithm: string;
11
+ signature: {
12
+ sig_algo: string;
13
+ raw_signature: ArrayBuffer;
14
+ flags?: number;
15
+ counter?: number;
16
+ };
17
+ }
@@ -0,0 +1,90 @@
1
+ import { Sig } from "./sig";
2
+ import { Reader } from "./reader";
3
+ import { parsePubkey } from "./formats";
4
+ import { dearmor } from "./armor";
5
+
6
+ /**
7
+ * Parse bytes into an SSH signature object.
8
+ *
9
+ * This function throws an Error when the signature is invalid or unsupported.
10
+ *
11
+ * @param {DataView | string} signature Raw bytes to parse.
12
+ */
13
+ export function parse(signature: DataView | string): Sig {
14
+ let view;
15
+ if (typeof signature === "string") {
16
+ const bytes = dearmor(signature);
17
+ view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
18
+ } else {
19
+ view = signature;
20
+ }
21
+ const reader = new Reader(view);
22
+
23
+ const magic = reader.readBytes(6).toString();
24
+ if (magic !== "SSHSIG") {
25
+ throw new Error("Expected SSHSIG magic value but got unexpected bytes");
26
+ }
27
+ const version = reader.readUint32();
28
+ if (version !== 1) {
29
+ throw new Error("Expected version 1 but got unexpected version");
30
+ }
31
+ const raw_publickey = reader.peekString().bytes();
32
+ const publickey = reader.readString();
33
+ const pk_algo = publickey.readString().toString();
34
+ const pubkey = parsePubkey(pk_algo, publickey, raw_publickey);
35
+ const namespace = reader.readString().toString();
36
+ const reserved = reader.readString().bytes();
37
+ const hash_algorithm = reader.readString().toString();
38
+ const raw_signature = reader.readString();
39
+ const sig_algo = raw_signature.readString().toString();
40
+ const sig_bytes = raw_signature.readString();
41
+ let bytes;
42
+ if (
43
+ sig_algo === "ecdsa-sha2-nistp256" || sig_algo === "ecdsa-sha2-nistp384" ||
44
+ sig_algo === "ecdsa-sha2-nistp512" ||
45
+ sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com"
46
+ ) {
47
+ let r = new Uint8Array(sig_bytes.readString().bytes());
48
+ if (r.length === 0) {
49
+ throw new Error("invalid ECDSA signature component")
50
+ }
51
+ if (r[0] === 0x00 && r.length % 2 == 1) {
52
+ r = r.slice(1);
53
+ }
54
+ let s = new Uint8Array(sig_bytes.readString().bytes());
55
+ if (s.length === 0) {
56
+ throw new Error("invalid ECDSA signature component")
57
+ }
58
+ if (s[0] === 0x00 && s.length % 2 == 1) {
59
+ s = s.slice(1);
60
+ }
61
+ bytes = new Uint8Array([...r, ...s]).buffer as ArrayBuffer;
62
+ } else {
63
+ bytes = sig_bytes.bytes();
64
+ }
65
+ let flags, counter;
66
+ if (
67
+ sig_algo === "sk-ecdsa-sha2-nistp256@openssh.com" ||
68
+ sig_algo == "sk-ssh-ed25519@openssh.com"
69
+ ) {
70
+ flags = new Uint8Array(raw_signature.readBytes(1).bytes())[0];
71
+ counter = raw_signature.readUint32();
72
+ }
73
+ if (!reader.isAtEnd) {
74
+ throw new Error(
75
+ "Signature was parsed but there were still bytes in the stream.",
76
+ );
77
+ }
78
+ return {
79
+ publickey: pubkey,
80
+ namespace,
81
+ reserved,
82
+ hash_algorithm,
83
+ signature: {
84
+ sig_algo,
85
+ raw_signature: bytes,
86
+ flags,
87
+ counter,
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,85 @@
1
+ import { Sig } from "./sig";
2
+ import { convertAlgorithm, convertHash, convertPublicKey } from "./formats";
3
+ import { Writer } from "./writer";
4
+
5
+ export async function verify(
6
+ subtle: SubtleCrypto,
7
+ signature: Sig,
8
+ signed_data: Uint8Array<ArrayBuffer>,
9
+ ) {
10
+ //https://nodejs.org/api/webcrypto.html#subtleimportkeyformat-keydata-algorithm-extractable-keyusages
11
+ // for 'RSASSA-PKCS1-v1_5' only spki, pkcs8 and jwk are supported
12
+ // https://stackoverflow.com/questions/46232571/webcrypto-importing-rsa-public-key-with-modulus-and-exponent-using-crypto-subtl
13
+ const { keyData, format } = convertPublicKey(signature.publickey);
14
+ const algorithm = convertAlgorithm(signature.signature.sig_algo);
15
+ const key = await subtle.importKey(
16
+ format as unknown as "raw",
17
+ keyData as unknown as BufferSource,
18
+ algorithm,
19
+ false,
20
+ [
21
+ "verify",
22
+ ],
23
+ );
24
+
25
+ const writer = new Writer(96 + signature.namespace.length);
26
+ // https://github.com/openssh/openssh-portable/blob/d575cf44895104e0fcb0629920fb645207218129/PROTOCOL.sshsig
27
+ // MAGIC_PREAMBLE
28
+ writer.writeBytes("SSHSIG");
29
+ // namespace
30
+ writer.writeString(signature.namespace);
31
+ // reserved
32
+ writer.writeUint32(0);
33
+ // hash_algorithm
34
+ const hash = signature.hash_algorithm;
35
+ writer.writeString(hash);
36
+ const digest = new Uint8Array(
37
+ await subtle.digest(
38
+ convertHash(hash),
39
+ signed_data,
40
+ ),
41
+ );
42
+ writer.writeString(digest);
43
+
44
+ const data = writer.bytes();
45
+
46
+ if (
47
+ signature.publickey.pk_algo === "sk-ecdsa-sha2-nistp256@openssh.com" ||
48
+ signature.publickey.pk_algo === "sk-ssh-ed25519@openssh.com"
49
+ ) {
50
+ // https://fuchsia.googlesource.com/third_party/openssh-portable/+/refs/heads/main/PROTOCOL.u2f#176
51
+ const u2f_data = new Writer(100);
52
+ u2f_data.writeBytes(
53
+ await subtle.digest(
54
+ "SHA-256",
55
+ Uint8Array.from(
56
+ Array.prototype.map.call(
57
+ signature.publickey.application,
58
+ (x) => x.charCodeAt(0),
59
+ ) as unknown as number[],
60
+ ),
61
+ ),
62
+ );
63
+ u2f_data.writeByte(signature.signature.flags || 0);
64
+ u2f_data.writeUint32(signature.signature.counter || 0);
65
+ u2f_data.writeBytes(
66
+ await subtle.digest(
67
+ "SHA-256",
68
+ data,
69
+ ),
70
+ );
71
+ return await subtle.verify(
72
+ algorithm,
73
+ key,
74
+ signature.signature.raw_signature,
75
+ u2f_data.bytes(),
76
+ );
77
+ }
78
+
79
+ return await subtle.verify(
80
+ algorithm,
81
+ key,
82
+ signature.signature.raw_signature,
83
+ data,
84
+ );
85
+ }
@@ -0,0 +1,56 @@
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
+ private pos = 0;
8
+ private buffer: ArrayBuffer;
9
+ private view: DataView;
10
+ constructor(maxByteLength: number) {
11
+ this.buffer = new (ArrayBuffer as unknown as {
12
+ new (a: number, b: { maxByteLength: number }): ArrayBuffer;
13
+ })(0, { maxByteLength });
14
+ this.view = new DataView(this.buffer);
15
+ }
16
+ private addResize(num: number) {
17
+ (this.buffer as unknown as { resize(num: number): void }).resize(
18
+ this.buffer.byteLength + num,
19
+ );
20
+ }
21
+ writeUint32(num: number) {
22
+ this.addResize(4);
23
+ this.view.setUint32(this.pos, num);
24
+ this.pos += 4;
25
+ }
26
+ writeString(bytes: ArrayLike<number> | string) {
27
+ this.writeUint32(bytes.length);
28
+ this.writeBytes(bytes);
29
+ }
30
+ writeBytes(bytes: ArrayLike<number> | string | ArrayBuffer) {
31
+ let uint8bytes;
32
+ if (typeof bytes === "string") {
33
+ uint8bytes = Array.prototype.map.call(
34
+ bytes,
35
+ (x) => x.charCodeAt(0),
36
+ ) as number[];
37
+ } else if (bytes instanceof ArrayBuffer) {
38
+ uint8bytes = new Uint8Array(bytes);
39
+ } else {
40
+ uint8bytes = bytes;
41
+ }
42
+ // it's more efficient to resize once and write all bytes
43
+ // instead of calling `writeByte` in each iteration
44
+ this.addResize(uint8bytes.length);
45
+ for (let i = 0; i < uint8bytes.length; i++) {
46
+ this.view.setUint8(this.pos++, uint8bytes[i]!);
47
+ }
48
+ }
49
+ writeByte(byte: number) {
50
+ this.addResize(1);
51
+ this.view.setUint8(this.pos++, byte);
52
+ }
53
+ bytes() {
54
+ return new Uint8Array(this.buffer);
55
+ }
56
+ }
package/src/types.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { JWTPayload } from "jose"
2
+
3
+ // this is the JSON payload of a certificate request
4
+ export type CertificateSignerPayload = {
5
+ public_key: string
6
+ identity: string
7
+ extensions?: string[]
8
+ lifetime?: number
9
+ nonce?: string
10
+ }
11
+
12
+ export type CertificateSignerResponse = {
13
+ certificate: string
14
+ }
15
+
16
+ // this is the expected JWT payload for a certificate request
17
+ export type CertificateRequestJWTPayload = {
18
+ email: string
19
+ sub: string
20
+ [key: string]: string | string[]
21
+ } & JWTPayload
22
+
23
+ export type SSHExtension = {
24
+ critical: boolean;
25
+ name: string;
26
+ data: Buffer<ArrayBuffer>
27
+ }
28
+
29
+ export interface SshCaBindings {
30
+ DB: D1Database
31
+ DB_CERTIFICATE_RETENTION: string
32
+ PRIVATE_KEY: SecretsStoreSecret
33
+ SSH_CERTIFICATE_EXTENSIONS: string
34
+ SSH_CERTIFICATE_LIFETIME: string
35
+ SSH_CERTIFICATE_INCLUDE_SELF: string
36
+ SSH_CERTIFICATE_PRINCIPALS: string
37
+ SSH_HOST_CERTIFICATE_LIFETIME: string
38
+ SSH_HOST_CERTIFICATE_ALLOWED_EMAILS: string
39
+ SSH_HOST_CERTIFICATE_ALLOWED_ROLES: string
40
+ ISSUER_DN: string
41
+ JWT_JWKS_URL: string
42
+ JWT_AUD: string
43
+ JWT_ISSUER: string
44
+ JWT_ALGORITHMS: string
45
+ JWT_SSH_CERTIFICATE_PRINCIPALS_CLAIM: string
46
+ SSH_HOST_CERTIFICATE_ALLOWED_ROLES_CLAIM: string
47
+ CERTIFICATE_REQUEST_TIME_SKEW_MAX: string
48
+ LOG_LEVEL: string
49
+ }