mic~ proxy for JA4-TLS fingerprinting evasion |
| git clone https://github.com/djnnvx/mic.git |
| log | files | refs | archive | README | LICENSE |
ja4s_test.go (7323B)
1 package fingerprint 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "regexp" 7 "testing" 8 ) 9 10 // buildServerHello synthesizes a TLS handshake record carrying a ServerHello. 11 // extensions is the wire-encoded extensions block (type + len + data tuples). 12 func buildServerHello(legacyVer uint16, sessionIDLen int, cipher uint16, extensions []byte) []byte { 13 var body bytes.Buffer 14 15 binary.Write(&body, binary.BigEndian, legacyVer) 16 body.Write(make([]byte, 32)) // random 17 18 body.WriteByte(byte(sessionIDLen)) 19 body.Write(make([]byte, sessionIDLen)) 20 21 binary.Write(&body, binary.BigEndian, cipher) 22 body.WriteByte(0x00) // compression_method = null 23 24 binary.Write(&body, binary.BigEndian, uint16(len(extensions))) 25 body.Write(extensions) 26 27 // handshake header: type(0x02) + 3-byte length 28 hsLen := body.Len() 29 hs := []byte{0x02, byte(hsLen >> 16), byte(hsLen >> 8), byte(hsLen)} 30 hs = append(hs, body.Bytes()...) 31 32 // record header: type(0x16) + version(0x0303) + 2-byte length 33 rec := []byte{0x16, 0x03, 0x03, byte(len(hs) >> 8), byte(len(hs))} 34 return append(rec, hs...) 35 } 36 37 func ext(t uint16, data []byte) []byte { 38 out := make([]byte, 4+len(data)) 39 binary.BigEndian.PutUint16(out[0:2], t) 40 binary.BigEndian.PutUint16(out[2:4], uint16(len(data))) 41 copy(out[4:], data) 42 return out 43 } 44 45 func TestParseServerHello_TLS13_Minimal(t *testing.T) { 46 var exts []byte 47 exts = append(exts, ext(0x002b, []byte{0x03, 0x04})...) // chosen version 48 exts = append(exts, ext(0x0033, []byte{})...) // key_share (data irrelevant) 49 50 raw := buildServerHello(0x0303, 32, 0x1301, exts) 51 52 sh, err := ParseServerHello(raw) 53 if err != nil { 54 t.Fatalf("ParseServerHello: %v", err) 55 } 56 57 if sh.CipherSuite != 0x1301 { 58 t.Errorf("cipher = 0x%04x; want 0x1301", sh.CipherSuite) 59 } 60 if got := len(sh.Extensions); got != 2 { 61 t.Errorf("extension count = %d; want 2", got) 62 } 63 if sh.SupportedVersion != 0x0304 { 64 t.Errorf("supported version = 0x%04x; want 0x0304", sh.SupportedVersion) 65 } 66 if sh.ALPN != "" { 67 t.Errorf("alpn = %q; want none", sh.ALPN) 68 } 69 } 70 71 func TestParseServerHello_TLS13_WithALPN_h2(t *testing.T) { 72 alpnList := []byte{0x00, 0x03, 0x02, 'h', '2'} 73 74 var exts []byte 75 exts = append(exts, ext(0x002b, []byte{0x03, 0x04})...) 76 exts = append(exts, ext(0x0010, alpnList)...) 77 exts = append(exts, ext(0x0033, []byte{})...) 78 79 raw := buildServerHello(0x0303, 0, 0x1302, exts) 80 81 sh, err := ParseServerHello(raw) 82 if err != nil { 83 t.Fatalf("ParseServerHello: %v", err) 84 } 85 if sh.ALPN != "h2" { 86 t.Fatalf("alpn = %q; want h2", sh.ALPN) 87 } 88 } 89 90 func TestParseServerHello_TLS12_NoSupportedVersions(t *testing.T) { 91 // Pre-1.3, legacy_version is the chosen version. 92 exts := ext(0x0023, []byte{}) // session_ticket, empty 93 raw := buildServerHello(0x0303, 32, 0xc02f, exts) 94 95 sh, err := ParseServerHello(raw) 96 if err != nil { 97 t.Fatalf("ParseServerHello: %v", err) 98 } 99 if sh.LegacyVersion != 0x0303 { 100 t.Errorf("legacy version = 0x%04x; want 0x0303", sh.LegacyVersion) 101 } 102 if sh.SupportedVersion != 0 { 103 t.Errorf("supported_version present in TLS 1.2 fixture: 0x%04x", sh.SupportedVersion) 104 } 105 if sh.CipherSuite != 0xc02f { 106 t.Errorf("cipher = 0x%04x; want 0xc02f", sh.CipherSuite) 107 } 108 } 109 110 func TestParseServerHello_GREASEFiltered(t *testing.T) { 111 var exts []byte 112 exts = append(exts, ext(0x0a0a, []byte{})...) // GREASE 113 exts = append(exts, ext(0x002b, []byte{0x03, 0x04})...) 114 exts = append(exts, ext(0x0033, []byte{})...) 115 116 raw := buildServerHello(0x0303, 0, 0x1301, exts) 117 118 sh, err := ParseServerHello(raw) 119 if err != nil { 120 t.Fatalf("ParseServerHello: %v", err) 121 } 122 for _, e := range sh.Extensions { 123 if e == 0x0a0a { 124 t.Errorf("GREASE extension 0x0a0a leaked into parsed extensions") 125 } 126 } 127 if len(sh.Extensions) != 2 { 128 t.Errorf("extension count = %d; want 2 (GREASE filtered)", len(sh.Extensions)) 129 } 130 } 131 132 func TestParseServerHello_Errors(t *testing.T) { 133 cases := []struct { 134 name string 135 raw []byte 136 wantErr bool 137 }{ 138 {"empty", nil, true}, 139 {"too short", []byte{0x16, 0x03}, true}, 140 {"wrong content type", []byte{0x17, 0x03, 0x03, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00}, true}, 141 {"wrong handshake type (ClientHello)", []byte{0x16, 0x03, 0x03, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00}, true}, 142 {"truncated record", []byte{0x16, 0x03, 0x03, 0x00, 0xff, 0x02, 0x00, 0x00, 0x00}, true}, 143 // Positive control: without it, a parser that always errors keeps this table green. 144 {"valid ServerHello", buildServerHello(0x0303, 32, 0x1301, ext(0x002b, []byte{0x03, 0x04})), false}, 145 } 146 for _, tc := range cases { 147 t.Run(tc.name, func(t *testing.T) { 148 _, err := ParseServerHello(tc.raw) 149 if tc.wantErr && err == nil { 150 t.Errorf("expected error, got nil") 151 } 152 if !tc.wantErr && err != nil { 153 t.Errorf("expected no error, got %v", err) 154 } 155 }) 156 } 157 } 158 159 func TestComputeJA4S_Shape(t *testing.T) { 160 // Shape: t<ver><nn><alpn>_<cipher>_<12hex> 161 sh := &ServerHelloFields{ 162 LegacyVersion: 0x0303, 163 CipherSuite: 0x1301, 164 Extensions: []uint16{0x002b, 0x0033}, 165 SupportedVersion: 0x0304, 166 } 167 got := ComputeJA4S(sh) 168 169 re := regexp.MustCompile(`^t1302(00|[a-z0-9]{2})_[0-9a-f]{4}_[0-9a-f]{12}$`) 170 if !re.MatchString(got) { 171 t.Errorf("JA4S = %q; does not match expected shape", got) 172 } 173 } 174 175 func TestComputeJA4S_KnownFixture(t *testing.T) { 176 // The extension list is DESCENDING on purpose. JA4S hashes wire order and 177 // must not sort; an ascending fixture cannot tell the two apart. 178 sh := &ServerHelloFields{ 179 LegacyVersion: 0x0303, 180 CipherSuite: 0x1301, 181 Extensions: []uint16{0x0033, 0x002b}, 182 SupportedVersion: 0x0304, 183 } 184 got := ComputeJA4S(sh) 185 const want = "t130200_1301_" 186 if got[:len(want)] != want { 187 t.Errorf("JA4S prefix = %q; want prefix %q", got, want) 188 } 189 // sha256("0033,002b")[:12]. Sorting would give sha256("002b,0033")[:12] 190 // = a56c5b993250 instead, which this pin rejects. 191 const wantHash = "234ea6891581" 192 if got[len(want):] != wantHash { 193 t.Errorf("JA4S ext hash = %q; want %q", got[len(want):], wantHash) 194 } 195 } 196 197 func TestComputeJA4S_EmptyExtensions(t *testing.T) { 198 // Pre-TLS-1.3 ServerHello with no extensions block: JA4S_c is all zeroes, 199 // not sha256(""). 200 sh := &ServerHelloFields{LegacyVersion: 0x0303, CipherSuite: 0xc02f} 201 if got := ComputeJA4S(sh); got != "t120000_c02f_000000000000" { 202 t.Errorf("JA4S = %q; want t120000_c02f_000000000000", got) 203 } 204 } 205 206 func TestJA4SALPNCode(t *testing.T) { 207 cases := []struct { 208 alpn string 209 want string 210 }{ 211 {"", "00"}, 212 {"h2", "h2"}, 213 {"http/1.1", "h1"}, 214 {"h", "hh"}, 215 {"h3", "h3"}, 216 {"\xab", "ab"}, 217 {"\x20", "20"}, 218 {"\xab\xcd", "ad"}, 219 {"\x20\x61", "21"}, 220 {"\x30\xab", "3b"}, 221 {"\x61\x20", "60"}, 222 {"\x30\x31\xab\xcd", "3d"}, 223 // Both end bytes are alphanumeric, so the hex fallback must NOT trigger. 224 {"\x30\xab\xcd\x31", "01"}, 225 } 226 for _, tc := range cases { 227 if got := ja4sALPNCode(tc.alpn); got != tc.want { 228 t.Errorf("ja4sALPNCode(%q) = %q; want %q", tc.alpn, got, tc.want) 229 } 230 } 231 } 232 233 func TestComputeJA4S_ALPN_h2(t *testing.T) { 234 sh := &ServerHelloFields{ 235 LegacyVersion: 0x0303, 236 CipherSuite: 0x1301, 237 Extensions: []uint16{0x002b, 0x0010, 0x0033}, 238 SupportedVersion: 0x0304, 239 ALPN: "h2", 240 } 241 got := ComputeJA4S(sh) 242 // nn=03, alpn=h2 243 const wantPrefix = "t1303h2_1301_" 244 if got[:len(wantPrefix)] != wantPrefix { 245 t.Errorf("JA4S = %q; want prefix %q", got, wantPrefix) 246 } 247 }