mirror of https://github.com/cbeuw/Cloak
Compare commits
No commits in common. 'master' and 'v2.7.1-pre' have entirely different histories.
master
...
v2.7.1-pre
@ -1,4 +1,5 @@
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
threshold: 1%
|
@ -1,30 +1,17 @@
|
||||
module github.com/cbeuw/Cloak
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.2
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/cbeuw/connutil v0.0.0-20200411215123-966bfaa51ee3
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/refraction-networking/utls v1.6.6
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
golang.org/x/crypto v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/juju/ratelimit v1.0.1
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
gitlab.com/yawning/utls.git v0.0.12-1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.1.0
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
@ -0,0 +1,39 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func htob(s string) []byte {
|
||||
b, _ := hex.DecodeString(s)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestMakeServerName(t *testing.T) {
|
||||
type testingPair struct {
|
||||
serverName string
|
||||
target []byte
|
||||
}
|
||||
|
||||
pairs := []testingPair{
|
||||
{
|
||||
"www.google.com",
|
||||
htob("001100000e7777772e676f6f676c652e636f6d"),
|
||||
},
|
||||
{
|
||||
"www.gstatic.com",
|
||||
htob("001200000f7777772e677374617469632e636f6d"),
|
||||
},
|
||||
{
|
||||
"googleads.g.doubleclick.net",
|
||||
htob("001e00001b676f6f676c656164732e672e646f75626c65636c69636b2e6e6574"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range pairs {
|
||||
assert.Equal(t, p.target, generateSNI(p.serverName))
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
// Fingerprint of Chrome 112
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
type Chrome struct{}
|
||||
|
||||
func makeGREASE() []byte {
|
||||
// see https://tools.ietf.org/html/draft-davidben-tls-grease-01
|
||||
// This is exclusive to Chrome.
|
||||
var one [1]byte
|
||||
common.CryptoRandRead(one[:])
|
||||
sixteenth := one[0] % 16
|
||||
monoGREASE := sixteenth*16 + 0xA
|
||||
doubleGREASE := []byte{monoGREASE, monoGREASE}
|
||||
return doubleGREASE
|
||||
}
|
||||
|
||||
func (c *Chrome) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
|
||||
makeSupportedGroups := func() []byte {
|
||||
suppGroupListLen := []byte{0x00, 0x08}
|
||||
ret := make([]byte, 2+8)
|
||||
copy(ret[0:2], suppGroupListLen)
|
||||
copy(ret[2:4], makeGREASE())
|
||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18})
|
||||
return ret
|
||||
}
|
||||
|
||||
makeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 43)
|
||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
||||
copy(ret[2:4], makeGREASE())
|
||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
||||
ret[6] = 0x00
|
||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
||||
copy(ret[11:43], hidden)
|
||||
return ret
|
||||
}
|
||||
|
||||
shuffle := func(exts [][]byte) {
|
||||
var qword [8]byte
|
||||
common.CryptoRandRead(qword[:])
|
||||
seed := int64(binary.BigEndian.Uint64(qword[:]))
|
||||
source := rand.NewSource(seed)
|
||||
r := rand.New(source)
|
||||
r.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
|
||||
|
||||
// Start shufflable extensions: https://chromestatus.com/feature/5124606246518784
|
||||
ext[1] = addExtRec([]byte{0x00, 0x00}, generateSNI(serverName)) // server name indication
|
||||
sniLen := len(ext[1])
|
||||
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
|
||||
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
|
||||
// sniLen + len(all other ext) + len(ext[17]) = 403
|
||||
// len(all other ext) = 175
|
||||
// len(ext[17]) = 228 - sniLen
|
||||
// 2+2+len(padding) = 228 - sniLen
|
||||
// len(padding) = 224 - sniLen
|
||||
ext[17] = addExtRec([]byte{0x00, 0x15}, make([]byte, 224-sniLen)) // padding
|
||||
var ret []byte
|
||||
for _, e := range ext {
|
||||
ret = append(ret, e...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
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
|
||||
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...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMakeGREASE(t *testing.T) {
|
||||
a := hex.EncodeToString(makeGREASE())
|
||||
if a[1] != 'a' || a[3] != 'a' {
|
||||
t.Errorf("GREASE got %v", a)
|
||||
}
|
||||
|
||||
var GREASEs []string
|
||||
for i := 0; i < 50; i++ {
|
||||
GREASEs = append(GREASEs, hex.EncodeToString(makeGREASE()))
|
||||
}
|
||||
var eqCount int
|
||||
for _, g := range GREASEs {
|
||||
if a == g {
|
||||
eqCount++
|
||||
}
|
||||
}
|
||||
if eqCount > 40 {
|
||||
t.Error("GREASE is not random", GREASEs)
|
||||
}
|
||||
}
|
||||
|
||||
//func TestChromeJA3(t *testing.T) {
|
||||
// result := common.AddRecordLayer((&Chrome{}).composeClientHello(hd), common.Handshake, common.VersionTLS11)
|
||||
// assert.Equal(t, 517, len(result))
|
||||
//
|
||||
// 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]))
|
||||
//}
|
@ -0,0 +1,73 @@
|
||||
// Fingerprint of Firefox 112
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/cbeuw/Cloak/internal/common"
|
||||
)
|
||||
|
||||
type Firefox struct{}
|
||||
|
||||
func (f *Firefox) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
composeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 107)
|
||||
ret[0], ret[1] = 0x00, 0x69 // length 105
|
||||
ret[2], ret[3] = 0x00, 0x1d // group x25519
|
||||
ret[4], ret[5] = 0x00, 0x20 // length 32
|
||||
copy(ret[6:38], hidden)
|
||||
ret[38], ret[39] = 0x00, 0x17 // group secp256r1
|
||||
ret[40], ret[41] = 0x00, 0x41 // length 65
|
||||
common.CryptoRandRead(ret[42:107])
|
||||
return ret
|
||||
}
|
||||
// extension length is always 401, and server name length is variable
|
||||
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...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
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
|
||||
clientHello[7] = decodeHex("130113031302c02bc02fcca9cca8c02cc030c00ac009c013c014009c009d002f0035") // cipher suites
|
||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
||||
clientHello[9] = []byte{0x00} // compression methods
|
||||
|
||||
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 {
|
||||
ret = append(ret, c...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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)
|
||||
//
|
||||
// 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"
|
||||
|
||||
// skip random secp256r1
|
||||
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)
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
// Fingerprint of Safari 16.4
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type Safari struct{}
|
||||
|
||||
func (s *Safari) composeExtensions(serverName string, keyShare []byte) []byte {
|
||||
makeSupportedGroups := func() []byte {
|
||||
suppGroupListLen := []byte{0x00, 0x0a}
|
||||
ret := make([]byte, 2+2+8)
|
||||
copy(ret[0:2], suppGroupListLen)
|
||||
copy(ret[2:4], makeGREASE())
|
||||
copy(ret[4:], []byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19})
|
||||
return ret
|
||||
}
|
||||
|
||||
makeKeyShare := func(hidden []byte) []byte {
|
||||
ret := make([]byte, 43)
|
||||
ret[0], ret[1] = 0x00, 0x29 // length 41
|
||||
copy(ret[2:4], makeGREASE())
|
||||
ret[4], ret[5] = 0x00, 0x01 // length 1
|
||||
ret[6] = 0x00
|
||||
ret[7], ret[8] = 0x00, 0x1d // group x25519
|
||||
ret[9], ret[10] = 0x00, 0x20 // length 32
|
||||
copy(ret[11:43], hidden)
|
||||
return ret
|
||||
}
|
||||
|
||||
// extension length is always 393, and server name length is variable
|
||||
var ext [16][]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, 0x10}, decodeHex("000c02683208687474702f312e31")) // app layer proto negotiation
|
||||
ext[7] = addExtRec([]byte{0x00, 0x05}, []byte{0x01, 0x00, 0x00, 0x00, 0x00}) // status request
|
||||
ext[8] = addExtRec([]byte{0x00, 0x0d}, decodeHex("001604030804040105030203080508050501080606010201")) // Signature Algorithms
|
||||
ext[9] = addExtRec([]byte{0x00, 0x12}, nil) // signed cert timestamp
|
||||
ext[10] = addExtRec([]byte{0x00, 0x33}, makeKeyShare(keyShare)) // key share
|
||||
ext[11] = addExtRec([]byte{0x00, 0x2d}, []byte{0x01, 0x01}) // psk key exchange modes
|
||||
suppVersions := decodeHex("0a5a5a0304030303020301") // 5a5a needs to be a GREASE
|
||||
copy(suppVersions[1:3], makeGREASE())
|
||||
ext[12] = addExtRec([]byte{0x00, 0x2b}, suppVersions) // supported versions
|
||||
ext[13] = addExtRec([]byte{0x00, 0x1b}, []byte{0x02, 0x00, 0x01}) // compress certificate
|
||||
ext[14] = addExtRec(makeGREASE(), []byte{0x00}) // Last GREASE
|
||||
// len(ext[1]) + len(all other ext) + len(ext[15]) = 393
|
||||
// len(all other ext) = 174
|
||||
// len(ext[15]) = 219 - len(ext[1])
|
||||
// 2+2+len(padding) = 219 - len(ext[1])
|
||||
// len(padding) = 215 - len(ext[1])
|
||||
ext[15] = addExtRec([]byte{0x00, 0x15}, make([]byte, 215-len(ext[1]))) // padding
|
||||
var ret []byte
|
||||
for _, e := range ext {
|
||||
ret = append(ret, e...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Safari) 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, 0x2a} // cipher suites length 42
|
||||
clientHello[7] = append(makeGREASE(), decodeHex("130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a")...) // cipher suites
|
||||
clientHello[8] = []byte{0x01} // compression methods length 1
|
||||
clientHello[9] = []byte{0x00} // compression methods
|
||||
|
||||
extensions := s.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...)
|
||||
}
|
||||
return ret
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var safariHd = clientHelloFields{
|
||||
random: decodeHex("977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc17"),
|
||||
sessionId: decodeHex("c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3"),
|
||||
x25519KeyShare: decodeHex("c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a"),
|
||||
serverName: "github.com",
|
||||
}
|
||||
|
||||
//func TestSafariJA3(t *testing.T) {
|
||||
// result := common.AddRecordLayer((&Safari{}).composeClientHello(safariHd), common.Handshake, common.VersionTLS11)
|
||||
//
|
||||
// hello := tlsx.ClientHelloBasic{}
|
||||
// err := hello.Unmarshal(result)
|
||||
// assert.Nil(t, err)
|
||||
//
|
||||
// digest := ja3.DigestHex(&hello)
|
||||
// assert.Equal(t, "773906b0efdefa24a7f2b8eb6985bf37", digest)
|
||||
//}
|
||||
|
||||
func TestSafariComposeClientHello(t *testing.T) {
|
||||
result := (&Safari{}).composeClientHello(safariHd)
|
||||
target := decodeHex("010001fc0303977ecef48c0fc5640fea4dbd638da89704d6d85ed2e81b8913ae5b27f9a5cc1720c2d5b91e77371bf154363b39194ac77c05617cc6164724d0ba7ded4aa349c6a3002acaca130113021303c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000189fafa00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000c000a7a7a001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d0018001604030804040105030203080508050501080606010201001200000033002b00297a7a000100001d0020c99fbe80dda71f6e24d9b798dc3f3f33cef946f0b917fa90154a4b95114fae2a002d00020101002b000b0a2a2a0304030303020301001b00030200017a7a000100001500c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["*"],
|
||||
"excludePackagePatterns": ["utls"],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue