@browsercore/testing 0.1.3 → 0.2.1

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 (201) hide show
  1. package/README.md +5 -0
  2. package/bin/coverage-md.mjs +99 -0
  3. package/captures/DIVERGENCE_REPORT.md +325 -0
  4. package/captures/IMPERSIONATION_GAPS_REPORT.md +367 -0
  5. package/captures/README.md +56 -0
  6. package/captures/REVIEW_FINDINGS.md +71 -0
  7. package/captures/_probe/analyze.py +67 -0
  8. package/captures/_probe/capture_bytes.py +127 -0
  9. package/captures/_probe/chrome131_determinism.py +32 -0
  10. package/captures/_probe/diagnose_capture.py +59 -0
  11. package/captures/_probe/golden_analysis.py +91 -0
  12. package/captures/_probe/inspect_golden.py +55 -0
  13. package/captures/_probe/inspect_sv.py +16 -0
  14. package/captures/_probe/make_golden.py +170 -0
  15. package/captures/_probe/output/chrome100.json +219 -0
  16. package/captures/_probe/output/chrome101.json +219 -0
  17. package/captures/_probe/output/chrome104.json +219 -0
  18. package/captures/_probe/output/chrome107.json +220 -0
  19. package/captures/_probe/output/chrome110.json +220 -0
  20. package/captures/_probe/output/chrome116.json +220 -0
  21. package/captures/_probe/output/chrome119.json +219 -0
  22. package/captures/_probe/output/chrome120.json +219 -0
  23. package/captures/_probe/output/chrome123.json +223 -0
  24. package/captures/_probe/output/chrome124.json +224 -0
  25. package/captures/_probe/output/chrome131.json +224 -0
  26. package/captures/_probe/output/chrome131_fresh.json +224 -0
  27. package/captures/_probe/output/chrome133a.json +224 -0
  28. package/captures/_probe/output/chrome136.json +224 -0
  29. package/captures/_probe/output/chrome99.json +219 -0
  30. package/captures/_probe/output/edge101.json +219 -0
  31. package/captures/_probe/output/edge99.json +219 -0
  32. package/captures/_probe/output/firefox133.json +227 -0
  33. package/captures/_probe/output/firefox135.json +230 -0
  34. package/captures/_probe/output/safari155.json +210 -0
  35. package/captures/_probe/output/safari170.json +214 -0
  36. package/captures/_probe/output/safari170_fresh.json +214 -0
  37. package/captures/_probe/output/safari180.json +216 -0
  38. package/captures/_probe/output/safari180_ios.json +216 -0
  39. package/captures/_probe/output/safari184.json +215 -0
  40. package/captures/_probe/output/safari260.json +211 -0
  41. package/captures/_probe/output/summary.json +2520 -0
  42. package/captures/_probe/parse_existing_settings.py +15 -0
  43. package/captures/_probe/peetprint.py +39 -0
  44. package/captures/_probe/probe.py +137 -0
  45. package/captures/_probe/raw_bytes/chrome-131_client_hello.bin +0 -0
  46. package/captures/_probe/raw_bytes/chrome-131_client_hello.hex +32 -0
  47. package/captures/_probe/raw_bytes/firefox-133_client_hello.bin +0 -0
  48. package/captures/_probe/raw_bytes/firefox-133_client_hello.hex +32 -0
  49. package/captures/_probe/raw_bytes/safari-17_client_hello.bin +0 -0
  50. package/captures/_probe/raw_bytes/safari-17_client_hello.hex +32 -0
  51. package/captures/_probe/relay_capture.py +137 -0
  52. package/captures/_probe/socks_capture.py +138 -0
  53. package/captures/_probe/validate_fresh.py +66 -0
  54. package/captures/_probe/validate_golden.py +97 -0
  55. package/captures/_probe/verify_capture.py +68 -0
  56. package/captures/chrome-131/http2/settings.bin +0 -0
  57. package/captures/chrome-131/http2/settings.meta.json +9 -0
  58. package/captures/chrome-131/oracle_capture.reference.json +224 -0
  59. package/captures/chrome-131/tls/client_hello.bin +0 -0
  60. package/captures/chrome-131/tls/client_hello.meta.json +20 -0
  61. package/captures/chrome-140/http2/settings.bin +0 -0
  62. package/captures/chrome-140/http2/settings.meta.json +9 -0
  63. package/captures/chrome-140/tls/client_hello.bin +0 -0
  64. package/captures/chrome-140/tls/client_hello.meta.json +12 -0
  65. package/captures/firefox-128/tls/client_hello.bin +0 -0
  66. package/captures/firefox-128/tls/client_hello.meta.json +12 -0
  67. package/captures/firefox-133/http2/settings.bin +0 -0
  68. package/captures/firefox-133/http2/settings.meta.json +9 -0
  69. package/captures/firefox-133/oracle_capture.reference.json +227 -0
  70. package/captures/firefox-133/tls/client_hello.bin +0 -0
  71. package/captures/firefox-133/tls/client_hello.meta.json +20 -0
  72. package/captures/safari-17/http2/settings.bin +0 -0
  73. package/captures/safari-17/http2/settings.meta.json +9 -0
  74. package/captures/safari-17/oracle_capture.reference.json +214 -0
  75. package/captures/safari-17/tls/client_hello.bin +0 -0
  76. package/captures/safari-17/tls/client_hello.meta.json +20 -0
  77. package/captures/test-malformed/http2/settings.bin +0 -0
  78. package/captures/test-malformed/http2/settings.meta.json +9 -0
  79. package/captures/test-no-extensions/tls/client_hello.bin +0 -0
  80. package/captures/test-no-extensions/tls/client_hello.meta.json +9 -0
  81. package/captures/test-wrong-type/http2/settings.bin +0 -0
  82. package/captures/test-wrong-type/http2/settings.meta.json +9 -0
  83. package/dist/bench/bench.d.ts +24 -6
  84. package/dist/bench/bench.d.ts.map +1 -1
  85. package/dist/bench/bench.js +110 -10
  86. package/dist/bench/bench.js.map +1 -1
  87. package/dist/captures/manifest.d.ts.map +1 -1
  88. package/dist/captures/manifest.js +7 -7
  89. package/dist/captures/manifest.js.map +1 -1
  90. package/dist/categories/compression.d.ts +6 -4
  91. package/dist/categories/compression.d.ts.map +1 -1
  92. package/dist/categories/compression.js +56 -6
  93. package/dist/categories/compression.js.map +1 -1
  94. package/dist/categories/cookies.d.ts +3 -2
  95. package/dist/categories/cookies.d.ts.map +1 -1
  96. package/dist/categories/cookies.js +216 -11
  97. package/dist/categories/cookies.js.map +1 -1
  98. package/dist/categories/redirects.d.ts +8 -2
  99. package/dist/categories/redirects.d.ts.map +1 -1
  100. package/dist/categories/redirects.js +97 -12
  101. package/dist/categories/redirects.js.map +1 -1
  102. package/dist/e2e/cert-gen.d.ts +31 -0
  103. package/dist/e2e/cert-gen.d.ts.map +1 -0
  104. package/dist/e2e/cert-gen.js +158 -0
  105. package/dist/e2e/cert-gen.js.map +1 -0
  106. package/dist/e2e/parse-clienthello.d.ts +177 -0
  107. package/dist/e2e/parse-clienthello.d.ts.map +1 -0
  108. package/dist/e2e/parse-clienthello.js +343 -0
  109. package/dist/e2e/parse-clienthello.js.map +1 -0
  110. package/dist/e2e/sink-server.d.ts +109 -0
  111. package/dist/e2e/sink-server.d.ts.map +1 -0
  112. package/dist/e2e/sink-server.js +292 -0
  113. package/dist/e2e/sink-server.js.map +1 -0
  114. package/dist/e2e/traffic-server.d.ts +48 -0
  115. package/dist/e2e/traffic-server.d.ts.map +1 -0
  116. package/dist/e2e/traffic-server.js +162 -0
  117. package/dist/e2e/traffic-server.js.map +1 -0
  118. package/dist/errors.d.ts.map +1 -1
  119. package/dist/errors.js.map +1 -1
  120. package/dist/fingerprint/index.d.ts +5 -2
  121. package/dist/fingerprint/index.d.ts.map +1 -1
  122. package/dist/fingerprint/index.js +3 -2
  123. package/dist/fingerprint/index.js.map +1 -1
  124. package/dist/fingerprint/ja3.d.ts.map +1 -1
  125. package/dist/fingerprint/ja3.js +33 -4
  126. package/dist/fingerprint/ja3.js.map +1 -1
  127. package/dist/fingerprint/ja4-errors.d.ts +17 -0
  128. package/dist/fingerprint/ja4-errors.d.ts.map +1 -0
  129. package/dist/fingerprint/ja4-errors.js +20 -0
  130. package/dist/fingerprint/ja4-errors.js.map +1 -0
  131. package/dist/fingerprint/ja4-reader.d.ts +44 -0
  132. package/dist/fingerprint/ja4-reader.d.ts.map +1 -0
  133. package/dist/fingerprint/ja4-reader.js +112 -0
  134. package/dist/fingerprint/ja4-reader.js.map +1 -0
  135. package/dist/fingerprint/ja4.d.ts +11 -43
  136. package/dist/fingerprint/ja4.d.ts.map +1 -1
  137. package/dist/fingerprint/ja4.js +54 -141
  138. package/dist/fingerprint/ja4.js.map +1 -1
  139. package/dist/fingerprint/ja4h.d.ts +46 -0
  140. package/dist/fingerprint/ja4h.d.ts.map +1 -0
  141. package/dist/fingerprint/ja4h.js +50 -0
  142. package/dist/fingerprint/ja4h.js.map +1 -0
  143. package/dist/golden/golden.d.ts +15 -1
  144. package/dist/golden/golden.d.ts.map +1 -1
  145. package/dist/golden/golden.js +42 -28
  146. package/dist/golden/golden.js.map +1 -1
  147. package/dist/index.d.ts +21 -1
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +36 -1
  150. package/dist/index.js.map +1 -1
  151. package/dist/node-provider.d.ts +42 -0
  152. package/dist/node-provider.d.ts.map +1 -0
  153. package/dist/node-provider.js +53 -0
  154. package/dist/node-provider.js.map +1 -0
  155. package/dist/provider.d.ts +52 -0
  156. package/dist/provider.d.ts.map +1 -0
  157. package/dist/provider.js +14 -0
  158. package/dist/provider.js.map +1 -0
  159. package/dist/reference/browser-provider.d.ts +33 -0
  160. package/dist/reference/browser-provider.d.ts.map +1 -0
  161. package/dist/reference/browser-provider.js +61 -0
  162. package/dist/reference/browser-provider.js.map +1 -0
  163. package/dist/reference/curl-provider.d.ts +40 -0
  164. package/dist/reference/curl-provider.d.ts.map +1 -0
  165. package/dist/reference/curl-provider.js +96 -0
  166. package/dist/reference/curl-provider.js.map +1 -0
  167. package/dist/reference/dump.d.ts +42 -0
  168. package/dist/reference/dump.d.ts.map +1 -0
  169. package/dist/reference/dump.js +102 -0
  170. package/dist/reference/dump.js.map +1 -0
  171. package/dist/reference/node-crypto-provider.d.ts +31 -0
  172. package/dist/reference/node-crypto-provider.d.ts.map +1 -0
  173. package/dist/reference/node-crypto-provider.js +199 -0
  174. package/dist/reference/node-crypto-provider.js.map +1 -0
  175. package/dist/reference/node-reference.d.ts.map +1 -1
  176. package/dist/reference/node-reference.js +2 -5
  177. package/dist/reference/node-reference.js.map +1 -1
  178. package/dist/reference/reference-errors.d.ts +36 -0
  179. package/dist/reference/reference-errors.d.ts.map +1 -0
  180. package/dist/reference/reference-errors.js +43 -0
  181. package/dist/reference/reference-errors.js.map +1 -0
  182. package/dist/reference/reference-types.d.ts +33 -0
  183. package/dist/reference/reference-types.d.ts.map +1 -0
  184. package/dist/reference/reference-types.js +9 -0
  185. package/dist/reference/reference-types.js.map +1 -0
  186. package/dist/reference/reference.d.ts +14 -77
  187. package/dist/reference/reference.d.ts.map +1 -1
  188. package/dist/reference/reference.js +43 -207
  189. package/dist/reference/reference.js.map +1 -1
  190. package/dist/rfc/rfcTests.d.ts +46 -8
  191. package/dist/rfc/rfcTests.d.ts.map +1 -1
  192. package/dist/rfc/rfcTests.js +203 -16
  193. package/dist/rfc/rfcTests.js.map +1 -1
  194. package/dist/types.d.ts +2 -1
  195. package/dist/types.d.ts.map +1 -1
  196. package/dist/types.js.map +1 -1
  197. package/dist/utils.d.ts +3 -0
  198. package/dist/utils.d.ts.map +1 -1
  199. package/dist/utils.js +11 -5
  200. package/dist/utils.js.map +1 -1
  201. package/package.json +68 -55
@@ -0,0 +1,292 @@
1
+ /**
2
+ * E2E TLS sink server for live handshake verification.
3
+ *
4
+ * Performs a **real** TLS 1.3 handshake against the `@browsercore/tls` client
5
+ * stack and returns the raw ClientHello bytes it saw on the wire — captured
6
+ * *before* Node's `tls` module parses them. This is the missing test oracle:
7
+ * every other TLS test in the stack runs against an in-process fixture; the
8
+ * sink exercises the full stack end-to-end over a real TCP socket.
9
+ *
10
+ * ## Two-socket tap
11
+ *
12
+ * Node's `tls` module parses the ClientHello before user code can see it, so
13
+ * the sink uses a two-socket tap (plan §2.3, Approach A):
14
+ *
15
+ * 1. A raw `node:net` server accepts the connection and pauses the socket.
16
+ * 2. It buffers the first bytes (the ClientHello TLS record) by reading until
17
+ * the record length is satisfied.
18
+ * 3. It unshifts the buffer back onto the socket and wraps the socket in a
19
+ * `tls.TLSSocket` (server-side) with a freshly generated self-signed
20
+ * ECDSA P-256 cert.
21
+ * 4. The handshake completes; the captured bytes + parsed inspection are
22
+ * returned via {@link TlsSinkServer.waitForHandshake}.
23
+ *
24
+ * The sink is single-shot by design: {@link TlsSinkServer.waitForHandshake}
25
+ * resolves on the first completed handshake. Tests create a fresh sink per
26
+ * case (ephemeral port, `maxConnections: 1`).
27
+ */
28
+ import { createServer } from "node:net";
29
+ import { createSecureContext, TLSSocket } from "node:tls";
30
+ import { generateSelfSignedCert } from "./cert-gen.js";
31
+ /**
32
+ * Lightweight TLS 1.3 sink server.
33
+ *
34
+ * Not a general-purpose TLS server — it exists only to confirm a real
35
+ * handshake completes and to return the wire bytes it saw. All assertions
36
+ * live in the *test*; the sink just reports.
37
+ */
38
+ export class TlsSinkServer {
39
+ server;
40
+ ctx;
41
+ state = { state: "idle" };
42
+ waiters = [];
43
+ failers = [];
44
+ /** Host the sink listens on (always 127.0.0.1). */
45
+ host;
46
+ /** Ephemeral port the sink listens on. */
47
+ port;
48
+ /** ALPN protocols the sink negotiates (empty = no ALPN negotiation). */
49
+ alpnProtocols;
50
+ constructor(server, ctx, host, port, alpnProtocols) {
51
+ this.server = server;
52
+ this.ctx = ctx;
53
+ this.host = host;
54
+ this.port = port;
55
+ this.alpnProtocols = alpnProtocols;
56
+ }
57
+ /**
58
+ * Start listening on an ephemeral port on 127.0.0.1.
59
+ *
60
+ * The returned sink is ready to accept one connection. The self-signed
61
+ * cert is generated at call time (throwaway — valid for 1 day).
62
+ *
63
+ * @param opts Optional configuration — ALPN protocols the sink should
64
+ * negotiate (chooses the first client-offered protocol that matches).
65
+ */
66
+ static start(opts = {}) {
67
+ const { certDer, keyPem } = generateSelfSignedCert(["localhost"], ["127.0.0.1"], 1);
68
+ const ctx = createSecureContext({
69
+ key: keyPem,
70
+ cert: derToPem(certDer, "CERTIFICATE"),
71
+ });
72
+ return new Promise((resolve, reject) => {
73
+ const server = createServer({ allowHalfOpen: false });
74
+ server.maxConnections = 1;
75
+ server.on("error", (err) => {
76
+ // If we haven't finished constructing the sink, reject start.
77
+ reject(err);
78
+ });
79
+ server.listen(0, "127.0.0.1", () => {
80
+ const addr = server.address();
81
+ if (addr === null || typeof addr === "string") {
82
+ reject(new Error("sink server did not bind to an ephemeral port"));
83
+ return;
84
+ }
85
+ const sink = new TlsSinkServer(server, ctx, addr.address, addr.port, opts.alpnProtocols ?? []);
86
+ sink.attach();
87
+ resolve(sink);
88
+ });
89
+ });
90
+ }
91
+ /** Wire up the server's connection handler. Must be called once. */
92
+ attach() {
93
+ this.server.on("connection", (socket) => this.onConnection(socket));
94
+ }
95
+ /**
96
+ * Handle an incoming TCP connection: buffer the ClientHello, then hand the
97
+ * socket to a server-side TLSSocket for a real handshake.
98
+ */
99
+ onConnection(rawSocket) {
100
+ // Guard: only one handshake per sink.
101
+ if (this.state.state !== "idle") {
102
+ rawSocket.destroy();
103
+ return;
104
+ }
105
+ const startedAt = Date.now();
106
+ this.state = { state: "capturing", startedAt };
107
+ const chunks = [];
108
+ let totalBuffered = 0;
109
+ // Pause the socket so data accumulates in Node's internal buffer and we
110
+ // can read it deterministically before the TLSSocket consumes it.
111
+ rawSocket.pause();
112
+ /**
113
+ * Read chunks until we have at least one complete TLS record. A TLS
114
+ * record is 5 header bytes + `length` bytes of fragment. The
115
+ * ClientHello typically arrives in a single segment, but we loop to be
116
+ * robust to fragmentation.
117
+ */
118
+ const tryCapture = () => {
119
+ if (totalBuffered < 5)
120
+ return null;
121
+ const recordLen = 5 + ((chunks[0][3] << 8) | chunks[0][4]);
122
+ if (totalBuffered < recordLen)
123
+ return null;
124
+ // Concatenate the buffered chunks and slice to the record boundary.
125
+ const flat = flattenChunks(chunks, totalBuffered);
126
+ return flat.subarray(0, recordLen);
127
+ };
128
+ const onData = (chunk) => {
129
+ chunks.push(chunk);
130
+ totalBuffered += chunk.length;
131
+ const capture = tryCapture();
132
+ if (capture !== null) {
133
+ rawSocket.removeListener("data", onData);
134
+ this.onClientHello(rawSocket, capture, startedAt);
135
+ }
136
+ };
137
+ rawSocket.on("data", onData);
138
+ rawSocket.on("error", (err) => this.fail(err));
139
+ rawSocket.on("close", () => {
140
+ // If we closed before completing the handshake, fail any waiter.
141
+ if (this.state.state !== "done") {
142
+ this.fail(new Error("socket closed before handshake completed"));
143
+ }
144
+ });
145
+ // Resume flow so the data handler fires.
146
+ rawSocket.resume();
147
+ }
148
+ /**
149
+ * We have the raw ClientHello. Unshift it back onto the socket and wrap
150
+ * the socket in a server-side TLSSocket to complete the handshake.
151
+ */
152
+ onClientHello(rawSocket, capture, startedAt) {
153
+ // Wrap the raw socket in a TLSSocket BEFORE unshifting. The TLSSocket
154
+ // attaches its own `data` listener on construction; only after that
155
+ // listener is in place does unshift deliver the captured bytes straight
156
+ // into the TLS layer. Unshifting first leaves the bytes buffered with
157
+ // no listener to consume them, so the handshake never starts.
158
+ const tlsSocket = new TLSSocket(rawSocket, {
159
+ isServer: true,
160
+ secureContext: this.ctx,
161
+ // Request but do not require a client cert — keeps the sink simple.
162
+ requestCert: false,
163
+ ALPNProtocols: this.alpnProtocols.length > 0 ? this.alpnProtocols : undefined,
164
+ });
165
+ // Now push the captured bytes back; the TLSSocket reads them as its
166
+ // first input and begins the handshake.
167
+ rawSocket.unshift(capture);
168
+ const clientHello = {
169
+ raw: capture,
170
+ receivedAt: Date.now(),
171
+ };
172
+ this.state = { state: "handshaking", capture: clientHello, startedAt };
173
+ tlsSocket.on("secure", () => {
174
+ // The secure event fires once the handshake is verified.
175
+ const result = {
176
+ clientHello,
177
+ protocolVersion: tlsSocket.getProtocol() ?? "unknown",
178
+ cipherSuite: tlsSocket.getCipher().name,
179
+ alpnProtocol: narrowServerName(tlsSocket.alpnProtocol),
180
+ serverName: narrowServerName(tlsSocket.servername),
181
+ handshakeDurationMs: Date.now() - startedAt,
182
+ };
183
+ this.done(result);
184
+ // Close the connection after reporting — single-shot sink.
185
+ tlsSocket.end();
186
+ });
187
+ tlsSocket.on("error", (err) => {
188
+ // Distinguish handshake rejection from transport errors.
189
+ this.fail(new Error(`TLS handshake failed: ${err.message}`, { cause: err }));
190
+ });
191
+ }
192
+ /** Transition to the done state and resolve any waiters. */
193
+ done(result) {
194
+ if (this.state.state === "done")
195
+ return;
196
+ this.state = { state: "done", result };
197
+ const w = this.waiters;
198
+ this.waiters = [];
199
+ this.failers = [];
200
+ for (const resolve of w)
201
+ resolve(result);
202
+ }
203
+ /** Transition to the failed state and reject any waiters. */
204
+ fail(error) {
205
+ if (this.state.state === "done")
206
+ return;
207
+ this.state = { state: "failed", error };
208
+ const f = this.failers;
209
+ this.failers = [];
210
+ this.waiters = [];
211
+ for (const reject of f)
212
+ reject(error);
213
+ }
214
+ /**
215
+ * Resolve once a ClientHello has been captured and the handshake completed.
216
+ *
217
+ * @param timeoutMs Max wait in milliseconds (default 5000). The sink is
218
+ * single-shot — only the first handshake resolves this promise.
219
+ * @throws {Error} on timeout or handshake failure.
220
+ */
221
+ waitForHandshake(timeoutMs = 5000) {
222
+ if (this.state.state === "done") {
223
+ return Promise.resolve(this.state.result);
224
+ }
225
+ if (this.state.state === "failed") {
226
+ return Promise.reject(this.state.error);
227
+ }
228
+ return new Promise((resolve, reject) => {
229
+ let timer;
230
+ this.waiters.push((result) => {
231
+ if (timer !== undefined)
232
+ clearTimeout(timer);
233
+ resolve(result);
234
+ });
235
+ this.failers.push((err) => {
236
+ if (timer !== undefined)
237
+ clearTimeout(timer);
238
+ reject(err);
239
+ });
240
+ timer = setTimeout(() => {
241
+ this.fail(new Error(`waitForHandshake timed out after ${timeoutMs}ms`));
242
+ }, timeoutMs);
243
+ });
244
+ }
245
+ /** Stop the server. Resolves once the underlying socket is closed. */
246
+ stop() {
247
+ // Reject any pending waiters so callers don't hang.
248
+ this.fail(new Error("sink server stopped"));
249
+ return new Promise((resolve, reject) => {
250
+ this.server.close((err) => {
251
+ if (err !== undefined)
252
+ reject(err);
253
+ else
254
+ resolve();
255
+ });
256
+ });
257
+ }
258
+ }
259
+ // --- Byte helpers -------------------------------------------------------
260
+ /** Concatenate an array of chunks into a single buffer of `total` bytes. */
261
+ function flattenChunks(chunks, total) {
262
+ const out = new Uint8Array(total);
263
+ let o = 0;
264
+ for (const c of chunks) {
265
+ out.set(c, o);
266
+ o += c.length;
267
+ }
268
+ return out;
269
+ }
270
+ /**
271
+ * Coerce Node's `TLSSocket.servername` (`string | false | null`) to
272
+ * `string | null`. `false` means SNI was requested but not matched; `null`
273
+ * means no SNI was sent. Both map to "no usable servername" here.
274
+ */
275
+ function narrowServerName(value) {
276
+ return typeof value === "string" ? value : null;
277
+ }
278
+ /**
279
+ * Convert a DER-encoded object to PEM (base64 with 64-char lines + headers).
280
+ * Used to feed the self-signed cert into `tls.createSecureContext`, which
281
+ * expects PEM for the `cert` option.
282
+ */
283
+ function derToPem(der, label) {
284
+ const b64 = Buffer.from(der).toString("base64");
285
+ const lines = [`-----BEGIN ${label}-----`];
286
+ for (let i = 0; i < b64.length; i += 64) {
287
+ lines.push(b64.slice(i, i + 64));
288
+ }
289
+ lines.push(`-----END ${label}-----`);
290
+ return lines.join("\n");
291
+ }
292
+ //# sourceMappingURL=sink-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink-server.js","sourceRoot":"","sources":["../../src/e2e/sink-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * E2E traffic server for Bug 6 (AES-128-GCM record decryption) regression.
3
+ *
4
+ * Extends the sink-server pattern: performs a **real** TLS 1.3 handshake
5
+ * against the @browsercore/tls client stack, then serves an encrypted HTTP
6
+ * response. The client must decrypt the full server flight (handshake +
7
+ * application data) — if AEAD key derivation, nonce construction, or record
8
+ * parsing is wrong, decryption fails and the fetch throws.
9
+ *
10
+ * This is the deterministic, network-free reproduction for Bug 6: the same
11
+ * TLS code path that fails against example.com fails here, but without a
12
+ * network dependency.
13
+ */
14
+ /** Result of a completed handshake against the traffic server. */
15
+ export interface TrafficResult {
16
+ /** Negotiated protocol version, e.g. "TLSv1.3". */
17
+ readonly protocolVersion: string;
18
+ /** Negotiated cipher suite. */
19
+ readonly cipherSuite: string;
20
+ /** ALPN protocol negotiated, or null. */
21
+ readonly alpnProtocol: string | null;
22
+ /** Wall-clock handshake duration in milliseconds. */
23
+ readonly handshakeDurationMs: number;
24
+ }
25
+ /**
26
+ * Single-shot TLS 1.3 traffic server. Performs a real handshake, then sends
27
+ * an encrypted HTTP response the client must decrypt.
28
+ */
29
+ export declare class TrafficServer {
30
+ private readonly server;
31
+ private readonly ctx;
32
+ private state;
33
+ private waiters;
34
+ private failers;
35
+ readonly host: string;
36
+ readonly port: number;
37
+ private constructor();
38
+ /** Start listening on an ephemeral port on 127.0.0.1. */
39
+ static start(): Promise<TrafficServer>;
40
+ private attach;
41
+ private onConnection;
42
+ private done;
43
+ private fail;
44
+ /** Resolve once handshake completes. */
45
+ waitForHandshake(timeoutMs?: number): Promise<TrafficResult>;
46
+ stop(): Promise<void>;
47
+ }
48
+ //# sourceMappingURL=traffic-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traffic-server.d.ts","sourceRoot":"","sources":["../../src/e2e/traffic-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAgBH,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,+BAA+B;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,qDAAqD;IACrD,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACxC;AAQD;;;GAGG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,OAAO,CAAmC;IAElD,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B,OAAO,eAKN;IAED,yDAAyD;IACzD,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,aAAa,CAAC,CAsBrC;IAED,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,YAAY;IA0CpB,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,IAAI;IASZ,wCAAwC;IACxC,gBAAgB,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CASzD;IAED,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAKpB;CACJ"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * E2E traffic server for Bug 6 (AES-128-GCM record decryption) regression.
3
+ *
4
+ * Extends the sink-server pattern: performs a **real** TLS 1.3 handshake
5
+ * against the @browsercore/tls client stack, then serves an encrypted HTTP
6
+ * response. The client must decrypt the full server flight (handshake +
7
+ * application data) — if AEAD key derivation, nonce construction, or record
8
+ * parsing is wrong, decryption fails and the fetch throws.
9
+ *
10
+ * This is the deterministic, network-free reproduction for Bug 6: the same
11
+ * TLS code path that fails against example.com fails here, but without a
12
+ * network dependency.
13
+ */
14
+ import { createServer } from "node:net";
15
+ import { createSecureContext, TLSSocket } from "node:tls";
16
+ import { generateSelfSignedCert } from "./cert-gen.js";
17
+ /** HTTP response the server sends after a successful handshake. */
18
+ const HTTP_RESPONSE = [
19
+ "HTTP/1.1 200 OK",
20
+ "Content-Type: text/plain",
21
+ "X-Traffic-Gate: bug6",
22
+ "Content-Length: 19",
23
+ "",
24
+ "TRAFFIC_GATE_OK_BUG6",
25
+ ].join("\r\n");
26
+ /**
27
+ * Single-shot TLS 1.3 traffic server. Performs a real handshake, then sends
28
+ * an encrypted HTTP response the client must decrypt.
29
+ */
30
+ export class TrafficServer {
31
+ server;
32
+ ctx;
33
+ state = { state: "idle" };
34
+ waiters = [];
35
+ failers = [];
36
+ host;
37
+ port;
38
+ constructor(server, ctx, host, port) {
39
+ this.server = server;
40
+ this.ctx = ctx;
41
+ this.host = host;
42
+ this.port = port;
43
+ }
44
+ /** Start listening on an ephemeral port on 127.0.0.1. */
45
+ static start() {
46
+ const { certDer, keyPem } = generateSelfSignedCert(["localhost"], ["127.0.0.1"], 1);
47
+ const ctx = createSecureContext({
48
+ key: keyPem,
49
+ cert: derToPem(certDer, "CERTIFICATE"),
50
+ });
51
+ return new Promise((resolve, reject) => {
52
+ const server = createServer({ allowHalfOpen: false });
53
+ server.maxConnections = 1;
54
+ server.on("error", reject);
55
+ server.listen(0, "127.0.0.1", () => {
56
+ const addr = server.address();
57
+ if (addr === null || typeof addr === "string") {
58
+ reject(new Error("traffic server did not bind"));
59
+ return;
60
+ }
61
+ const traffic = new TrafficServer(server, ctx, addr.address, addr.port);
62
+ traffic.attach();
63
+ resolve(traffic);
64
+ });
65
+ });
66
+ }
67
+ attach() {
68
+ this.server.on("connection", (socket) => this.onConnection(socket));
69
+ }
70
+ onConnection(rawSocket) {
71
+ if (this.state.state !== "idle") {
72
+ rawSocket.destroy();
73
+ return;
74
+ }
75
+ const startedAt = Date.now();
76
+ this.state = { state: "handshaking", startedAt };
77
+ const tlsSocket = new TLSSocket(rawSocket, {
78
+ isServer: true,
79
+ secureContext: this.ctx,
80
+ requestCert: false,
81
+ ALPNProtocols: ["h2", "http/1.1"],
82
+ });
83
+ tlsSocket.on("secure", () => {
84
+ // Handshake completed. Now send the encrypted HTTP response.
85
+ // The client must decrypt this to read the body — this is what
86
+ // exercises the Bug 6 path.
87
+ tlsSocket.write(HTTP_RESPONSE, () => {
88
+ const result = {
89
+ protocolVersion: tlsSocket.getProtocol() ?? "unknown",
90
+ cipherSuite: tlsSocket.getCipher().name,
91
+ alpnProtocol: narrowServerName(tlsSocket.alpnProtocol),
92
+ handshakeDurationMs: Date.now() - startedAt,
93
+ };
94
+ this.done(result);
95
+ });
96
+ });
97
+ tlsSocket.on("error", (err) => {
98
+ this.fail(new Error(`TLS handshake failed: ${err.message}`, { cause: err }));
99
+ });
100
+ tlsSocket.on("close", () => {
101
+ if (this.state.state !== "done") {
102
+ this.fail(new Error("socket closed before handshake completed"));
103
+ }
104
+ });
105
+ }
106
+ done(result) {
107
+ if (this.state.state === "done")
108
+ return;
109
+ this.state = { state: "done", result };
110
+ const w = this.waiters;
111
+ this.waiters = [];
112
+ this.failers = [];
113
+ for (const resolve of w)
114
+ resolve(result);
115
+ }
116
+ fail(error) {
117
+ if (this.state.state === "done")
118
+ return;
119
+ this.state = { state: "failed", error };
120
+ const f = this.failers;
121
+ this.failers = [];
122
+ this.waiters = [];
123
+ for (const reject of f)
124
+ reject(error);
125
+ }
126
+ /** Resolve once handshake completes. */
127
+ waitForHandshake(timeoutMs = 5000) {
128
+ if (this.state.state === "done")
129
+ return Promise.resolve(this.state.result);
130
+ if (this.state.state === "failed")
131
+ return Promise.reject(this.state.error);
132
+ return new Promise((resolve, reject) => {
133
+ let timer;
134
+ this.waiters.push((r) => { if (timer)
135
+ clearTimeout(timer); resolve(r); });
136
+ this.failers.push((e) => { if (timer)
137
+ clearTimeout(timer); reject(e); });
138
+ timer = setTimeout(() => this.fail(new Error("waitForHandshake timed out")), timeoutMs);
139
+ });
140
+ }
141
+ stop() {
142
+ this.fail(new Error("traffic server stopped"));
143
+ return new Promise((resolve, reject) => {
144
+ this.server.close((err) => { if (err)
145
+ reject(err);
146
+ else
147
+ resolve(); });
148
+ });
149
+ }
150
+ }
151
+ function narrowServerName(value) {
152
+ return typeof value === "string" ? value : null;
153
+ }
154
+ function derToPem(der, label) {
155
+ const b64 = Buffer.from(der).toString("base64");
156
+ const lines = [`-----BEGIN ${label}-----`];
157
+ for (let i = 0; i < b64.length; i += 64)
158
+ lines.push(b64.slice(i, i + 64));
159
+ lines.push(`-----END ${label}-----`);
160
+ return lines.join("\n");
161
+ }
162
+ //# sourceMappingURL=traffic-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traffic-server.js","sourceRoot":"","sources":["../../src/e2e/traffic-server.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,0CAA0C;AAC1C,qBAAa,YAAa,SAAQ,KAAK;IACnC,SAAgB,IAAI,EAAG,cAAc,CAAU;IAC/C,SAAyB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;gBAEtC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAK3D;AAED,mDAAmD;AACnD,qBAAa,mBAAoB,SAAQ,KAAK;IAC1C,SAAgB,IAAI,EAAG,qBAAqB,CAAU;IACtD,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,mBAAmB,EAAE,MAAM,CAAC;IAC5C,SAAyB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;gBAG9C,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,EAC3B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAWlC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,0CAA0C;AAC1C,qBAAa,YAAa,SAAQ,KAAK;IACnC,SAAgB,IAAI,EAAG,cAAc,CAAU;IAC/C,SAAyB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IAElD,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,EAIvD;CACJ;AAED,mDAAmD;AACnD,qBAAa,mBAAoB,SAAQ,KAAK;IAC1C,SAAgB,IAAI,EAAG,qBAAqB,CAAU;IACtD,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,SAAgB,mBAAmB,EAAE,MAAM,CAAC;IAC5C,SAAyB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IAElD,YACI,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,EAC3B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,EAU9B;CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,0CAA0C;AAC1C,MAAM,OAAO,YAAa,SAAQ,KAAK;IACnB,IAAI,GAAG,cAAuB,CAAC;IACtB,KAAK,CAAoB;IAElD,YAAY,OAAe,EAAE,OAA2B;QACpD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC;IAChC,CAAC;CACJ;AAED,mDAAmD;AACnD,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC1B,IAAI,GAAG,qBAA8B,CAAC;IACtC,SAAS,CAAS;IAClB,mBAAmB,CAAS;IACnB,KAAK,CAAoB;IAElD,YACI,SAAiB,EACjB,mBAA2B,EAC3B,OAA2B;QAE3B,KAAK,CACD,kBAAkB,SAAS,qBAAqB,mBAAmB,EAAE,EACrE,OAAO,CACV,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC;IAChC,CAAC;CACJ"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,0CAA0C;AAC1C,MAAM,OAAO,YAAa,SAAQ,KAAK;IACnB,IAAI,GAAG,cAAuB,CAAC;IACtB,KAAK,CAAoB;IAElD,YAAY,OAAe,EAAE,OAA2B;QACpD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC;IAChC,CAAC;CACJ;AAED,mDAAmD;AACnD,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC1B,IAAI,GAAG,qBAA8B,CAAC;IACtC,SAAS,CAAS;IAClB,mBAAmB,CAAS;IACnB,KAAK,CAAoB;IAElD,YACI,SAAiB,EACjB,mBAA2B,EAC3B,OAA2B;QAE3B,KAAK,CACD,kBAAkB,SAAS,qBAAqB,mBAAmB,EAAE,EACrE,OAAO,CACV,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC;IAChC,CAAC;CACJ"}
@@ -1,5 +1,8 @@
1
- /** Barrel for fingerprint computation (JA3 / JA4). */
1
+ /** Barrel for fingerprint computation (JA3 / JA4 / JA4H). */
2
2
  export { computeJa3, parseClientHello, Ja3ParseError } from "./ja3.js";
3
3
  export type { Ja3Segments } from "./ja3.js";
4
- export { computeJa4 } from "./ja4.js";
4
+ export { computeJa4, computeJa4Fingerprint, Ja4ParseError } from "./ja4.js";
5
+ export type { Ja4ClientHello, Ja4Fingerprint } from "./ja4.js";
6
+ export { computeJa4h } from "./ja4h.js";
7
+ export type { Ja4hFingerprint, Ja4hRequest } from "./ja4h.js";
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fingerprint/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fingerprint/index.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5E,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
@@ -1,4 +1,5 @@
1
- /** Barrel for fingerprint computation (JA3 / JA4). */
1
+ /** Barrel for fingerprint computation (JA3 / JA4 / JA4H). */
2
2
  export { computeJa3, parseClientHello, Ja3ParseError } from "./ja3.js";
3
- export { computeJa4 } from "./ja4.js";
3
+ export { computeJa4, computeJa4Fingerprint, Ja4ParseError } from "./ja4.js";
4
+ export { computeJa4h } from "./ja4h.js";
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fingerprint/index.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEvE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fingerprint/index.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEvE,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE5E,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ja3.d.ts","sourceRoot":"","sources":["../../src/fingerprint/ja3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,+DAA+D;AAC/D,qBAAa,aAAc,SAAQ,KAAK;IACpC,SAAgB,IAAI,EAAG,eAAe,CAAU;gBACpC,OAAO,EAAE,MAAM;CAI9B;AAOD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,UAAU,GAAG,MAAM,CAM1D;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACnC;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,UAAU,GAAG,WAAW,CAgGrE"}
1
+ {"version":3,"file":"ja3.d.ts","sourceRoot":"","sources":["../../src/fingerprint/ja3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,+DAA+D;AAC/D,qBAAa,aAAc,SAAQ,KAAK;IACpC,SAAgB,IAAI,EAAG,eAAe,CAAU;IAChD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACJ;AAYD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,UAAU,GAAG,MAAM,CAM1D;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACnC;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,UAAU,GAAG,WAAW,CA6GrE"}
@@ -21,7 +21,12 @@ export class Ja3ParseError extends Error {
21
21
  }
22
22
  /** Read a big-endian uint16 at `offset` in `buf`. */
23
23
  function uint16(buf, offset) {
24
- return (buf[offset] << 8) | buf[offset + 1];
24
+ const hi = buf[offset];
25
+ const lo = buf[offset + 1];
26
+ if (hi === undefined || lo === undefined) {
27
+ throw new Ja3ParseError(`uint16 read out of bounds at offset ${offset}`);
28
+ }
29
+ return (hi << 8) | lo;
25
30
  }
26
31
  /**
27
32
  * Parse a TLS ClientHello from a TLS record (or handshake) buffer and return
@@ -75,6 +80,9 @@ export function parseClientHello(clientHello) {
75
80
  pos += 32;
76
81
  // session_id(variable)
77
82
  const sessionIdLen = clientHello[pos];
83
+ if (sessionIdLen === undefined) {
84
+ throw new Ja3ParseError("ClientHello truncated at session id length");
85
+ }
78
86
  pos += 1 + sessionIdLen;
79
87
  // cipher_suites(variable)
80
88
  const cipherSuitesLen = uint16(clientHello, pos);
@@ -83,6 +91,9 @@ export function parseClientHello(clientHello) {
83
91
  pos += cipherSuitesLen;
84
92
  // compression_methods(variable)
85
93
  const compLen = clientHello[pos];
94
+ if (compLen === undefined) {
95
+ throw new Ja3ParseError("ClientHello truncated at compression methods length");
96
+ }
86
97
  pos += 1 + compLen;
87
98
  // extensions(variable)
88
99
  if (pos + 2 > end) {
@@ -114,8 +125,15 @@ export function parseClientHello(clientHello) {
114
125
  else if (extType === 0x000b && extLen >= 1) {
115
126
  // ec_point_formats(11): list of uint8 formats
116
127
  const listLen = clientHello[pos];
128
+ if (listLen === undefined) {
129
+ throw new Ja3ParseError("ClientHello truncated at ec_point_formats length");
130
+ }
117
131
  for (let i = 0; i < listLen; i++) {
118
- ecPointFormats.push(clientHello[pos + 1 + i]);
132
+ const fmt = clientHello[pos + 1 + i];
133
+ if (fmt === undefined) {
134
+ throw new Ja3ParseError("ClientHello truncated in ec_point_formats list");
135
+ }
136
+ ecPointFormats.push(fmt);
119
137
  }
120
138
  }
121
139
  pos += extLen;
@@ -129,12 +147,23 @@ export function parseClientHello(clientHello) {
129
147
  };
130
148
  }
131
149
  function readInt24(buf, offset) {
132
- return (buf[offset] << 16) | (buf[offset + 1] << 8) | buf[offset + 2];
150
+ const hi = buf[offset];
151
+ const mid = buf[offset + 1];
152
+ const lo = buf[offset + 2];
153
+ if (hi === undefined || mid === undefined || lo === undefined) {
154
+ throw new Ja3ParseError(`readInt24 out of bounds at offset ${offset}`);
155
+ }
156
+ return (hi << 16) | (mid << 8) | lo;
133
157
  }
134
158
  function readUint16List(buf, offset, byteLen) {
135
159
  const out = [];
136
160
  for (let i = 0; i + 1 < byteLen; i += 2) {
137
- out.push((buf[offset + i] << 8) | buf[offset + i + 1]);
161
+ const hi = buf[offset + i];
162
+ const lo = buf[offset + i + 1];
163
+ if (hi === undefined || lo === undefined) {
164
+ throw new Ja3ParseError(`readUint16List out of bounds at offset ${offset + i}`);
165
+ }
166
+ out.push((hi << 8) | lo);
138
167
  }
139
168
  return out;
140
169
  }