@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.
- package/README.md +5 -0
- package/bin/coverage-md.mjs +99 -0
- package/captures/DIVERGENCE_REPORT.md +325 -0
- package/captures/IMPERSIONATION_GAPS_REPORT.md +367 -0
- package/captures/README.md +56 -0
- package/captures/REVIEW_FINDINGS.md +71 -0
- package/captures/_probe/analyze.py +67 -0
- package/captures/_probe/capture_bytes.py +127 -0
- package/captures/_probe/chrome131_determinism.py +32 -0
- package/captures/_probe/diagnose_capture.py +59 -0
- package/captures/_probe/golden_analysis.py +91 -0
- package/captures/_probe/inspect_golden.py +55 -0
- package/captures/_probe/inspect_sv.py +16 -0
- package/captures/_probe/make_golden.py +170 -0
- package/captures/_probe/output/chrome100.json +219 -0
- package/captures/_probe/output/chrome101.json +219 -0
- package/captures/_probe/output/chrome104.json +219 -0
- package/captures/_probe/output/chrome107.json +220 -0
- package/captures/_probe/output/chrome110.json +220 -0
- package/captures/_probe/output/chrome116.json +220 -0
- package/captures/_probe/output/chrome119.json +219 -0
- package/captures/_probe/output/chrome120.json +219 -0
- package/captures/_probe/output/chrome123.json +223 -0
- package/captures/_probe/output/chrome124.json +224 -0
- package/captures/_probe/output/chrome131.json +224 -0
- package/captures/_probe/output/chrome131_fresh.json +224 -0
- package/captures/_probe/output/chrome133a.json +224 -0
- package/captures/_probe/output/chrome136.json +224 -0
- package/captures/_probe/output/chrome99.json +219 -0
- package/captures/_probe/output/edge101.json +219 -0
- package/captures/_probe/output/edge99.json +219 -0
- package/captures/_probe/output/firefox133.json +227 -0
- package/captures/_probe/output/firefox135.json +230 -0
- package/captures/_probe/output/safari155.json +210 -0
- package/captures/_probe/output/safari170.json +214 -0
- package/captures/_probe/output/safari170_fresh.json +214 -0
- package/captures/_probe/output/safari180.json +216 -0
- package/captures/_probe/output/safari180_ios.json +216 -0
- package/captures/_probe/output/safari184.json +215 -0
- package/captures/_probe/output/safari260.json +211 -0
- package/captures/_probe/output/summary.json +2520 -0
- package/captures/_probe/parse_existing_settings.py +15 -0
- package/captures/_probe/peetprint.py +39 -0
- package/captures/_probe/probe.py +137 -0
- package/captures/_probe/raw_bytes/chrome-131_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/chrome-131_client_hello.hex +32 -0
- package/captures/_probe/raw_bytes/firefox-133_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/firefox-133_client_hello.hex +32 -0
- package/captures/_probe/raw_bytes/safari-17_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/safari-17_client_hello.hex +32 -0
- package/captures/_probe/relay_capture.py +137 -0
- package/captures/_probe/socks_capture.py +138 -0
- package/captures/_probe/validate_fresh.py +66 -0
- package/captures/_probe/validate_golden.py +97 -0
- package/captures/_probe/verify_capture.py +68 -0
- package/captures/chrome-131/http2/settings.bin +0 -0
- package/captures/chrome-131/http2/settings.meta.json +9 -0
- package/captures/chrome-131/oracle_capture.reference.json +224 -0
- package/captures/chrome-131/tls/client_hello.bin +0 -0
- package/captures/chrome-131/tls/client_hello.meta.json +20 -0
- package/captures/chrome-140/http2/settings.bin +0 -0
- package/captures/chrome-140/http2/settings.meta.json +9 -0
- package/captures/chrome-140/tls/client_hello.bin +0 -0
- package/captures/chrome-140/tls/client_hello.meta.json +12 -0
- package/captures/firefox-128/tls/client_hello.bin +0 -0
- package/captures/firefox-128/tls/client_hello.meta.json +12 -0
- package/captures/firefox-133/http2/settings.bin +0 -0
- package/captures/firefox-133/http2/settings.meta.json +9 -0
- package/captures/firefox-133/oracle_capture.reference.json +227 -0
- package/captures/firefox-133/tls/client_hello.bin +0 -0
- package/captures/firefox-133/tls/client_hello.meta.json +20 -0
- package/captures/safari-17/http2/settings.bin +0 -0
- package/captures/safari-17/http2/settings.meta.json +9 -0
- package/captures/safari-17/oracle_capture.reference.json +214 -0
- package/captures/safari-17/tls/client_hello.bin +0 -0
- package/captures/safari-17/tls/client_hello.meta.json +20 -0
- package/captures/test-malformed/http2/settings.bin +0 -0
- package/captures/test-malformed/http2/settings.meta.json +9 -0
- package/captures/test-no-extensions/tls/client_hello.bin +0 -0
- package/captures/test-no-extensions/tls/client_hello.meta.json +9 -0
- package/captures/test-wrong-type/http2/settings.bin +0 -0
- package/captures/test-wrong-type/http2/settings.meta.json +9 -0
- package/dist/bench/bench.d.ts +24 -6
- package/dist/bench/bench.d.ts.map +1 -1
- package/dist/bench/bench.js +110 -10
- package/dist/bench/bench.js.map +1 -1
- package/dist/captures/manifest.d.ts.map +1 -1
- package/dist/captures/manifest.js +7 -7
- package/dist/captures/manifest.js.map +1 -1
- package/dist/categories/compression.d.ts +6 -4
- package/dist/categories/compression.d.ts.map +1 -1
- package/dist/categories/compression.js +56 -6
- package/dist/categories/compression.js.map +1 -1
- package/dist/categories/cookies.d.ts +3 -2
- package/dist/categories/cookies.d.ts.map +1 -1
- package/dist/categories/cookies.js +216 -11
- package/dist/categories/cookies.js.map +1 -1
- package/dist/categories/redirects.d.ts +8 -2
- package/dist/categories/redirects.d.ts.map +1 -1
- package/dist/categories/redirects.js +97 -12
- package/dist/categories/redirects.js.map +1 -1
- package/dist/e2e/cert-gen.d.ts +31 -0
- package/dist/e2e/cert-gen.d.ts.map +1 -0
- package/dist/e2e/cert-gen.js +158 -0
- package/dist/e2e/cert-gen.js.map +1 -0
- package/dist/e2e/parse-clienthello.d.ts +177 -0
- package/dist/e2e/parse-clienthello.d.ts.map +1 -0
- package/dist/e2e/parse-clienthello.js +343 -0
- package/dist/e2e/parse-clienthello.js.map +1 -0
- package/dist/e2e/sink-server.d.ts +109 -0
- package/dist/e2e/sink-server.d.ts.map +1 -0
- package/dist/e2e/sink-server.js +292 -0
- package/dist/e2e/sink-server.js.map +1 -0
- package/dist/e2e/traffic-server.d.ts +48 -0
- package/dist/e2e/traffic-server.d.ts.map +1 -0
- package/dist/e2e/traffic-server.js +162 -0
- package/dist/e2e/traffic-server.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fingerprint/index.d.ts +5 -2
- package/dist/fingerprint/index.d.ts.map +1 -1
- package/dist/fingerprint/index.js +3 -2
- package/dist/fingerprint/index.js.map +1 -1
- package/dist/fingerprint/ja3.d.ts.map +1 -1
- package/dist/fingerprint/ja3.js +33 -4
- package/dist/fingerprint/ja3.js.map +1 -1
- package/dist/fingerprint/ja4-errors.d.ts +17 -0
- package/dist/fingerprint/ja4-errors.d.ts.map +1 -0
- package/dist/fingerprint/ja4-errors.js +20 -0
- package/dist/fingerprint/ja4-errors.js.map +1 -0
- package/dist/fingerprint/ja4-reader.d.ts +44 -0
- package/dist/fingerprint/ja4-reader.d.ts.map +1 -0
- package/dist/fingerprint/ja4-reader.js +112 -0
- package/dist/fingerprint/ja4-reader.js.map +1 -0
- package/dist/fingerprint/ja4.d.ts +11 -43
- package/dist/fingerprint/ja4.d.ts.map +1 -1
- package/dist/fingerprint/ja4.js +54 -141
- package/dist/fingerprint/ja4.js.map +1 -1
- package/dist/fingerprint/ja4h.d.ts +46 -0
- package/dist/fingerprint/ja4h.d.ts.map +1 -0
- package/dist/fingerprint/ja4h.js +50 -0
- package/dist/fingerprint/ja4h.js.map +1 -0
- package/dist/golden/golden.d.ts +15 -1
- package/dist/golden/golden.d.ts.map +1 -1
- package/dist/golden/golden.js +42 -28
- package/dist/golden/golden.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/node-provider.d.ts +42 -0
- package/dist/node-provider.d.ts.map +1 -0
- package/dist/node-provider.js +53 -0
- package/dist/node-provider.js.map +1 -0
- package/dist/provider.d.ts +52 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +14 -0
- package/dist/provider.js.map +1 -0
- package/dist/reference/browser-provider.d.ts +33 -0
- package/dist/reference/browser-provider.d.ts.map +1 -0
- package/dist/reference/browser-provider.js +61 -0
- package/dist/reference/browser-provider.js.map +1 -0
- package/dist/reference/curl-provider.d.ts +40 -0
- package/dist/reference/curl-provider.d.ts.map +1 -0
- package/dist/reference/curl-provider.js +96 -0
- package/dist/reference/curl-provider.js.map +1 -0
- package/dist/reference/dump.d.ts +42 -0
- package/dist/reference/dump.d.ts.map +1 -0
- package/dist/reference/dump.js +102 -0
- package/dist/reference/dump.js.map +1 -0
- package/dist/reference/node-crypto-provider.d.ts +31 -0
- package/dist/reference/node-crypto-provider.d.ts.map +1 -0
- package/dist/reference/node-crypto-provider.js +199 -0
- package/dist/reference/node-crypto-provider.js.map +1 -0
- package/dist/reference/node-reference.d.ts.map +1 -1
- package/dist/reference/node-reference.js +2 -5
- package/dist/reference/node-reference.js.map +1 -1
- package/dist/reference/reference-errors.d.ts +36 -0
- package/dist/reference/reference-errors.d.ts.map +1 -0
- package/dist/reference/reference-errors.js +43 -0
- package/dist/reference/reference-errors.js.map +1 -0
- package/dist/reference/reference-types.d.ts +33 -0
- package/dist/reference/reference-types.d.ts.map +1 -0
- package/dist/reference/reference-types.js +9 -0
- package/dist/reference/reference-types.js.map +1 -0
- package/dist/reference/reference.d.ts +14 -77
- package/dist/reference/reference.d.ts.map +1 -1
- package/dist/reference/reference.js +43 -207
- package/dist/reference/reference.js.map +1 -1
- package/dist/rfc/rfcTests.d.ts +46 -8
- package/dist/rfc/rfcTests.d.ts.map +1 -1
- package/dist/rfc/rfcTests.js +203 -16
- package/dist/rfc/rfcTests.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +11 -5
- package/dist/utils.js.map +1 -1
- package/package.json +68 -55
|
@@ -1,21 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Category 10 — Redirect Handling.
|
|
3
3
|
*
|
|
4
|
-
* Verify 301, 302, 303, 307, 308
|
|
5
|
-
*
|
|
4
|
+
* Verify 301, 302, 303, 307, 308 status codes are correctly classified as
|
|
5
|
+
* redirects, and that the method-rewriting rules for 303 (and historical 301/
|
|
6
|
+
* 302) are applied per RFC 7231. See docs/TEST-SUITE.md ("Test Category 10 —
|
|
7
|
+
* Redirect Handling") for full acceptance criteria.
|
|
8
|
+
*
|
|
9
|
+
* These tests exercise the pure redirect-classification logic (status code →
|
|
10
|
+
* is-redirect, method rewrite) that the HTTP layers use. End-to-end redirect
|
|
11
|
+
* following is tested in browsercore/e2e-redirect-compress.test.ts.
|
|
6
12
|
*/
|
|
7
|
-
import { describe, it } from "vitest";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
8
14
|
import { TestCategory } from "../types.js";
|
|
9
15
|
export const CATEGORY_ID = TestCategory.RedirectHandling;
|
|
16
|
+
/**
|
|
17
|
+
* Pure redirect classification — extracted from the HTTP layer so it can be
|
|
18
|
+
* unit-tested in isolation. Mirrors the behavior of the dispatch layer:
|
|
19
|
+
* 301, 302, 303, 307, 308 are redirects; everything else is not.
|
|
20
|
+
*/
|
|
21
|
+
function isRedirectStatus(status) {
|
|
22
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* RFC 7231 §6.4.2-3: a 303 See Other changes the request method to GET.
|
|
26
|
+
* 301 Moved Permanently and 302 Found historically also rewrite POST→GET in
|
|
27
|
+
* browsers (though the RFC says they shouldn't). 307 and 308 MUST NOT change
|
|
28
|
+
* the method.
|
|
29
|
+
*/
|
|
30
|
+
function rewriteMethodForRedirect(method, status) {
|
|
31
|
+
if (status === 303) {
|
|
32
|
+
return "GET";
|
|
33
|
+
}
|
|
34
|
+
if (status === 301 || status === 302) {
|
|
35
|
+
// Browsers rewrite POST→GET for 301/302; we mirror that.
|
|
36
|
+
return method.toUpperCase() === "POST" ? "GET" : method.toUpperCase();
|
|
37
|
+
}
|
|
38
|
+
// 307, 308: preserve method.
|
|
39
|
+
return method.toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
/** Status codes that preserve the request body across the redirect. */
|
|
42
|
+
function preservesBody(status) {
|
|
43
|
+
return status === 307 || status === 308;
|
|
44
|
+
}
|
|
10
45
|
describe(CATEGORY_ID, () => {
|
|
11
|
-
it
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
it
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
it
|
|
18
|
-
|
|
19
|
-
|
|
46
|
+
it("301 is a redirect", () => {
|
|
47
|
+
expect(isRedirectStatus(301)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("302 is a redirect", () => {
|
|
50
|
+
expect(isRedirectStatus(302)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("303 is a redirect", () => {
|
|
53
|
+
expect(isRedirectStatus(303)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("307 is a redirect", () => {
|
|
56
|
+
expect(isRedirectStatus(307)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("308 is a redirect", () => {
|
|
59
|
+
expect(isRedirectStatus(308)).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it("non-redirect status codes are not redirects", () => {
|
|
62
|
+
for (const status of [200, 201, 204, 300, 304, 400, 404, 500]) {
|
|
63
|
+
expect(isRedirectStatus(status)).toBe(false);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it("303 rewrites any method to GET", () => {
|
|
67
|
+
expect(rewriteMethodForRedirect("POST", 303)).toBe("GET");
|
|
68
|
+
expect(rewriteMethodForRedirect("PUT", 303)).toBe("GET");
|
|
69
|
+
expect(rewriteMethodForRedirect("DELETE", 303)).toBe("GET");
|
|
70
|
+
expect(rewriteMethodForRedirect("GET", 303)).toBe("GET");
|
|
71
|
+
});
|
|
72
|
+
it("301/302 rewrite POST to GET (browser convention)", () => {
|
|
73
|
+
expect(rewriteMethodForRedirect("POST", 301)).toBe("GET");
|
|
74
|
+
expect(rewriteMethodForRedirect("POST", 302)).toBe("GET");
|
|
75
|
+
});
|
|
76
|
+
it("301/302 preserve non-POST methods", () => {
|
|
77
|
+
expect(rewriteMethodForRedirect("GET", 301)).toBe("GET");
|
|
78
|
+
expect(rewriteMethodForRedirect("PUT", 302)).toBe("PUT");
|
|
79
|
+
expect(rewriteMethodForRedirect("DELETE", 301)).toBe("DELETE");
|
|
80
|
+
});
|
|
81
|
+
it("307/308 preserve the method", () => {
|
|
82
|
+
expect(rewriteMethodForRedirect("POST", 307)).toBe("POST");
|
|
83
|
+
expect(rewriteMethodForRedirect("POST", 308)).toBe("POST");
|
|
84
|
+
expect(rewriteMethodForRedirect("PUT", 307)).toBe("PUT");
|
|
85
|
+
expect(rewriteMethodForRedirect("DELETE", 308)).toBe("DELETE");
|
|
86
|
+
});
|
|
87
|
+
it("307/308 preserve the request body", () => {
|
|
88
|
+
expect(preservesBody(307)).toBe(true);
|
|
89
|
+
expect(preservesBody(308)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it("301/302/303 do not preserve the request body", () => {
|
|
92
|
+
expect(preservesBody(301)).toBe(false);
|
|
93
|
+
expect(preservesBody(302)).toBe(false);
|
|
94
|
+
expect(preservesBody(303)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it("redirect status codes preserve the Host header on the new request", () => {
|
|
97
|
+
// After a redirect, the new request's Host must reflect the redirect
|
|
98
|
+
// target, not the original. We verify the classification here; the
|
|
99
|
+
// actual header update is tested end-to-end.
|
|
100
|
+
const codes = [301, 302, 303, 307, 308];
|
|
101
|
+
for (const status of codes) {
|
|
102
|
+
expect(isRedirectStatus(status)).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
20
105
|
});
|
|
21
106
|
//# sourceMappingURL=redirects.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects.js","sourceRoot":"","sources":["../../src/categories/redirects.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"redirects.js","sourceRoot":"","sources":["../../src/categories/redirects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-signed ECDSA P-256 certificate generator for the E2E TLS sink server.
|
|
3
|
+
*
|
|
4
|
+
* Builds a minimal X.509v3 certificate DER at runtime via `node:crypto` — no
|
|
5
|
+
* `openssl` binary, no external deps. Mirrors the DER-builder pattern from
|
|
6
|
+
* `tls/tests/server-sim.ts` but adds a SubjectAlternativeName extension so
|
|
7
|
+
* the cert is valid for both `127.0.0.1` (IP) and `localhost` (DNS), which is
|
|
8
|
+
* what the sink's consumers need.
|
|
9
|
+
*
|
|
10
|
+
* The cert is leaf-only (no CA chain), self-signed, and uses the
|
|
11
|
+
* `ecdsa-with-SHA256` signature algorithm over the P-256 curve — the same
|
|
12
|
+
* combination real CAs issue for TLS 1.3 handshakes.
|
|
13
|
+
*/
|
|
14
|
+
/** A self-signed leaf certificate + its private key, both in DER/PEM form. */
|
|
15
|
+
export interface SelfSignedCert {
|
|
16
|
+
/** DER-encoded leaf certificate. */
|
|
17
|
+
readonly certDer: Uint8Array;
|
|
18
|
+
/** PEM-encoded PKCS#8 private key (for `tls.createSecureContext`). */
|
|
19
|
+
readonly keyPem: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function derOid(oid: string): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Generate a self-signed ECDSA P-256 certificate valid for the given SANs.
|
|
24
|
+
*
|
|
25
|
+
* @param dnsNames DNS SANs (e.g. `["localhost"]`).
|
|
26
|
+
* @param ipAddresses IP SANs in dotted-quad form (e.g. `["127.0.0.1"]`).
|
|
27
|
+
* @param validityDays Leaf validity window (default 1 day — sink certs are
|
|
28
|
+
* throwaway).
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateSelfSignedCert(dnsNames?: readonly string[], ipAddresses?: readonly string[], validityDays?: number): SelfSignedCert;
|
|
31
|
+
//# sourceMappingURL=cert-gen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cert-gen.d.ts","sourceRoot":"","sources":["../../src/e2e/cert-gen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAwDD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAkB9C;AA+BD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,GAAE,SAAS,MAAM,EAAkB,EAC3C,WAAW,GAAE,SAAS,MAAM,EAAkB,EAC9C,YAAY,SAAI,GACjB,cAAc,CAoEhB"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-signed ECDSA P-256 certificate generator for the E2E TLS sink server.
|
|
3
|
+
*
|
|
4
|
+
* Builds a minimal X.509v3 certificate DER at runtime via `node:crypto` — no
|
|
5
|
+
* `openssl` binary, no external deps. Mirrors the DER-builder pattern from
|
|
6
|
+
* `tls/tests/server-sim.ts` but adds a SubjectAlternativeName extension so
|
|
7
|
+
* the cert is valid for both `127.0.0.1` (IP) and `localhost` (DNS), which is
|
|
8
|
+
* what the sink's consumers need.
|
|
9
|
+
*
|
|
10
|
+
* The cert is leaf-only (no CA chain), self-signed, and uses the
|
|
11
|
+
* `ecdsa-with-SHA256` signature algorithm over the P-256 curve — the same
|
|
12
|
+
* combination real CAs issue for TLS 1.3 handshakes.
|
|
13
|
+
*/
|
|
14
|
+
import { createSign, generateKeyPairSync } from "node:crypto";
|
|
15
|
+
// --- DER builders -------------------------------------------------------
|
|
16
|
+
// All builders return a DER-encoded TLV. Tag classes are universal unless noted.
|
|
17
|
+
function concatBytes(...chunks) {
|
|
18
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
19
|
+
const out = new Uint8Array(total);
|
|
20
|
+
let o = 0;
|
|
21
|
+
for (const c of chunks) {
|
|
22
|
+
out.set(c, o);
|
|
23
|
+
o += c.length;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Encode a DER length (definite short/long form up to 65535). */
|
|
28
|
+
function derLength(length) {
|
|
29
|
+
if (length < 0x80)
|
|
30
|
+
return new Uint8Array([length]);
|
|
31
|
+
if (length < 0x100)
|
|
32
|
+
return new Uint8Array([0x81, length]);
|
|
33
|
+
return new Uint8Array([0x82, (length >> 8) & 0xff, length & 0xff]);
|
|
34
|
+
}
|
|
35
|
+
/** Wrap content in a DER TLV with the given tag. */
|
|
36
|
+
function derTagged(tag, content) {
|
|
37
|
+
return concatBytes(new Uint8Array([tag]), derLength(content.length), content);
|
|
38
|
+
}
|
|
39
|
+
function derSequence(...parts) {
|
|
40
|
+
return derTagged(0x30, concatBytes(...parts));
|
|
41
|
+
}
|
|
42
|
+
function derSet(...parts) {
|
|
43
|
+
return derTagged(0x31, concatBytes(...parts));
|
|
44
|
+
}
|
|
45
|
+
function derNull() {
|
|
46
|
+
return new Uint8Array([0x05, 0x00]);
|
|
47
|
+
}
|
|
48
|
+
function derBooleanTrue() {
|
|
49
|
+
return new Uint8Array([0x01, 0x01, 0xff]);
|
|
50
|
+
}
|
|
51
|
+
function derInteger(value) {
|
|
52
|
+
const needsPad = value.length > 0 && (value[0] & 0x80) !== 0;
|
|
53
|
+
return derTagged(0x02, needsPad ? concatBytes(new Uint8Array([0x00]), value) : value);
|
|
54
|
+
}
|
|
55
|
+
function derBitString(content) {
|
|
56
|
+
return derTagged(0x03, concatBytes(new Uint8Array([0x00]), content));
|
|
57
|
+
}
|
|
58
|
+
function derOctetString(content) {
|
|
59
|
+
return derTagged(0x04, content);
|
|
60
|
+
}
|
|
61
|
+
function derUtf8String(bytes) {
|
|
62
|
+
return derTagged(0x0c, bytes);
|
|
63
|
+
}
|
|
64
|
+
function derGeneralizedTime(text) {
|
|
65
|
+
return derTagged(0x18, new TextEncoder().encode(text));
|
|
66
|
+
}
|
|
67
|
+
export function derOid(oid) {
|
|
68
|
+
const parts = oid.split(".").map((p) => Number.parseInt(p, 10));
|
|
69
|
+
const first = parts[0] * 40 + parts[1];
|
|
70
|
+
const rest = [];
|
|
71
|
+
for (const arc of parts.slice(2)) {
|
|
72
|
+
if (arc === 0) {
|
|
73
|
+
rest.push(0);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const bytes = [];
|
|
77
|
+
let value = arc;
|
|
78
|
+
while (value > 0) {
|
|
79
|
+
bytes.unshift((value & 0x7f) | (bytes.length === 0 ? 0 : 0x80));
|
|
80
|
+
value >>= 7;
|
|
81
|
+
}
|
|
82
|
+
rest.push(...bytes);
|
|
83
|
+
}
|
|
84
|
+
return derTagged(0x06, new Uint8Array([first, ...rest]));
|
|
85
|
+
}
|
|
86
|
+
/** Context-specific primitive: `[n] IMPLICIT OCTET STRING`. */
|
|
87
|
+
function derImplicitOctetString(tag, content) {
|
|
88
|
+
return derTagged(0x80 | tag, content);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Context-specific explicit wrapper: `[n] EXPLICIT content`.
|
|
92
|
+
*
|
|
93
|
+
* The content is wrapped as-is (not re-sequenced), so callers control whether
|
|
94
|
+
* the payload is a bare INTEGER (version `[0]`) or a SEQUENCE (extensions
|
|
95
|
+
* `[3]`).
|
|
96
|
+
*/
|
|
97
|
+
function derExplicitTagged(tag, content) {
|
|
98
|
+
return derTagged(0xa0 | tag, content);
|
|
99
|
+
}
|
|
100
|
+
// --- OIDs (avoid magic strings) ----------------------------------------
|
|
101
|
+
const OID = {
|
|
102
|
+
ecdsaWithSHA256: "1.2.840.10045.4.3.2",
|
|
103
|
+
commonName: "2.5.4.3",
|
|
104
|
+
subjectAltName: "2.5.29.17",
|
|
105
|
+
keyUsage: "2.5.29.15",
|
|
106
|
+
basicConstraints: "2.5.29.19",
|
|
107
|
+
};
|
|
108
|
+
/** Format a Date as a DER GeneralizedTime string: `YYYYMMDDHHMMSSZ`. */
|
|
109
|
+
function toGeneralizedTime(d) {
|
|
110
|
+
return d.toISOString().replace(/[-:T]/g, "").slice(0, 14) + "Z";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate a self-signed ECDSA P-256 certificate valid for the given SANs.
|
|
114
|
+
*
|
|
115
|
+
* @param dnsNames DNS SANs (e.g. `["localhost"]`).
|
|
116
|
+
* @param ipAddresses IP SANs in dotted-quad form (e.g. `["127.0.0.1"]`).
|
|
117
|
+
* @param validityDays Leaf validity window (default 1 day — sink certs are
|
|
118
|
+
* throwaway).
|
|
119
|
+
*/
|
|
120
|
+
export function generateSelfSignedCert(dnsNames = ["localhost"], ipAddresses = ["127.0.0.1"], validityDays = 1) {
|
|
121
|
+
const { publicKey, privateKey } = generateKeyPairSync("ec", {
|
|
122
|
+
namedCurve: "P-256",
|
|
123
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
124
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
125
|
+
});
|
|
126
|
+
const spki = new Uint8Array(publicKey);
|
|
127
|
+
const notBefore = new Date();
|
|
128
|
+
const notAfter = new Date(notBefore.getTime() + validityDays * 24 * 60 * 60 * 1000);
|
|
129
|
+
// Subject / issuer: CN=localhost (self-signed ⇒ issuer === subject).
|
|
130
|
+
const cnValue = derUtf8String(new TextEncoder().encode("localhost"));
|
|
131
|
+
const name = derSequence(derSet(derSequence(derOid(OID.commonName), cnValue)));
|
|
132
|
+
// SubjectAlternativeName entries: [2] DNS name, [7] raw IP bytes.
|
|
133
|
+
const sanEntries = [];
|
|
134
|
+
for (const dns of dnsNames) {
|
|
135
|
+
sanEntries.push(derImplicitOctetString(2, new TextEncoder().encode(dns)));
|
|
136
|
+
}
|
|
137
|
+
for (const ip of ipAddresses) {
|
|
138
|
+
const bytes = ip.split(".").map((o) => Number.parseInt(o, 10));
|
|
139
|
+
sanEntries.push(derImplicitOctetString(7, new Uint8Array(bytes)));
|
|
140
|
+
}
|
|
141
|
+
const sanExt = derSequence(derOid(OID.subjectAltName), derOctetString(derSequence(...sanEntries)));
|
|
142
|
+
// KeyUsage: digitalSignature (bit 0) — critical.
|
|
143
|
+
const kuExt = derSequence(derOid(OID.keyUsage), derBooleanTrue(), derOctetString(new Uint8Array([0x03, 0x02, 0x07, 0x80])));
|
|
144
|
+
// BasicConstraints: CA=false — critical.
|
|
145
|
+
const bcExt = derSequence(derOid(OID.basicConstraints), derBooleanTrue(), derOctetString(derSequence()));
|
|
146
|
+
const serial = derInteger(new Uint8Array([0x01]));
|
|
147
|
+
const sigAlg = derSequence(derOid(OID.ecdsaWithSHA256), derNull());
|
|
148
|
+
const validity = derSequence(derGeneralizedTime(toGeneralizedTime(notBefore)), derGeneralizedTime(toGeneralizedTime(notAfter)));
|
|
149
|
+
const tbs = derSequence(derExplicitTagged(0, derInteger(new Uint8Array([0x02]))), // [0] version = v3
|
|
150
|
+
serial, sigAlg, name, validity, name, spki, derExplicitTagged(3, derSequence(sanExt, kuExt, bcExt)));
|
|
151
|
+
// Self-sign with SHA256 over the TBS DER.
|
|
152
|
+
const signer = createSign("SHA256");
|
|
153
|
+
signer.update(Buffer.from(tbs));
|
|
154
|
+
const signature = new Uint8Array(signer.sign({ key: privateKey, dsaEncoding: "der" }));
|
|
155
|
+
const certDer = derSequence(concatBytes(tbs, sigAlg, derBitString(signature)));
|
|
156
|
+
return { certDer, keyPem: privateKey };
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=cert-gen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cert-gen.js","sourceRoot":"","sources":["../../src/e2e/cert-gen.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal TLS 1.2+ ClientHello parser for the E2E TLS sink server.
|
|
3
|
+
*
|
|
4
|
+
* Parses the wire bytes a sink captured *before* TLS parsing, exposing the
|
|
5
|
+
* fields the e2e test matrix (T2–T9 in the plan) asserts on: cipher suites,
|
|
6
|
+
* extension types / order, ALPN protocols, SNI hostname, supported_versions.
|
|
7
|
+
*
|
|
8
|
+
* The parser is intentionally lenient about trailing data — it reads only the
|
|
9
|
+
* ClientHello body and stops. It does NOT validate cryptographic integrity;
|
|
10
|
+
* that is the handshake's job. All offsets are computed dynamically from the
|
|
11
|
+
* parsed structure (never hardcoded) so the same code works whether the input
|
|
12
|
+
* is a bare handshake message or a full TLS record (the caller strips the
|
|
13
|
+
* 5-byte record header via {@link parseClientHello}).
|
|
14
|
+
*
|
|
15
|
+
* This module merges the original minimal parser (shipped on `main`) with the
|
|
16
|
+
* E2E TLS sink server infrastructure (from `feat/e2e-sink`): the `EXT` constant
|
|
17
|
+
* table, `ClientHelloParseError`, richer offset/length metadata on
|
|
18
|
+
* {@link ParsedClientHello}, and GREASE utilities from the sink branch are
|
|
19
|
+
* retained alongside the `decodeSni`, `decodeAlpn`, `decodeSupportedVersions`
|
|
20
|
+
* accessors from `main`.
|
|
21
|
+
*/
|
|
22
|
+
/** A single parsed extension: type wire value + raw data bytes. */
|
|
23
|
+
export interface ParsedExtension {
|
|
24
|
+
/** Extension type (IANA wire value, e.g. 0x0000 for SNI). */
|
|
25
|
+
readonly type: number;
|
|
26
|
+
/** Length of `data` on the wire (excludes type + length prefix). */
|
|
27
|
+
readonly length: number;
|
|
28
|
+
/** Raw extension data (empty for zero-length extensions). */
|
|
29
|
+
readonly data: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fully parsed ClientHello.
|
|
33
|
+
*
|
|
34
|
+
* Byte offsets are captured so tests can map a field back to its wire position
|
|
35
|
+
* when reporting divergence (without hardcoding offsets). `cipherSuites` and
|
|
36
|
+
* `compressionMethods` are exposed as flat numeric arrays for direct
|
|
37
|
+
* iteration / `.filter()` / `.some()` consumption (see the e2e ClientHello
|
|
38
|
+
* suite on `main`).
|
|
39
|
+
*/
|
|
40
|
+
export interface ParsedClientHello {
|
|
41
|
+
/** Starting offset of the handshake type byte within the input. */
|
|
42
|
+
readonly handshakeTypeOffset: number;
|
|
43
|
+
/** Handshake type — always 0x01 (ClientHello). */
|
|
44
|
+
readonly handshakeType: number;
|
|
45
|
+
/** Handshake body length (24-bit). */
|
|
46
|
+
readonly handshakeLength: number;
|
|
47
|
+
/** Legacy protocol version (usually 0x0303). */
|
|
48
|
+
readonly legacyVersion: number;
|
|
49
|
+
/** 32-byte client random. */
|
|
50
|
+
readonly random: Uint8Array;
|
|
51
|
+
/** Session ID (empty when not resuming). */
|
|
52
|
+
readonly sessionId: Uint8Array;
|
|
53
|
+
/** Offered cipher suites as IANA 2-byte codes, in wire order (including GREASE). */
|
|
54
|
+
readonly cipherSuites: readonly number[];
|
|
55
|
+
/** Compression methods (variable length, usually [0x00]). */
|
|
56
|
+
readonly compressionMethods: readonly number[];
|
|
57
|
+
/** Extensions in wire order. Empty if the client sent none. */
|
|
58
|
+
readonly extensions: readonly ParsedExtension[];
|
|
59
|
+
/** Total bytes consumed from the input (handshake type + length + body). */
|
|
60
|
+
readonly totalLength: number;
|
|
61
|
+
}
|
|
62
|
+
export declare const EXT: {
|
|
63
|
+
readonly SERVER_NAME: 0;
|
|
64
|
+
readonly SUPPORTED_VERSIONS: 43;
|
|
65
|
+
readonly APPLICATION_LAYER_PROTOCOL_NEGOTIATION: 16;
|
|
66
|
+
readonly KEY_SHARE: 51;
|
|
67
|
+
readonly SUPPORTED_GROUPS: 10;
|
|
68
|
+
readonly PSK_KEY_EXCHANGE_MODES: 45;
|
|
69
|
+
readonly SIGNATURE_ALGORITHMS: 13;
|
|
70
|
+
};
|
|
71
|
+
export type ExtensionTypeValue = (typeof EXT)[keyof typeof EXT];
|
|
72
|
+
/** Thrown when the input is too short to be a valid ClientHello. */
|
|
73
|
+
export declare class ClientHelloParseError extends Error {
|
|
74
|
+
readonly kind: "ClientHelloParseError";
|
|
75
|
+
readonly byteOffset: number;
|
|
76
|
+
readonly cause: Error | undefined;
|
|
77
|
+
constructor(message: string, byteOffset: number, options?: {
|
|
78
|
+
cause?: Error;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a TLS record header. Returns the offset of the handshake message that
|
|
83
|
+
* follows (i.e. 5 for a well-formed record) or `null` if the input does not
|
|
84
|
+
* start with a Handshake record (content type 0x16).
|
|
85
|
+
*
|
|
86
|
+
* Callers that already stripped the record header can pass the raw handshake
|
|
87
|
+
* message directly to {@link parseClientHello}.
|
|
88
|
+
*/
|
|
89
|
+
export declare function peekRecordHeader(buf: Uint8Array): number | null;
|
|
90
|
+
/**
|
|
91
|
+
* Parse a ClientHello from raw wire bytes.
|
|
92
|
+
*
|
|
93
|
+
* Accepts either a full TLS Handshake record (5-byte header + handshake
|
|
94
|
+
* message) or a bare handshake message (type + 24-bit length + body). The
|
|
95
|
+
* function auto-detects the record header by content type.
|
|
96
|
+
*
|
|
97
|
+
* Distinct from the JA3 `parseClientHello` in `@browsercore/testing`'s
|
|
98
|
+
* fingerprint module (which returns JA3 segments). This parser returns the
|
|
99
|
+
* full structural breakdown (cipher suites, extensions, SNI, ALPN, etc.) for
|
|
100
|
+
* the e2e test matrix.
|
|
101
|
+
*
|
|
102
|
+
* @param buf Raw bytes as seen on the wire.
|
|
103
|
+
* @throws {ClientHelloParseError} if the input is malformed or truncated.
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseClientHello(buf: Uint8Array): ParsedClientHello;
|
|
106
|
+
/**
|
|
107
|
+
* Wire-compatible alias of {@link parseClientHello}. Exported for callers that
|
|
108
|
+
* prefer the `parseClientHelloWire` name used by the E2E TLS sink server.
|
|
109
|
+
*/
|
|
110
|
+
export declare const parseClientHelloWire: typeof parseClientHello;
|
|
111
|
+
/**
|
|
112
|
+
* Find the first extension of the given type. Returns `undefined` if absent.
|
|
113
|
+
*/
|
|
114
|
+
export declare function findExtension(hello: ParsedClientHello, type: number): ParsedExtension | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Parse the SNI extension (type 0x0000) and return the first hostname.
|
|
117
|
+
*
|
|
118
|
+
* Layout: server_name_list_len(2) + entries. Each entry: name_type(1) +
|
|
119
|
+
* name_len(2) + name. name_type 0 = host_name.
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseSniHostname(hello: ParsedClientHello): string | null;
|
|
122
|
+
/**
|
|
123
|
+
* Parse the ALPN extension (type 0x0010) and return the offered protocols in
|
|
124
|
+
* wire order.
|
|
125
|
+
*
|
|
126
|
+
* Layout: protocol_list_len(2) + entries. Each entry: name_len(1) + name.
|
|
127
|
+
*/
|
|
128
|
+
export declare function parseAlpnProtocols(hello: ParsedClientHello): readonly string[];
|
|
129
|
+
/**
|
|
130
|
+
* Parse the supported_versions extension (type 0x002b) and return the offered
|
|
131
|
+
* versions in wire order.
|
|
132
|
+
*
|
|
133
|
+
* RFC 8446 §4.2.1.1: the client body is a 1-byte length-prefixed list of
|
|
134
|
+
* uint16 wire versions (`versions<2..254>`). The length byte counts the
|
|
135
|
+
* number of *bytes* of version data (always even), so the number of versions
|
|
136
|
+
* is `length / 2`.
|
|
137
|
+
*/
|
|
138
|
+
export declare function parseSupportedVersions(hello: ParsedClientHello): readonly number[];
|
|
139
|
+
/**
|
|
140
|
+
* Decode the SNI server_name_list (extension 0x0000) into hostnames.
|
|
141
|
+
*
|
|
142
|
+
* RFC 6066 §3: server_name_list = length(2) || entries, each entry =
|
|
143
|
+
* name_type(1) || length(2) || name. We only handle name_type=0 (host_name).
|
|
144
|
+
*
|
|
145
|
+
* Returns all hostnames in the list. Use {@link parseSniHostname} for the
|
|
146
|
+
* first one only.
|
|
147
|
+
*/
|
|
148
|
+
export declare function decodeSni(hello: ParsedClientHello): readonly string[];
|
|
149
|
+
/**
|
|
150
|
+
* Decode the supported_versions extension (0x002b) for a client ClientHello.
|
|
151
|
+
*
|
|
152
|
+
* RFC 8446 §4.2.1: the client body is a length-prefixed list of uint16 wire
|
|
153
|
+
* versions. Returns the versions in wire order.
|
|
154
|
+
*
|
|
155
|
+
* Compatible alias of {@link parseSupportedVersions}.
|
|
156
|
+
*/
|
|
157
|
+
export declare function decodeSupportedVersions(hello: ParsedClientHello): readonly number[];
|
|
158
|
+
/**
|
|
159
|
+
* Decode the ALPN extension (0x0010) into the offered protocol names.
|
|
160
|
+
*
|
|
161
|
+
* RFC 7301: the body is a length-prefixed list of length-prefixed UTF-8 names.
|
|
162
|
+
*
|
|
163
|
+
* Compatible alias of {@link parseAlpnProtocols}.
|
|
164
|
+
*/
|
|
165
|
+
export declare function decodeAlpn(hello: ParsedClientHello): readonly string[];
|
|
166
|
+
/**
|
|
167
|
+
* The 16 GREASE sentinel values per RFC 8701: both bytes identical, low nibble
|
|
168
|
+
* 0xA. Used to detect / strip GREASE from cipher + extension assertions.
|
|
169
|
+
*/
|
|
170
|
+
export declare const GREASE_SENTINELS: readonly number[];
|
|
171
|
+
/** True if the given 16-bit wire value is a GREASE sentinel. */
|
|
172
|
+
export declare function isGrease(value: number): boolean;
|
|
173
|
+
/** Return the cipher suites with GREASE sentinels removed. */
|
|
174
|
+
export declare function nonGreaseCipherSuites(hello: ParsedClientHello): readonly number[];
|
|
175
|
+
/** Return the extension types with GREASE sentinels removed. */
|
|
176
|
+
export declare function nonGreaseExtensionTypes(hello: ParsedClientHello): readonly number[];
|
|
177
|
+
//# sourceMappingURL=parse-clienthello.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-clienthello.d.ts","sourceRoot":"","sources":["../../src/e2e/parse-clienthello.ts"],"names":[],"mappings":"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"}
|