@browsercore/testing 0.1.3 → 0.2.0

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 (174) hide show
  1. package/README.md +5 -0
  2. package/captures/DIVERGENCE_REPORT.md +325 -0
  3. package/captures/IMPERSIONATION_GAPS_REPORT.md +367 -0
  4. package/captures/README.md +56 -0
  5. package/captures/REVIEW_FINDINGS.md +71 -0
  6. package/captures/_probe/analyze.py +67 -0
  7. package/captures/_probe/capture_bytes.py +127 -0
  8. package/captures/_probe/chrome131_determinism.py +32 -0
  9. package/captures/_probe/diagnose_capture.py +59 -0
  10. package/captures/_probe/golden_analysis.py +91 -0
  11. package/captures/_probe/inspect_golden.py +55 -0
  12. package/captures/_probe/inspect_sv.py +16 -0
  13. package/captures/_probe/make_golden.py +170 -0
  14. package/captures/_probe/output/chrome100.json +219 -0
  15. package/captures/_probe/output/chrome101.json +219 -0
  16. package/captures/_probe/output/chrome104.json +219 -0
  17. package/captures/_probe/output/chrome107.json +220 -0
  18. package/captures/_probe/output/chrome110.json +220 -0
  19. package/captures/_probe/output/chrome116.json +220 -0
  20. package/captures/_probe/output/chrome119.json +219 -0
  21. package/captures/_probe/output/chrome120.json +219 -0
  22. package/captures/_probe/output/chrome123.json +223 -0
  23. package/captures/_probe/output/chrome124.json +224 -0
  24. package/captures/_probe/output/chrome131.json +224 -0
  25. package/captures/_probe/output/chrome131_fresh.json +224 -0
  26. package/captures/_probe/output/chrome133a.json +224 -0
  27. package/captures/_probe/output/chrome136.json +224 -0
  28. package/captures/_probe/output/chrome99.json +219 -0
  29. package/captures/_probe/output/edge101.json +219 -0
  30. package/captures/_probe/output/edge99.json +219 -0
  31. package/captures/_probe/output/firefox133.json +227 -0
  32. package/captures/_probe/output/firefox135.json +230 -0
  33. package/captures/_probe/output/safari155.json +210 -0
  34. package/captures/_probe/output/safari170.json +214 -0
  35. package/captures/_probe/output/safari170_fresh.json +214 -0
  36. package/captures/_probe/output/safari180.json +216 -0
  37. package/captures/_probe/output/safari180_ios.json +216 -0
  38. package/captures/_probe/output/safari184.json +215 -0
  39. package/captures/_probe/output/safari260.json +211 -0
  40. package/captures/_probe/output/summary.json +2520 -0
  41. package/captures/_probe/parse_existing_settings.py +15 -0
  42. package/captures/_probe/peetprint.py +39 -0
  43. package/captures/_probe/probe.py +137 -0
  44. package/captures/_probe/raw_bytes/chrome-131_client_hello.bin +0 -0
  45. package/captures/_probe/raw_bytes/chrome-131_client_hello.hex +32 -0
  46. package/captures/_probe/raw_bytes/firefox-133_client_hello.bin +0 -0
  47. package/captures/_probe/raw_bytes/firefox-133_client_hello.hex +32 -0
  48. package/captures/_probe/raw_bytes/safari-17_client_hello.bin +0 -0
  49. package/captures/_probe/raw_bytes/safari-17_client_hello.hex +32 -0
  50. package/captures/_probe/relay_capture.py +137 -0
  51. package/captures/_probe/socks_capture.py +138 -0
  52. package/captures/_probe/validate_fresh.py +66 -0
  53. package/captures/_probe/validate_golden.py +97 -0
  54. package/captures/_probe/verify_capture.py +68 -0
  55. package/captures/chrome-131/http2/settings.bin +0 -0
  56. package/captures/chrome-131/http2/settings.meta.json +9 -0
  57. package/captures/chrome-131/oracle_capture.reference.json +224 -0
  58. package/captures/chrome-131/tls/client_hello.bin +0 -0
  59. package/captures/chrome-131/tls/client_hello.meta.json +20 -0
  60. package/captures/chrome-140/http2/settings.bin +0 -0
  61. package/captures/chrome-140/http2/settings.meta.json +9 -0
  62. package/captures/chrome-140/tls/client_hello.bin +0 -0
  63. package/captures/chrome-140/tls/client_hello.meta.json +12 -0
  64. package/captures/firefox-128/tls/client_hello.bin +0 -0
  65. package/captures/firefox-128/tls/client_hello.meta.json +12 -0
  66. package/captures/firefox-133/http2/settings.bin +0 -0
  67. package/captures/firefox-133/http2/settings.meta.json +9 -0
  68. package/captures/firefox-133/oracle_capture.reference.json +227 -0
  69. package/captures/firefox-133/tls/client_hello.bin +0 -0
  70. package/captures/firefox-133/tls/client_hello.meta.json +20 -0
  71. package/captures/safari-17/http2/settings.bin +0 -0
  72. package/captures/safari-17/http2/settings.meta.json +9 -0
  73. package/captures/safari-17/oracle_capture.reference.json +214 -0
  74. package/captures/safari-17/tls/client_hello.bin +0 -0
  75. package/captures/safari-17/tls/client_hello.meta.json +20 -0
  76. package/dist/bench/bench.d.ts +22 -6
  77. package/dist/bench/bench.d.ts.map +1 -1
  78. package/dist/bench/bench.js +105 -10
  79. package/dist/bench/bench.js.map +1 -1
  80. package/dist/captures/manifest.d.ts.map +1 -1
  81. package/dist/captures/manifest.js +2 -3
  82. package/dist/captures/manifest.js.map +1 -1
  83. package/dist/categories/cookies.d.ts +3 -2
  84. package/dist/categories/cookies.d.ts.map +1 -1
  85. package/dist/categories/cookies.js +228 -11
  86. package/dist/categories/cookies.js.map +1 -1
  87. package/dist/categories/redirects.d.ts +8 -2
  88. package/dist/categories/redirects.d.ts.map +1 -1
  89. package/dist/categories/redirects.js +97 -12
  90. package/dist/categories/redirects.js.map +1 -1
  91. package/dist/e2e/cert-gen.d.ts +30 -0
  92. package/dist/e2e/cert-gen.d.ts.map +1 -0
  93. package/dist/e2e/cert-gen.js +158 -0
  94. package/dist/e2e/cert-gen.js.map +1 -0
  95. package/dist/e2e/parse-clienthello.d.ts +177 -0
  96. package/dist/e2e/parse-clienthello.d.ts.map +1 -0
  97. package/dist/e2e/parse-clienthello.js +343 -0
  98. package/dist/e2e/parse-clienthello.js.map +1 -0
  99. package/dist/e2e/sink-server.d.ts +109 -0
  100. package/dist/e2e/sink-server.d.ts.map +1 -0
  101. package/dist/e2e/sink-server.js +292 -0
  102. package/dist/e2e/sink-server.js.map +1 -0
  103. package/dist/errors.d.ts.map +1 -1
  104. package/dist/errors.js.map +1 -1
  105. package/dist/fingerprint/index.d.ts +5 -2
  106. package/dist/fingerprint/index.d.ts.map +1 -1
  107. package/dist/fingerprint/index.js +3 -2
  108. package/dist/fingerprint/index.js.map +1 -1
  109. package/dist/fingerprint/ja3.d.ts.map +1 -1
  110. package/dist/fingerprint/ja3.js +33 -4
  111. package/dist/fingerprint/ja3.js.map +1 -1
  112. package/dist/fingerprint/ja4-errors.d.ts +17 -0
  113. package/dist/fingerprint/ja4-errors.d.ts.map +1 -0
  114. package/dist/fingerprint/ja4-errors.js +20 -0
  115. package/dist/fingerprint/ja4-errors.js.map +1 -0
  116. package/dist/fingerprint/ja4-reader.d.ts +44 -0
  117. package/dist/fingerprint/ja4-reader.d.ts.map +1 -0
  118. package/dist/fingerprint/ja4-reader.js +112 -0
  119. package/dist/fingerprint/ja4-reader.js.map +1 -0
  120. package/dist/fingerprint/ja4.d.ts +11 -43
  121. package/dist/fingerprint/ja4.d.ts.map +1 -1
  122. package/dist/fingerprint/ja4.js +54 -141
  123. package/dist/fingerprint/ja4.js.map +1 -1
  124. package/dist/fingerprint/ja4h.d.ts +46 -0
  125. package/dist/fingerprint/ja4h.d.ts.map +1 -0
  126. package/dist/fingerprint/ja4h.js +50 -0
  127. package/dist/fingerprint/ja4h.js.map +1 -0
  128. package/dist/golden/golden.d.ts +15 -1
  129. package/dist/golden/golden.d.ts.map +1 -1
  130. package/dist/golden/golden.js +35 -22
  131. package/dist/golden/golden.js.map +1 -1
  132. package/dist/index.d.ts +17 -1
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +26 -1
  135. package/dist/index.js.map +1 -1
  136. package/dist/reference/browser-provider.d.ts +33 -0
  137. package/dist/reference/browser-provider.d.ts.map +1 -0
  138. package/dist/reference/browser-provider.js +60 -0
  139. package/dist/reference/browser-provider.js.map +1 -0
  140. package/dist/reference/curl-provider.d.ts +40 -0
  141. package/dist/reference/curl-provider.d.ts.map +1 -0
  142. package/dist/reference/curl-provider.js +96 -0
  143. package/dist/reference/curl-provider.js.map +1 -0
  144. package/dist/reference/dump.d.ts +42 -0
  145. package/dist/reference/dump.d.ts.map +1 -0
  146. package/dist/reference/dump.js +102 -0
  147. package/dist/reference/dump.js.map +1 -0
  148. package/dist/reference/node-reference.d.ts.map +1 -1
  149. package/dist/reference/node-reference.js +2 -5
  150. package/dist/reference/node-reference.js.map +1 -1
  151. package/dist/reference/reference-errors.d.ts +36 -0
  152. package/dist/reference/reference-errors.d.ts.map +1 -0
  153. package/dist/reference/reference-errors.js +43 -0
  154. package/dist/reference/reference-errors.js.map +1 -0
  155. package/dist/reference/reference-types.d.ts +33 -0
  156. package/dist/reference/reference-types.d.ts.map +1 -0
  157. package/dist/reference/reference-types.js +9 -0
  158. package/dist/reference/reference-types.js.map +1 -0
  159. package/dist/reference/reference.d.ts +14 -77
  160. package/dist/reference/reference.d.ts.map +1 -1
  161. package/dist/reference/reference.js +43 -207
  162. package/dist/reference/reference.js.map +1 -1
  163. package/dist/rfc/rfcTests.d.ts +46 -8
  164. package/dist/rfc/rfcTests.d.ts.map +1 -1
  165. package/dist/rfc/rfcTests.js +203 -16
  166. package/dist/rfc/rfcTests.js.map +1 -1
  167. package/dist/types.d.ts +2 -1
  168. package/dist/types.d.ts.map +1 -1
  169. package/dist/types.js.map +1 -1
  170. package/dist/utils.d.ts +3 -0
  171. package/dist/utils.d.ts.map +1 -1
  172. package/dist/utils.js +11 -5
  173. package/dist/utils.js.map +1 -1
  174. package/package.json +61 -55
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Validate reconstructed SETTINGS frames + meta.json schema compliance."""
3
+ import os, json, struct, sys
4
+
5
+ # Reuse the library's meta.json schema check (parseCaptureMeta) if importable,
6
+ # else replicate minimally.
7
+ sys.path.insert(0, "/Users/matte/projects/browsercore/testing/src")
8
+ try:
9
+ from golden.golden import parseCaptureMeta
10
+ HAS_LIB = True
11
+ except Exception as e:
12
+ HAS_LIB = False
13
+ print(f"Note: could not import library golden loader ({e}); using local schema check")
14
+
15
+ CAPTURES = "/Users/matte/projects/browsercore/testing/captures"
16
+ ORACLE_OUT = os.path.join(CAPTURES, "_probe", "output")
17
+
18
+ SETTING_IDS_REV = {1:"HEADER_TABLE_SIZE",2:"ENABLE_PUSH",3:"MAX_CONCURRENT_STREAMS",
19
+ 4:"INITIAL_WINDOW_SIZE",5:"MAX_FRAME_SIZE",6:"MAX_HEADER_LIST_SIZE",
20
+ 8:"ENABLE_CONNECT_PROTOCOL",9:"NO_RFC7540_PRIORITIES"}
21
+
22
+ def parse_settings(data):
23
+ # HTTP/2 frame: length(3)+type(1)+flags(1)+stream(4)
24
+ assert len(data) >= 9, "settings frame too short"
25
+ length = struct.unpack(">I", b'\x00'+data[0:3])[0]
26
+ ftype = data[3]
27
+ flags = data[4]
28
+ stream = struct.unpack(">I", data[5:9])[0]
29
+ assert ftype == 0x04, f"expected SETTINGS (0x04) got 0x{ftype:02x}"
30
+ assert stream == 0, "SETTINGS must be on stream 0"
31
+ payload = data[9:]
32
+ assert len(payload) == length, f"payload len {len(payload)} != frame length {length}"
33
+ settings = []
34
+ for i in range(0, len(payload), 6):
35
+ sid = struct.unpack(">H", payload[i:i+2])[0]
36
+ val = struct.unpack(">I", payload[i+2:i+6])[0]
37
+ settings.append((SETTING_IDS_REV.get(sid, f"UNKNOWN({sid})"), val))
38
+ return {"flags": flags, "settings": settings}
39
+
40
+ def load_oracle_settings(oracle_name):
41
+ for suffix in ["", "_fresh"]:
42
+ p = os.path.join(ORACLE_OUT, f"{oracle_name}{suffix}.json")
43
+ if os.path.exists(p):
44
+ with open(p) as f: return json.load(f)
45
+ return None
46
+
47
+ VALID_RECORDS = {"client_hello","settings","headers","server_hello"}
48
+ VALID_PROTOCOLS = {"tls","http2","http1","tcp"}
49
+ VALID_SOURCES = {"curl-impersonate","real-browser"}
50
+ VALID_REASONS = {"ephemeral_key","nonce","grease","random"}
51
+
52
+ def local_validate_meta(meta, path):
53
+ assert meta["source"] in VALID_SOURCES, f"bad source {meta['source']}"
54
+ assert meta["protocol"] in VALID_PROTOCOLS, f"bad protocol {meta['protocol']}"
55
+ assert meta["record"] in VALID_RECORDS, f"bad record {meta['record']}"
56
+ assert isinstance(meta["description"], str)
57
+ assert isinstance(meta["createdAt"], str)
58
+ assert isinstance(meta["profile"], str)
59
+ for rf in meta["randomizedFields"]:
60
+ assert rf["reason"] in VALID_REASONS
61
+ assert isinstance(rf["byteOffset"], int) and rf["byteOffset"] >= 0
62
+ assert isinstance(rf["length"], int) and rf["length"] >= 0
63
+
64
+ PROFILES = [("chrome-131","chrome131"), ("firefox-133","firefox133"), ("safari-17","safari170")]
65
+ all_ok = True
66
+ for gname, oracle in PROFILES:
67
+ print(f"\n=== {gname} ===")
68
+ # Validate meta.json schema (both local and library if available)
69
+ for proto in ["tls","http2"]:
70
+ mp = os.path.join(CAPTURES, gname, proto, "client_hello.meta.json" if proto=="tls" else "settings.meta.json")
71
+ with open(mp) as f: meta = json.load(f)
72
+ try:
73
+ local_validate_meta(meta, mp)
74
+ if HAS_LIB:
75
+ parseCaptureMeta(meta, f"{gname}/{proto}/{'client_hello' if proto=='tls' else 'settings'}")
76
+ print(f" {proto} meta.json: SCHEMA OK")
77
+ except Exception as e:
78
+ print(f" {proto} meta.json: SCHEMA FAIL: {e}")
79
+ all_ok = False
80
+ # Validate settings frame
81
+ sp = os.path.join(CAPTURES, gname, "http2", "settings.bin")
82
+ with open(sp,"rb") as f: sdata=f.read()
83
+ parsed = parse_settings(sdata)
84
+ od = load_oracle_settings(oracle)
85
+ oracle_settings = od["http2"]["sent_frames"][0]["settings"] # first SETTINGS
86
+ # Compare
87
+ oracle_pairs = [tuple(s.split(" = ",1)) for s in oracle_settings]
88
+ parsed_pairs = [(k,str(v)) for k,v in parsed["settings"]]
89
+ oracle_pairs_str = [(k,str(v)) for k,v in oracle_pairs]
90
+ if parsed_pairs == oracle_pairs_str:
91
+ print(f" settings.bin: MATCHES oracle ({len(parsed_pairs)} settings)")
92
+ else:
93
+ print(f" settings.bin: MISMATCH")
94
+ print(f" parsed: {parsed_pairs}")
95
+ print(f" oracle: {oracle_pairs_str}")
96
+ all_ok = False
97
+ print(f"\n{'ALL VALID' if all_ok else 'VALIDATION FAILED'}")
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """Verify captured ClientHello bytes reproduce oracle JA3/JA4."""
3
+ import os, json, hashlib, struct
4
+
5
+ RAW = "/Users/matte/projects/browsercore/testing/captures/_probe/raw_bytes"
6
+ OUT = "/Users/matte/projects/browsercore/testing/captures/_probe/output"
7
+
8
+ # Replicate our library's exact parseClientHello + computeJa3
9
+ def parse_client_hello(d):
10
+ pos = 5 # TLS record wrapper
11
+ assert d[0] == 0x16 and d[5] == 0x01
12
+ handshake_len = struct.unpack(">I", b'\x00'+d[6:9])[0]
13
+ pos = 9
14
+ end = 9 + handshake_len
15
+ version = struct.unpack(">H", d[pos:pos+2])[0]; pos += 2
16
+ pos += 32 # random
17
+ sid_len = d[pos]; pos += 1 + sid_len
18
+ cs_len = struct.unpack(">H", d[pos:pos+2])[0]; pos += 2
19
+ ciphers = []
20
+ for i in range(0, cs_len, 2):
21
+ ciphers.append(struct.unpack(">H", d[pos+i:pos+i+2])[0])
22
+ pos += cs_len
23
+ comp_len = d[pos]; pos += 1 + comp_len
24
+ ext_types=[]; sg=[]; ecpf=[]
25
+ if pos + 2 <= end:
26
+ ext_len = struct.unpack(">H", d[pos:pos+2])[0]; pos += 2
27
+ ext_end = pos + ext_len
28
+ while pos + 4 <= ext_end:
29
+ et = struct.unpack(">H", d[pos:pos+2])[0]
30
+ el = struct.unpack(">H", d[pos+2:pos+4])[0]
31
+ pos += 4
32
+ ext_types.append(et)
33
+ if et == 0x000a and el >= 4:
34
+ ll = struct.unpack(">H", d[pos:pos+2])[0]
35
+ for i in range(0,ll,2): sg.append(struct.unpack(">H", d[pos+2+i:pos+4+i])[0])
36
+ elif et == 0x000b and el >= 1:
37
+ ll = d[pos]
38
+ for i in range(ll): ecpf.append(d[pos+1+i])
39
+ pos += el
40
+ return version, ciphers, ext_types, sg, ecpf
41
+
42
+ def ja3(segments):
43
+ s = ",".join(segments)
44
+ return hashlib.md5(s.encode()).hexdigest(), s
45
+
46
+ MAP = [("chrome-131", "chrome131"), ("firefox-133", "firefox133"), ("safari-17", "safari170")]
47
+
48
+ for cap_name, oracle_name in MAP:
49
+ with open(os.path.join(RAW, f"{cap_name}_client_hello.bin"),"rb") as f:
50
+ d = f.read()
51
+ ver, ciphers, exts, sg, ecpf = parse_client_hello(d)
52
+ # JA3 segments use decimal of GREASE as-is (GREASE values included in JA3 string)
53
+ j3s = [str(ver), "-".join(str(c) for c in ciphers), "-".join(str(e) for e in exts), "-".join(str(g) for g in sg), "-".join(str(f) for f in ecpf)]
54
+ md5, j3str = ja3(j3s)
55
+ # oracle values
56
+ od = json.load(open(os.path.join(OUT, f"{oracle_name}.json")))
57
+ oracle_ja3 = od['tls']['ja3_hash']
58
+ oracle_ja3_str = od['tls']['ja3']
59
+ match = "MATCH" if md5 == oracle_ja3 else "MISMATCH"
60
+ print(f"{cap_name}: captured_ja3={md5} oracle_ja3={oracle_ja3} [{match}]")
61
+ print(f" captured JA3 str: {j3str}")
62
+ print(f" oracle JA3 str: {oracle_ja3_str}")
63
+ print(f" version=0x{ver:04x} ciphers={len(ciphers)} exts={len(exts)}")
64
+ # GREASE check
65
+ grease_exts = [hex(e) for e in exts if (e & 0x0f0f) == 0x0a0a]
66
+ grease_ciphers = [hex(c) for c in ciphers if (c & 0x0f0f) == 0x0a0a]
67
+ print(f" GREASE ciphers: {grease_ciphers} GREASE exts: {grease_exts}")
68
+ print()
@@ -0,0 +1,9 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "chrome-131",
4
+ "protocol": "http2",
5
+ "record": "settings",
6
+ "description": "First HTTP/2 SETTINGS frame sent by curl-impersonate chrome131. HTTP/2 SETTINGS are not randomized, so the ignore-list is empty.",
7
+ "randomizedFields": [],
8
+ "createdAt": "2026-08-04T00:28:49Z"
9
+ }
@@ -0,0 +1,224 @@
1
+ {
2
+ "donate": "Please consider donating to keep this API running. Visit https://tls.peet.ws",
3
+ "ip": "92.218.110.236:59988",
4
+ "http_version": "h2",
5
+ "method": "GET",
6
+ "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
7
+ "tls": {
8
+ "ciphers": [
9
+ "TLS_GREASE (0x4A4A)",
10
+ "TLS_AES_128_GCM_SHA256",
11
+ "TLS_AES_256_GCM_SHA384",
12
+ "TLS_CHACHA20_POLY1305_SHA256",
13
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
14
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
15
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
16
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
17
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
18
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
19
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
20
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
21
+ "TLS_RSA_WITH_AES_128_GCM_SHA256",
22
+ "TLS_RSA_WITH_AES_256_GCM_SHA384",
23
+ "TLS_RSA_WITH_AES_128_CBC_SHA",
24
+ "TLS_RSA_WITH_AES_256_CBC_SHA"
25
+ ],
26
+ "extensions": [
27
+ {
28
+ "name": "TLS_GREASE (0x5a5a)"
29
+ },
30
+ {
31
+ "name": "compress_certificate (27)",
32
+ "algorithms": [
33
+ "brotli (2)"
34
+ ]
35
+ },
36
+ {
37
+ "name": "key_share (51)",
38
+ "shared_keys": [
39
+ {
40
+ "TLS_GREASE (0x2a2a)": "00"
41
+ },
42
+ {
43
+ "X25519MLKEM768 (4588)": "1dccada64a898dea92fce03760679badbb30ded95e9d3c1f9c745b4f8436c31a83c0b7ca40b24483888670bb33c8f604fee6c95549091436808740783090bcf4420f4a6064ae4c52b536c80cb42f8449449999913b5129194a3f7e6b8cddb085aae78b680a61cab901a812a5f53716666aa5b7e8a0e0ab3635f75918f430cc0c18c0904d628522c5960ba7ec24f23214bf691d2060affeb51ef4b452779ab4d4b32f1e7943c74043d4abbf0c038d7ac52e90b1b9276545cf5022f4565e5886a12423143b5b9c5ad386f3914748403e627017b4311255c45e44aa856d1437d722765ae25db9894e9e9b6c0de318a147c03c5644ba9512fe928c75304d1c0bc02956c57069c9ff22b115412f79f83849d109a36211364825dea05a0c20541dca064d0b21a34858f306360f27c0f91195c0bb72edf2b84e7b1941ea3eb3cc6d19712988979536930c0f4c2d935620fd86200f929d0f78ab6c785789e06b1b6432935173de1c60b57b79fd325e81429a6d63c604055bc66ac3e9ba5d8cfac53d623e5e7c6215d873b4f2273f4c4c3b792cd868cac6b199c2298965e6ac0c357535d0cb753202b409700f81cc9a229ec8a36432081af4902c454916b7b5b2f154259f3a1f351c954448782bfc2ddcebc2803881a6884670e381c427a47e60631ce8a02e88ada2b6ce2d518564c1c5b5289d14550faed0a761975f701877006808e2708501b74cf75143e7d30054d7970c21ce0bc0b340b1c5a2a96903e88eaacc876f19b69e33ca73a5a937e83f07098fb0c2a2ea970929c94f6598c993c7bdef26a19fdcc30b80bff44829ad8a0268e0cf3818567f3705be746ce5d186e9929431706db7128feb97403e6250617ccbbe508913a2a549ab976707c9dbf5595f9cc7147484478962c8cbcfb5d3b10401c50be5c1ffa44121e10eee698537991f21ca8020732af42bc5e453396b1c39d73a5e0f776aca009a05726a5c2c1f89e5a7e0469f77022bdd44076d50c0e47b98c6f85182890f81010e046a2ed1a88f78931750f0b230579a85c44039e59e80093e8619cb89f1c5bb28883f218a70f66284c9445ba919d3d94bae5654282027eee399fb45a7f7c052abbc5c33a95058124914e2cfa1478162ec6ec1e66f52ea93af66ce16d83fa5cb4d0d9c2825d53f70991e9eb95d848b3170ab9ba4b35d00b73afae59fdfa19bd691bd302c202038046fbb611816ce9b75c70529aa21f131900145c0910e7c580b39b0a51226bb2aa01b4ce16d0fb19d6bbcae71f94dec1a128c43b2e3c8905dac0586f48bd84a1473017d6b953574d360dc302341c28248b1bda9a26eb709d019938ee01cc18e2b2332179416b500a8c100f6471a841458a8a53fbe9c4fac465287538415e824bb4b2d981bbb8f0b436879b1ad1c9f448a60278799f07941b4866d06e778ca300c8989b9f0065090ac510a8a016446a50de71686a75e51e32ea1983813f7b20c0c43a61a7f093209b81115cea11330769626531e52c3b8025c2abd317ff6204a4da76a08bc8f06ec784ab9909ca61adde0bbae3582b07b84950477eff1c354ccb526c57fb3f8bf55632a77367342da76f0969345047981881d57507075b9c74bf531491412acf86b296d9a0ba118dc28ba3988ef2bc890d6224d22b3e227146b4125e934bc7e162b33ec4d23c4d4ebfeb47d89c34c186aa32d9fdd9af8dc786ca108e6b258"
44
+ },
45
+ {
46
+ "X25519 (29)": "39c652ecafe34c95a52b59b23734a6e76fea4c8449990cc73371f4c384d8756a"
47
+ }
48
+ ]
49
+ },
50
+ {
51
+ "name": "server_name (0)",
52
+ "server_name": "tls.peet.ws"
53
+ },
54
+ {
55
+ "name": "signature_algorithms (13)",
56
+ "signature_algorithms": [
57
+ "ecdsa_secp256r1_sha256",
58
+ "rsa_pss_rsae_sha256",
59
+ "rsa_pkcs1_sha256",
60
+ "ecdsa_secp384r1_sha384",
61
+ "rsa_pss_rsae_sha384",
62
+ "rsa_pkcs1_sha384",
63
+ "rsa_pss_rsae_sha512",
64
+ "rsa_pkcs1_sha512"
65
+ ]
66
+ },
67
+ {
68
+ "name": "supported_versions (43)",
69
+ "versions": [
70
+ "TLS_GREASE (0x7a7a)",
71
+ "TLS 1.3",
72
+ "TLS 1.2"
73
+ ]
74
+ },
75
+ {
76
+ "name": "extensionRenegotiationInfo (boringssl) (65281)",
77
+ "data": "00"
78
+ },
79
+ {
80
+ "name": "psk_key_exchange_modes (45)",
81
+ "PSK_Key_Exchange_Mode": "PSK with (EC)DHE key establishment (psk_dhe_ke) (1)"
82
+ },
83
+ {
84
+ "name": "status_request (5)",
85
+ "status_request": {
86
+ "certificate_status_type": "OSCP (1)",
87
+ "responder_id_list_length": 0,
88
+ "request_extensions_length": 0
89
+ }
90
+ },
91
+ {
92
+ "name": "extensionEncryptedClientHello (boringssl) (65037)",
93
+ "data": "0000010001730020237d34614940b7b222815f71403f87df482731e32bff7c5d8451d2a8436a21490090c2bac74a80b07c823ff04a68504530212817163e79ef93800ef535483576ad06510e4891387db4f6259a4f428e9fd636d4df00f4096976aa8248c9e3f8f786479aa2942dc0b2b4b5c086cf949b903f3a465543d3c686ea0055715a4fac9ada361c3872fe930f76e00c440502fe494a24d69dbe6bc0928d4be7c6026fdb2285335ed45dc177eb2e60dc1f634ea45b6cce"
94
+ },
95
+ {
96
+ "name": "application_layer_protocol_negotiation (16)",
97
+ "protocols": [
98
+ "h2",
99
+ "http/1.1"
100
+ ]
101
+ },
102
+ {
103
+ "name": "signed_certificate_timestamp (18)"
104
+ },
105
+ {
106
+ "name": "ec_point_formats (11)",
107
+ "elliptic_curves_point_formats": [
108
+ "0x00"
109
+ ]
110
+ },
111
+ {
112
+ "name": "supported_groups (10)",
113
+ "supported_groups": [
114
+ "TLS_GREASE (0x2a2a)",
115
+ "X25519MLKEM768 (4588)",
116
+ "X25519 (29)",
117
+ "P-256 (23)",
118
+ "P-384 (24)"
119
+ ]
120
+ },
121
+ {
122
+ "name": "session_ticket (35)",
123
+ "data": ""
124
+ },
125
+ {
126
+ "name": "extended_master_secret (23)",
127
+ "master_secret_data": "",
128
+ "extended_master_secret_data": ""
129
+ },
130
+ {
131
+ "name": "application_settings_old (17513)",
132
+ "protocols": [
133
+ "h2"
134
+ ]
135
+ },
136
+ {
137
+ "name": "TLS_GREASE (0x2a2a)"
138
+ }
139
+ ],
140
+ "tls_version_record": "771",
141
+ "tls_version_negotiated": "772",
142
+ "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,27-51-0-13-43-65281-45-5-65037-16-18-11-10-35-23-17513,4588-29-23-24,0",
143
+ "ja3_hash": "fb519300321e7e157792ac8d3a77e9ee",
144
+ "ja4": "t13d1516h2_8daaf6152771_02713d6af862",
145
+ "ja4_r": "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,4469,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
146
+ "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17513-18-23-27-35-43-45-5-51-65037-65281-GREASE-GREASE",
147
+ "peetprint_hash": "7466733991096b3f4e6c0e79b0083559",
148
+ "client_random": "863163dc7e5a1c86f1a8249603bd85722d3c89daac25212b4167f08d6e83f109",
149
+ "session_id": "02030897d9c146d208efbcdf654e4fd13b2bd2559109dfdb47a3b57bf498c383"
150
+ },
151
+ "http2": {
152
+ "akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
153
+ "akamai_fingerprint_hash": "52d84b11737d980aef856699f885ca86",
154
+ "sent_frames": [
155
+ {
156
+ "frame_type": "SETTINGS",
157
+ "length": 24,
158
+ "settings": [
159
+ "HEADER_TABLE_SIZE = 65536",
160
+ "ENABLE_PUSH = 0",
161
+ "INITIAL_WINDOW_SIZE = 6291456",
162
+ "MAX_HEADER_LIST_SIZE = 262144"
163
+ ]
164
+ },
165
+ {
166
+ "frame_type": "WINDOW_UPDATE",
167
+ "length": 4,
168
+ "increment": 15663105
169
+ },
170
+ {
171
+ "frame_type": "HEADERS",
172
+ "stream_id": 1,
173
+ "length": 462,
174
+ "headers": [
175
+ ":method: GET",
176
+ ":authority: tls.peet.ws",
177
+ ":scheme: https",
178
+ ":path: /api/all",
179
+ "sec-ch-ua: \"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
180
+ "sec-ch-ua-mobile: ?0",
181
+ "sec-ch-ua-platform: \"macOS\"",
182
+ "upgrade-insecure-requests: 1",
183
+ "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
184
+ "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
185
+ "sec-fetch-site: none",
186
+ "sec-fetch-mode: navigate",
187
+ "sec-fetch-user: ?1",
188
+ "sec-fetch-dest: document",
189
+ "accept-encoding: gzip, deflate, br, zstd",
190
+ "accept-language: en-US,en;q=0.9",
191
+ "priority: u=0, i"
192
+ ],
193
+ "flags": [
194
+ "EndStream (0x1)",
195
+ "EndHeaders (0x4)",
196
+ "Priority (0x20)"
197
+ ],
198
+ "priority": {
199
+ "weight": 256,
200
+ "depends_on": 0,
201
+ "exclusive": 1
202
+ }
203
+ }
204
+ ]
205
+ },
206
+ "tcpip": {
207
+ "cap_length": 1024,
208
+ "dst_port": 443,
209
+ "src_port": 59988,
210
+ "ip": {
211
+ "tos": 74,
212
+ "ttl": 44,
213
+ "ip_version": 4,
214
+ "dst_ip": "205.185.123.167",
215
+ "src_ip": "92.218.110.236"
216
+ },
217
+ "tcp": {
218
+ "ack": 3744637131,
219
+ "checksum": 7175,
220
+ "seq": 552557913,
221
+ "window": 2061
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "chrome-131",
4
+ "protocol": "tls",
5
+ "record": "client_hello",
6
+ "description": "TLS 1.3 ClientHello generated by curl-impersonate against tls.peet.ws impersonating chrome131. Captured via transparent SOCKS5 relay with correct SNI. Fields below are intentionally randomized per RFC 8446/8701 and must be masked during golden comparison (Cat 14).",
7
+ "randomizedFields": [
8
+ {
9
+ "byteOffset": 12,
10
+ "length": 32,
11
+ "reason": "random"
12
+ },
13
+ {
14
+ "byteOffset": 112,
15
+ "length": 1641,
16
+ "reason": "grease"
17
+ }
18
+ ],
19
+ "createdAt": "2026-08-04T00:28:49Z"
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "chrome-140",
4
+ "protocol": "http2",
5
+ "record": "settings",
6
+ "description": "First HTTP/2 SETTINGS frame sent by curl-impersonate --chrome-140. HTTP/2 SETTINGS are not randomized, so the ignore-list is empty.",
7
+ "randomizedFields": [],
8
+ "createdAt": "2026-08-02T00:00:00Z"
9
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "chrome-140",
4
+ "protocol": "tls",
5
+ "record": "client_hello",
6
+ "description": "TLS 1.3 ClientHello generated by curl-impersonate --chrome-140 against example.com. Fields below are intentionally randomized per RFC 8446 and must be masked during golden comparison (Cat 14).",
7
+ "randomizedFields": [
8
+ { "byteOffset": 12, "length": 32, "reason": "random" },
9
+ { "byteOffset": 49, "length": 32, "reason": "ephemeral_key" }
10
+ ],
11
+ "createdAt": "2026-08-02T00:00:00Z"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "firefox-128",
4
+ "protocol": "tls",
5
+ "record": "client_hello",
6
+ "description": "TLS 1.3 ClientHello generated by curl-impersonate --firefox-128 against example.com.",
7
+ "randomizedFields": [
8
+ { "byteOffset": 12, "length": 32, "reason": "random" },
9
+ { "byteOffset": 49, "length": 32, "reason": "ephemeral_key" }
10
+ ],
11
+ "createdAt": "2026-08-02T00:00:00Z"
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "source": "curl-impersonate",
3
+ "profile": "firefox-133",
4
+ "protocol": "http2",
5
+ "record": "settings",
6
+ "description": "First HTTP/2 SETTINGS frame sent by curl-impersonate firefox133. HTTP/2 SETTINGS are not randomized, so the ignore-list is empty.",
7
+ "randomizedFields": [],
8
+ "createdAt": "2026-08-04T00:28:49Z"
9
+ }