mic.git


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 }