diff --git a/go.mod b/go.mod index 62c97dc..fcea442 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.14 require ( github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3 + github.com/dreadl0ck/ja3 v1.0.4 + github.com/dreadl0ck/tlsx v1.0.1-google-gopacket github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 github.com/juju/ratelimit v1.0.1 diff --git a/go.sum b/go.sum index e2c9ee8..227294d 100644 --- a/go.sum +++ b/go.sum @@ -3,9 +3,16 @@ github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3/go.mod h1:6jR2SzckG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dreadl0ck/ja3 v1.0.4 h1:/2wao59Ezn8xBWxn8CVq8eRcPZHbhoTdX6fmg7tQtnw= +github.com/dreadl0ck/ja3 v1.0.4/go.mod h1:jATodgf1qBzTGieskRF2O1DXEwDgzEdqQjVcMMrCNpI= +github.com/dreadl0ck/tlsx v1.0.1-google-gopacket h1:/P3y+CGRiCQbW0nZU2jWkEwKfXLkpEgHNhbbqlnrTTM= +github.com/dreadl0ck/tlsx v1.0.1-google-gopacket/go.mod h1:amAb73WEEgPHWniMfwro6UpN6St3e5ypgq2tXM89IOo= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= +github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY= +github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -37,18 +44,24 @@ go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/client/chrome.go b/internal/client/chrome.go index 032a4ea..2125828 100644 --- a/internal/client/chrome.go +++ b/internal/client/chrome.go @@ -1,10 +1,10 @@ -// Fingerprint of Chrome 99 +// Fingerprint of Chrome 112 package client import ( "encoding/binary" - "encoding/hex" + "math/rand" "github.com/cbeuw/Cloak/internal/common" ) @@ -45,33 +45,44 @@ func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte { return ret } - // extension length is always 403, and server name length is variable + shuffle := func(exts [][]byte) { + var qword [8]byte + common.CryptoRandRead(qword[:]) + seed := int64(binary.BigEndian.Uint64(qword[:])) + rand.Seed(seed) + rand.Shuffle(len(exts), func(i, j int) { exts[i], exts[j] = exts[j], exts[i] }) + } + // extension length is always 403, and server name length is variable var ext [18][]byte - ext[0] = addExtRec(makeGREASE(), nil) // First GREASE - ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication - ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret - ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info - ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups - ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats - ext[6] = addExtRec([]byte{0x00, 0x23}, nil) // Session tickets - ALPN, _ := hex.DecodeString("000c02683208687474702f312e31") - ext[7] = addExtRec([]byte{0x00, 0x10}, ALPN) // app layer proto negotiation - ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request - sigAlgo, _ := hex.DecodeString("001004030804040105030805050108060601") - ext[9] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // Signature Algorithms - ext[10] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp - ext[11] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share - ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes - suppVersions, _ := hex.DecodeString("069A9A03040303") // 9A9A needs to be a GREASE + ext[0] = addExtRec(makeGREASE(), nil) // First GREASE + + // Start shufflable extensions: https://chromestatus.com/feature/5124606246518784 + ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication + ext[2] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret + ext[3] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info + ext[4] = addExtRec([]byte{0x00, 0x0a}, makeSupportedGroups()) // supported groups + ext[5] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats + ext[6] = addExtRec([]byte{0x00, 0x23}, nil) // Session tickets + ext[7] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation + ext[8] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request + ext[9] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001004030804040105030805050108060601")) // Signature Algorithms + ext[10] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp + ext[11] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share + ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes + suppVersions := decodeHex("069A9A03040303") // 9A9A needs to be a GREASE copy(suppVersions[1:3], makeGREASE()) ext[13] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions ext[14] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x02}) // compress certificate - applicationSettings, _ := hex.DecodeString("0003026832") - ext[15] = addExtRec([]byte{0x44, 0x69}, applicationSettings) // application settings - ext[16] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE - // len(ext[1]) + 175 + len(ext[16]) = 403 - // len(ext[16]) = 228 - len(ext[1]) + ext[15] = addExtRec([]byte{0x44, 0x69}, decodeHex("0003026832")) // application settings + // End shufflable extensions + + shuffle(ext[1:16]) + + ext[16] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE + // len(ext[1]) + len(all other ext) + len(ext[17]) = 403 + // len(all other ext) = 175 + // len(ext[17]) = 228 - len(ext[1]) // 2+2+len(padding) = 228 - len(ext[1]) // len(padding) = 224 - len(ext[1]) ext[17] = addExtRec([]byte{0x00, 0x15}, make([]byte, 224-len(ext[1]))) // padding @@ -84,20 +95,22 @@ func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte { func (c *Chrome) composeClientHello(hd clientHelloFields) (ch []byte) { var clientHello [12][]byte - clientHello[0] = []byte{0x01} // handshake type - clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 - clientHello[2] = []byte{0x03, 0x03} // client version - clientHello[3] = hd.random // random - clientHello[4] = []byte{0x20} // session id length 32 - clientHello[5] = hd.sessionId // session id - clientHello[6] = []byte{0x00, 0x20} // cipher suites length 32 - cipherSuites, _ := hex.DecodeString("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035") - clientHello[7] = append(makeGREASE(), cipherSuites...) // cipher suites - clientHello[8] = []byte{0x01} // compression methods length 1 - clientHello[9] = []byte{0x00} // compression methods - clientHello[11] = c.composeExtensions(hd.serverName, hd.x25519KeyShare) - clientHello[10] = []byte{0x00, 0x00} // extensions length 403 - binary.BigEndian.PutUint16(clientHello[10], uint16(len(clientHello[11]))) + clientHello[0] = []byte{0x01} // handshake type + clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 + clientHello[2] = []byte{0x03, 0x03} // client version + clientHello[3] = hd.random // random + clientHello[4] = []byte{0x20} // session id length 32 + clientHello[5] = hd.sessionId // session id + clientHello[6] = []byte{0x00, 0x20} // cipher suites length 32 + clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f0035")...) // cipher suites + clientHello[8] = []byte{0x01} // compression methods length 1 + clientHello[9] = []byte{0x00} // compression methods + + extensions := c.composeExtensions(hd.serverName, hd.x25519KeyShare) + clientHello[10] = []byte{0x00, 0x00} + binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length + clientHello[11] = extensions + var ret []byte for _, c := range clientHello { ret = append(ret, c...) diff --git a/internal/client/chrome_test.go b/internal/client/chrome_test.go index b147078..48dac50 100644 --- a/internal/client/chrome_test.go +++ b/internal/client/chrome_test.go @@ -2,6 +2,12 @@ package client import ( "encoding/hex" + "github.com/cbeuw/Cloak/internal/common" + "github.com/dreadl0ck/ja3" + "github.com/dreadl0ck/tlsx" + "github.com/stretchr/testify/assert" + "sort" + "strings" "testing" ) @@ -26,21 +32,34 @@ func TestMakeGREASE(t *testing.T) { } } -func TestComposeExtension(t *testing.T) { - serverName := "github.com" - keyShare, _ := hex.DecodeString("690f074f5c01756982269b66d58c90c47dc0f281d654c7b2c16f63c9033f5604") - - result := (&Chrome{}).composeExtensions(serverName, keyShare) - target, _ := hex.DecodeString("3a3a00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000a00080a0a001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b00296a6a000100001d0020690f074f5c01756982269b66d58c90c47dc0f281d654c7b2c16f63c9033f5604002d00020101002b000706dada03040303001b0003020002446900050003026832eaea000100001500cd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - for p := 0; p < len(result); p++ { - if result[p] != target[p] { - if result[p]&0x0F == 0xA && target[p]&0x0F == 0xA && - ((p > 0 && result[p-1] == result[p] && target[p-1] == target[p]) || - (p < len(result)-1 && result[p+1] == result[p] && target[p+1] == target[p])) { - continue - } - t.Errorf("inequality at %v", p) - } - } +func TestChromeJA3(t *testing.T) { + result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11) + + hello := tlsx.ClientHelloBasic{} + err := hello.Unmarshal(result) + assert.Nil(t, err) + // Chrome shuffles the order of extensions, so it needs special handling + full := string(ja3.Bare(&hello)) + // TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats + parts := strings.Split(full, ",") + + // TLSVersion,Ciphers + assert.Equal(t, + []string{ + "771", + "4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53", + }, parts[0:2]) + // EllipticCurves,EllipticCurvePointFormats + assert.Equal(t, + []string{ + "29-23-24", "0", + }, parts[3:5]) + + normaliseExtensions := func(extensions string) []string { + extensionParts := strings.Split(parts[2], "-") + sort.Strings(extensionParts) + return extensionParts + } + assert.Equal(t, normaliseExtensions("10-5-45-0-17513-13-18-11-23-16-35-27-65281-43-51-21"), normaliseExtensions(parts[2])) } diff --git a/internal/client/firefox.go b/internal/client/firefox.go index 239dbaf..ad68673 100644 --- a/internal/client/firefox.go +++ b/internal/client/firefox.go @@ -1,11 +1,9 @@ -// Fingerprint of Firefox 99 +// Fingerprint of Firefox 112 package client import ( "encoding/binary" - "encoding/hex" - "github.com/cbeuw/Cloak/internal/common" ) @@ -24,29 +22,24 @@ func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte { return ret } // extension length is always 401, and server name length is variable - var ext [15][]byte - ext[0] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication - ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret - ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info - suppGroup, _ := hex.DecodeString("000c001d00170018001901000101") - ext[3] = addExtRec([]byte{0x00, 0x0a}, suppGroup) // supported groups - ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats - ext[5] = addExtRec([]byte{0x00, 0x23}, nil) // session ticket - ALPN, _ := hex.DecodeString("000c02683208687474702f312e31") - ext[6] = addExtRec([]byte{0x00, 0x10}, ALPN) // app layer proto negotiation - ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request - delegatedCredentials, _ := hex.DecodeString("00080403050306030203") - ext[8] = addExtRec([]byte{0x00, 0x22}, delegatedCredentials) // delegated credentials - ext[9] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share - suppVersions, _ := hex.DecodeString("0403040303") - ext[10] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions - sigAlgo, _ := hex.DecodeString("001604030503060308040805080604010501060102030201") - ext[11] = addExtRec([]byte{0x00, 0x0d}, sigAlgo) // Signature Algorithms - ext[12] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes - ext[13] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit - // len(ext[0]) + 238 + 4 + len(padding) = 401 - // len(padding) = 177 - len(ext[0]) - ext[14] = addExtRec([]byte{0x00, 0x15}, make([]byte, 159-len(ext[0]))) // padding + var ext [13][]byte + ext[0] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication + ext[1] = addExtRec([]byte{0x00, 0x17}, nil) // extended_master_secret + ext[2] = addExtRec([]byte{0xff, 0x01}, []byte{0x00}) // renegotiation_info + ext[3] = addExtRec([]byte{0x00, 0x0a}, decodeHex("000c001d00170018001901000101")) // supported groups + ext[4] = addExtRec([]byte{0x00, 0x0b}, []byte{0x01, 0x00}) // ec point formats + ext[5] = addExtRec([]byte{0x00, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation + ext[6] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request + ext[7] = addExtRec([]byte{0x00, 0x22}, decodeHex("00080403050306030203")) // delegated credentials + ext[8] = addExtRec([]byte{0x00, 0x33}, composeKeyShare(keyShare)) // key share + ext[9] = addExtRec([]byte{0x00, 0x2b}, decodeHex("0403040303")) // supported versions + ext[10] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030503060308040805080604010501060102030201")) // Signature Algorithms + ext[11] = addExtRec([]byte{0x00, 0x1c}, []byte{0x40, 0x01}) // record size limit + // len(ext[0]) + len(all other ext) + len(len field of padding) + len(padding) = 401 + // len(all other ext) = 228 + // len(len field of padding) = 4 + // len(padding) = 169 - len(ext[0]) + ext[12] = addExtRec([]byte{0x00, 0x15}, make([]byte, 169-len(ext[0]))) // padding var ret []byte for _, e := range ext { ret = append(ret, e...) @@ -56,21 +49,21 @@ func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte { func (f *Firefox) composeClientHello(hd clientHelloFields) (ch []byte) { var clientHello [12][]byte - clientHello[0] = []byte{0x01} // handshake type - clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 - clientHello[2] = []byte{0x03, 0x03} // client version - clientHello[3] = hd.random // random - clientHello[4] = []byte{0x20} // session id length 32 - clientHello[5] = hd.sessionId // session id - clientHello[6] = []byte{0x00, 0x22} // cipher suites length 34 - cipherSuites, _ := hex.DecodeString("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") - clientHello[7] = cipherSuites // cipher suites - clientHello[8] = []byte{0x01} // compression methods length 1 - clientHello[9] = []byte{0x00} // compression methods + clientHello[0] = []byte{0x01} // handshake type + clientHello[1] = []byte{0x00, 0x01, 0xfc} // length 508 + clientHello[2] = []byte{0x03, 0x03} // client version + clientHello[3] = hd.random // random + clientHello[4] = []byte{0x20} // session id length 32 + clientHello[5] = hd.sessionId // session id + clientHello[6] = []byte{0x00, 0x22} // cipher suites length 34 + clientHello[7] = decodeHex("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") // cipher suites + clientHello[8] = []byte{0x01} // compression methods length 1 + clientHello[9] = []byte{0x00} // compression methods - clientHello[11] = f.composeExtensions(hd.serverName, hd.x25519KeyShare) - clientHello[10] = []byte{0x00, 0x00} // extensions length - binary.BigEndian.PutUint16(clientHello[10], uint16(len(clientHello[11]))) + extensions := f.composeExtensions(hd.serverName, hd.x25519KeyShare) + clientHello[10] = []byte{0x00, 0x00} + binary.BigEndian.PutUint16(clientHello[10], uint16(len(extensions))) // extension length + clientHello[11] = extensions var ret []byte for _, c := range clientHello { diff --git a/internal/client/firefox_test.go b/internal/client/firefox_test.go index 20ae821..c9de536 100644 --- a/internal/client/firefox_test.go +++ b/internal/client/firefox_test.go @@ -1,20 +1,43 @@ package client import ( - "bytes" "encoding/hex" + "github.com/cbeuw/Cloak/internal/common" + "github.com/dreadl0ck/ja3" + "github.com/dreadl0ck/tlsx" + "github.com/stretchr/testify/assert" + "strings" "testing" ) -func TestComposeExtensions(t *testing.T) { - target, _ := hex.DecodeString("000000170015000012636f6e73656e742e676f6f676c652e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b00020100002300000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d00208d8ea1b80430b7710b65f0d89b0144a5eeb218709ce6613d4fc8bfb117657c1500170041947458330e3553dcde0a8741eb1dde26ebaee8262029c5edb3cbacc9ee1d7c866085b9cf483d943248997a65c5fa1d35725213895d0e5569d4e291863061b7d075002b00050403040303000d0018001604030503060308040805080604010501060102030201002d00020101001c0002400100150084000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +var hd = clientHelloFields{ + random: decodeHex("ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a0"), + sessionId: decodeHex("47485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b9"), + x25519KeyShare: decodeHex("d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa44"), + serverName: "github.com", +} + +func TestFirefoxJA3(t *testing.T) { + result := common.AddRecordLayer((&Firefox{}).composeClientHello(hd), common.Handshake, common.VersionTLS11) - serverName := "consent.google.com" - keyShare, _ := hex.DecodeString("8d8ea1b80430b7710b65f0d89b0144a5eeb218709ce6613d4fc8bfb117657c15") + hello := tlsx.ClientHelloBasic{} + err := hello.Unmarshal(result) + assert.Nil(t, err) + + digest := ja3.DigestHex(&hello) + assert.Equal(t, "ad55557b7cbd735c2627f7ebb3b3d493", digest) +} + +func TestFirefoxComposeClientHello(t *testing.T) { + result := hex.EncodeToString((&Firefox{}).composeClientHello(hd)) + target := "010001fc0303ed0117085ed70be0799b1fc96af7f675d4747f86cd03bb36392e03e8d1b0e9a02047485f67c59ca787009bba83ede4da4f2397169c696c275d96c4c7af803019b90022130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035010001910000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b000201000010000e000c02683208687474702f312e310005000501000000000022000a000804030503060302030033006b0069001d0020d395003163a6f751b4c68a67bcec1f883885a7ada8a63fda389b29986e51fa440017004104c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47002b00050403040303000d0018001604030503060308040805080604010501060102030201001c0002400100150096000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - result := (&Firefox{}).composeExtensions(serverName, keyShare) // skip random secp256r1 - if !bytes.Equal(result[:151], target[:151]) || !bytes.Equal(result[216:], target[216:]) { - t.Errorf("got %x", result) - } + secp256r1 := "04c49751010e35370cf8e89c23471b40579387b3dd5ce6862c9850b121632b527128b75ef7051c5284ae94894d846cc3dc88ce01ce49b605167f63473c1d772b47" + start := strings.Index(target, secp256r1) + + target = strings.Replace(target, secp256r1, "", 1) + result = strings.Replace(result, result[start:start+len(secp256r1)], "", 1) + + assert.Equal(t, target, result) }