Implement consistent randomized fingerprint (#20)

- Uses a chacha20-based CSPRNG to generate randomized fingeprints
 - Refactors generation of randomized fingerprints, removing many redundant shuffle functions.
 - Adds Seed field to ClientHelloID
 - ClientHelloID.Version is now a string (was uint16)
This commit is contained in:
sergeyfrolov 2019-03-06 16:14:34 -07:00 committed by GitHub
parent 1188641a16
commit 7c97cdb476
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 442 additions and 215 deletions

View file

@ -5,20 +5,54 @@
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance, low-level access to handshake, fake session tickets and some other features. Handshake is still performed by "crypto/tls", this library merely changes ClientHello part of it and provides low-level access.
Golang 1.11+ is required.
If you have any questions, bug reports or contributions, you are welcome to publish those on GitHub. If you want to do so in private, you can contact one of developers personally via sergey.frolov@colorado.edu
Documentation below may not keep up with all the changes and new features at all times,
so you are encouraged to use [godoc](https://godoc.org/github.com/refraction-networking/utls#UConn).
# Features
## Low-level access to handshake
* Read/write access to all bits of client hello message.
* Read access to fields of ClientHandshakeState, which, among other things, includes ServerHello and MasterSecret.
* Read keystream. Can be used, for example, to "write" something in ciphertext.
## ClientHello fingerprinting resistance
Golang's ClientHello has a very unique fingerprint, which especially sticks out on mobile clients,
where Golang is not too popular yet.
Some members of anti-censorship community are concerned that their tools could be trivially blocked based on
ClientHello with relatively small collateral damage. There are multiple solutions to this issue.
### Randomized handshake
This package can generate randomized ClientHello using only extensions and cipherSuites "crypto/tls" already supports.
This provides a solid moving target without any compatibility or parrot-is-dead attack risks.
**Feedback about opinionated implementation details of randomized handshake is appreciated.**
**It is highly recommended to use multiple fingeprints, including randomized ones to avoid relying on a single fingerprint.**
[utls.Roller](#roller) does this automatically.
### Randomized Fingerprint
Randomized Fingerprints are supposedly good at defeating blacklists, since
those fingerprints have random ciphersuites and extensions in random order.
Note that all used ciphersuites and extensions are fully supported by uTLS,
which provides a solid moving target without any compatibility or parrot-is-dead attack risks.
But note that there's a small chance that generated fingerprint won't work,
so you may want to keep generating until a working one is found,
and then keep reusing the working fingerprint to avoid suspicious behavior of constantly changing fingerprints.
[utls.Roller](#roller) reuses working fingerprint automatically.
#### Generating randomized fingerprints
To generate a randomized fingerprint, simply do:
```Golang
uTlsConn := tls.UClient(tcpConn, &config, tls.HelloRandomized)
```
you can use `helloRandomizedALPN` or `helloRandomizedNoALPN` to ensure presence or absence of
ALPN(Application-Layer Protocol Negotiation) extension.
It is recommended, but certainly not required to include ALPN (or use helloRandomized which may or may not include ALPN).
If you do use ALPN, you will want to correctly handle potential application layer protocols (likely h2 or http/1.1).
#### Reusing randomized fingerprint
```Golang
// oldConn is an old connection that worked before, so we want to reuse it
// newConn is a new connection we'd like to establish
newConn := tls.UClient(tcpConn, &config, oldConn.ClientHelloID)
```
### Parroting
This package can be used to parrot ClientHello of popular browsers.
There are some caveats to this parroting:
@ -48,10 +82,11 @@ It LGTM, but please open up Wireshark and check. If you see something — [say s
There sure are. If you found one that approaches practicality at line speed — [please tell us](issues).
#### Things to implement in Golang to make parrots better
uTLS is fundamentially limited in parroting, because Golang's "crypto/tls" doesn't support many things. Would be nice to have:
* ChannelID extension
* In general, any modern crypto is likely to be useful going forward.
However, there is a difference between this sort of parroting and techniques like SkypeMorth.
Namely, TLS is highly standardized protocol, therefore simply not that many subtle things in TLS protocol
could be different and/or suddenly change in one of mimicked implementation(potentially undermining the mimicry).
It is possible that we have a distinguisher right now, but amount of those potential distinguishers is limited.
### Custom Handshake
It is possible to create custom handshake by
1) Use `HelloCustom` as an argument for `UClient()` to get empty config
@ -63,6 +98,29 @@ If you need to manually control all the bytes on the wire(certainly not recommen
you can set UConn.HandshakeStateBuilt = true, and marshal clientHello into UConn.HandshakeState.Hello.raw yourself.
In this case you will be responsible for modifying other parts of Config and ClientHelloMsg to reflect your setup
and not confuse "crypto/tls", which will be processing response from server.
## Roller
A simple wrapper, that allows to easily use multiple latest(auto-updated) fingerprints.
```Golang
// NewRoller creates Roller object with default range of HelloIDs to cycle
// through until a working/unblocked one is found.
func NewRoller() (*Roller, error)
```
```Golang
// Dial attempts to connect to given address using different HelloIDs.
// If a working HelloID is found, it is used again for subsequent Dials.
// If tcp connection fails or all HelloIDs are tried, returns with last error.
//
// Usage examples:
//
// Dial("tcp4", "google.com:443", "google.com")
// Dial("tcp", "10.23.144.22:443", "mywebserver.org")
func (c *Roller) Dial(network, addr, serverName string) (*UConn, error)
```
## Fake Session Tickets
Fake session tickets is a very nifty trick that allows power users to hide parts of handshake, which may have some very fingerprintable features of handshake, and saves 1 RTT.
Currently, there is a simple function to set session ticket to any desired state:
@ -85,7 +143,7 @@ See full list of `clientHelloID` values [here](https://godoc.org/github.com/refr
There are different behaviors you can get, depending on your `clientHelloID`:
1. ```utls.HelloRandomized``` adds/reorders extensions, ciphersuites, etc. randomly.
`HelloRandomized` adds ALPN in 50% of cases, you may want to use `HelloRandomizedALPN` or
`HelloRandomized` adds ALPN in a percentage of cases, you may want to use `HelloRandomizedALPN` or
`HelloRandomizedNoALPN` to choose specific behavior explicitly, as ALPN might affect application layer.
2. ```utls.HelloGolang```
HelloGolang will use default "crypto/tls" handshake marshaling codepath, which WILL

View file

@ -52,6 +52,37 @@ func HttpGetByHelloID(hostname string, addr string, helloID tls.ClientHelloID) (
return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol)
}
// this example generates a randomized fingeprint, then re-uses it in a follow-up connection
func HttpGetConsistentRandomized(hostname string, addr string) (*http.Response, error) {
config := tls.Config{ServerName: hostname}
tcpConn, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, fmt.Errorf("net.DialTimeout error: %+v", err)
}
uTlsConn := tls.UClient(tcpConn, &config, tls.HelloRandomized)
defer uTlsConn.Close()
err = uTlsConn.Handshake()
if err != nil {
return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err)
}
uTlsConn.Close()
// At this point uTlsConn.ClientHelloID holds a seed that was used to generate
// randomized fingerprint. Now we can establish second connection with same fp
tcpConn2, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, fmt.Errorf("net.DialTimeout error: %+v", err)
}
uTlsConn2 := tls.UClient(tcpConn2, &config, uTlsConn.ClientHelloID)
defer uTlsConn2.Close()
err = uTlsConn2.Handshake()
if err != nil {
return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err)
}
return httpGetOverConn(uTlsConn2, uTlsConn2.HandshakeState.ServerHello.AlpnProtocol)
}
func HttpGetExplicitRandom(hostname string, addr string) (*http.Response, error) {
dialConn, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
@ -306,7 +337,6 @@ func forgeConn() {
}
func main() {
var response *http.Response
var err error
@ -325,11 +355,11 @@ func main() {
fmt.Printf("#> HttpGetByHelloID(HelloChrome_62) response: %+s\n", dumpResponseNoBody(response))
}
response, err = HttpGetByHelloID(requestHostname, requestAddr, tls.HelloRandomizedNoALPN)
response, err = HttpGetConsistentRandomized(requestHostname, requestAddr)
if err != nil {
fmt.Printf("#> HttpGetByHelloID(Randomized) failed: %+v\n", err)
fmt.Printf("#> HttpGetConsistentRandomized() failed: %+v\n", err)
} else {
fmt.Printf("#> HttpGetByHelloID(Randomized) response: %+s\n", dumpResponseNoBody(response))
fmt.Printf("#> HttpGetConsistentRandomized() response: %+s\n", dumpResponseNoBody(response))
}
response, err = HttpGetExplicitRandom(requestHostname, requestAddr)

View file

@ -260,7 +260,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
// Only extensionCookie, extensionPreSharedKey, extensionKeyShare, extensionEarlyData, extensionSupportedVersions,
// and utlsExtensionPadding are supposed to change
if hs.uconn != nil {
if hs.uconn.clientHelloID != HelloGolang {
if hs.uconn.ClientHelloID != HelloGolang {
if len(hs.hello.pskIdentities) > 0 {
// TODO: wait for someone who cares about PSK to implement
return errors.New("uTLS does not support reprocessing of PSK key triggered by HelloRetryRequest")
@ -292,10 +292,11 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error {
if !cookieFound {
// pick a random index where to add cookieExtension
// -2 instead of -1 is a lazy way to ensure that PSK is still a last extension
cookieIndex, err := getRandInt(len(hs.uconn.Extensions) - 2)
p, err := newPRNG()
if err != nil {
return err
}
cookieIndex := p.Intn(len(hs.uconn.Extensions) - 2)
if cookieIndex >= len(hs.uconn.Extensions) {
// this check is for empty hs.uconn.Extensions
return fmt.Errorf("cookieIndex >= len(hs.uconn.Extensions): %v >= %v",

View file

@ -59,29 +59,39 @@ var (
)
type ClientHelloID struct {
Browser string
Version uint16
// TODO: consider adding OS?
Client string
// Version specifies version of a mimicked clients (e.g. browsers).
// Not used in randomized, custom handshake, and default Go.
Version string
// Seed is only used for randomized fingerprints to seed PRNG.
// Must not be modified once set.
Seed *PRNGSeed
}
func (p *ClientHelloID) Str() string {
return fmt.Sprintf("%s-%d", p.Browser, p.Version)
return fmt.Sprintf("%s-%s", p.Client, p.Version)
}
func (p *ClientHelloID) IsSet() bool {
return (p.Client == "") && (p.Version == "")
}
const (
helloGolang = "Golang"
helloRandomized = "Randomized"
helloCustom = "Custom"
helloFirefox = "Firefox"
helloChrome = "Chrome"
helloIOS = "iOS"
helloAndroid = "Android"
)
// clients
helloGolang = "Golang"
helloRandomized = "Randomized"
helloRandomizedALPN = "Randomized-ALPN"
helloRandomizedNoALPN = "Randomized-NoALPN"
helloCustom = "Custom"
helloFirefox = "Firefox"
helloChrome = "Chrome"
helloIOS = "iOS"
helloAndroid = "Android"
const (
helloAutoVers = iota
helloRandomizedALPN
helloRandomizedNoALPN
// versions
helloAutoVers = "0"
)
type ClientHelloSpec struct {
@ -104,30 +114,30 @@ var (
// overwrite your changes to Hello(Config, Session are fine).
// You might want to call BuildHandshakeState() before applying any changes.
// UConn.Extensions will be completely ignored.
HelloGolang = ClientHelloID{helloGolang, helloAutoVers}
HelloGolang = ClientHelloID{helloGolang, helloAutoVers, nil}
// HelloCustom will prepare ClientHello with empty uconn.Extensions so you can fill it with
// TLSExtensions manually or use ApplyPreset function
HelloCustom = ClientHelloID{helloCustom, helloAutoVers}
HelloCustom = ClientHelloID{helloCustom, helloAutoVers, nil}
// HelloRandomized* randomly adds/reorders extensions, ciphersuites, etc.
HelloRandomized = ClientHelloID{helloRandomized, helloAutoVers}
HelloRandomizedALPN = ClientHelloID{helloRandomized, helloRandomizedALPN}
HelloRandomizedNoALPN = ClientHelloID{helloRandomized, helloRandomizedNoALPN}
HelloRandomized = ClientHelloID{helloRandomized, helloAutoVers, nil}
HelloRandomizedALPN = ClientHelloID{helloRandomizedALPN, helloAutoVers, nil}
HelloRandomizedNoALPN = ClientHelloID{helloRandomizedNoALPN, helloAutoVers, nil}
// The rest will will parrot given browser.
HelloFirefox_Auto = HelloFirefox_63
HelloFirefox_55 = ClientHelloID{helloFirefox, 55}
HelloFirefox_56 = ClientHelloID{helloFirefox, 56}
HelloFirefox_63 = ClientHelloID{helloFirefox, 63}
HelloFirefox_55 = ClientHelloID{helloFirefox, "55", nil}
HelloFirefox_56 = ClientHelloID{helloFirefox, "56", nil}
HelloFirefox_63 = ClientHelloID{helloFirefox, "63", nil}
HelloChrome_Auto = HelloChrome_70
HelloChrome_58 = ClientHelloID{helloChrome, 58}
HelloChrome_62 = ClientHelloID{helloChrome, 62}
HelloChrome_70 = ClientHelloID{helloChrome, 70}
HelloChrome_58 = ClientHelloID{helloChrome, "58", nil}
HelloChrome_62 = ClientHelloID{helloChrome, "62", nil}
HelloChrome_70 = ClientHelloID{helloChrome, "70", nil}
HelloIOS_Auto = HelloIOS_11_1
HelloIOS_11_1 = ClientHelloID{helloIOS, 111}
HelloIOS_11_1 = ClientHelloID{helloIOS, "111", nil}
)
// based on spec's GreaseStyle, GREASE_PLACEHOLDER may be replaced by another GREASE value

View file

@ -21,7 +21,7 @@ type UConn struct {
*Conn
Extensions []TLSExtension
clientHelloID ClientHelloID
ClientHelloID ClientHelloID
ClientHelloBuilt bool
HandshakeState ClientHandshakeState
@ -40,7 +40,7 @@ func UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID) *UConn
}
tlsConn := Conn{conn: conn, config: config, isClient: true}
handshakeState := ClientHandshakeState{C: &tlsConn, Hello: &ClientHelloMsg{}}
uconn := UConn{Conn: &tlsConn, clientHelloID: clientHelloID, HandshakeState: handshakeState}
uconn := UConn{Conn: &tlsConn, ClientHelloID: clientHelloID, HandshakeState: handshakeState}
uconn.HandshakeState.uconn = &uconn
return &uconn
}
@ -58,7 +58,7 @@ func UClient(conn net.Conn, config *Config, clientHelloID ClientHelloID) *UConn
// amd should only be called explicitly to inspect/change fields of
// default/mimicked ClientHello.
func (uconn *UConn) BuildHandshakeState() error {
if uconn.clientHelloID == HelloGolang {
if uconn.ClientHelloID == HelloGolang {
if uconn.ClientHelloBuilt {
return nil
}
@ -74,7 +74,7 @@ func (uconn *UConn) BuildHandshakeState() error {
uconn.HandshakeState.C = uconn.Conn
} else {
if !uconn.ClientHelloBuilt {
err := uconn.applyPresetByID(uconn.clientHelloID)
err := uconn.applyPresetByID(uconn.ClientHelloID)
if err != nil {
return err
}

View file

@ -5,16 +5,13 @@
package tls
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"sort"
"strconv"
"time"
)
func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
@ -326,25 +323,15 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) {
var spec ClientHelloSpec
uconn.ClientHelloID = id
// choose/generate the spec
switch id {
case HelloRandomized:
if tossBiasedCoin(0.5) {
return uconn.applyPresetByID(HelloRandomizedALPN)
} else {
return uconn.applyPresetByID(HelloRandomizedNoALPN)
}
case HelloRandomizedALPN:
spec, err = uconn.generateRandomizedSpec(true)
switch id.Client {
case helloRandomized, helloRandomizedNoALPN, helloRandomizedALPN:
spec, err = uconn.generateRandomizedSpec()
if err != nil {
return err
}
case HelloRandomizedNoALPN:
spec, err = uconn.generateRandomizedSpec(false)
if err != nil {
return err
}
case HelloCustom:
case helloCustom:
return nil
default:
@ -354,7 +341,6 @@ func (uconn *UConn) applyPresetByID(id ClientHelloID) (err error) {
}
}
uconn.clientHelloID = id
return uconn.ApplyPreset(&spec)
}
@ -486,24 +472,51 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error {
return nil
}
func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, error) {
func (uconn *UConn) generateRandomizedSpec() (ClientHelloSpec, error) {
p := ClientHelloSpec{}
if uconn.ClientHelloID.Seed == nil {
seed, err := NewPRNGSeed()
if err != nil {
return p, err
}
uconn.ClientHelloID.Seed = seed
}
r := newPRNGWithSeed(uconn.ClientHelloID.Seed)
id := uconn.ClientHelloID
var WithALPN bool
switch id.Client {
case helloRandomizedALPN:
WithALPN = true
case helloRandomizedNoALPN:
WithALPN = false
case helloRandomized:
if r.FlipWeightedCoin(0.7) {
WithALPN = true
} else {
WithALPN = false
}
default:
return p, fmt.Errorf("using non-randomized ClientHelloID %v to generate randomized spec", id.Client)
}
p.CipherSuites = make([]uint16, len(defaultCipherSuites()))
copy(p.CipherSuites, defaultCipherSuites())
shuffledSuites, err := shuffledCiphers()
shuffledSuites, err := shuffledCiphers(r)
if err != nil {
return p, err
}
if tossBiasedCoin(0.4) {
if r.FlipWeightedCoin(0.4) {
p.TLSVersMin = VersionTLS10
p.TLSVersMax = VersionTLS13
tls13ciphers := defaultCipherSuitesTLS13()
err = shuffleUInts16(tls13ciphers)
if err != nil {
return p, err
}
r.rand.Shuffle(len(tls13ciphers), func(i, j int) {
tls13ciphers[i], tls13ciphers[j] = tls13ciphers[j], tls13ciphers[i]
})
// appending TLS 1.3 ciphers before TLS 1.2, since that's what popular implementations do
shuffledSuites = append(tls13ciphers, shuffledSuites...)
@ -514,7 +527,7 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
p.TLSVersMax = VersionTLS12
}
p.CipherSuites = removeRandomCiphers(shuffledSuites, 0.4)
p.CipherSuites = removeRandomCiphers(r, shuffledSuites, 0.4)
sni := SNIExtension{uconn.config.ServerName}
sessionTicket := SessionTicketExtension{Session: uconn.HandshakeState.Session}
@ -528,26 +541,25 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
PKCS1WithSHA512,
}
if tossBiasedCoin(0.63) {
if r.FlipWeightedCoin(0.63) {
sigAndHashAlgos = append(sigAndHashAlgos, ECDSAWithSHA1)
}
if tossBiasedCoin(0.59) {
if r.FlipWeightedCoin(0.59) {
sigAndHashAlgos = append(sigAndHashAlgos, ECDSAWithP521AndSHA512)
}
if tossBiasedCoin(0.51) || p.TLSVersMax == VersionTLS13 {
if r.FlipWeightedCoin(0.51) || p.TLSVersMax == VersionTLS13 {
// https://tools.ietf.org/html/rfc8446 says "...RSASSA-PSS (which is mandatory in TLS 1.3)..."
sigAndHashAlgos = append(sigAndHashAlgos, PSSWithSHA256)
if tossBiasedCoin(0.9) {
if r.FlipWeightedCoin(0.9) {
// these usually go together
sigAndHashAlgos = append(sigAndHashAlgos, PSSWithSHA384)
sigAndHashAlgos = append(sigAndHashAlgos, PSSWithSHA512)
}
}
err = shuffleSignatures(sigAndHashAlgos)
if err != nil {
return p, err
}
r.rand.Shuffle(len(sigAndHashAlgos), func(i, j int) {
sigAndHashAlgos[i], sigAndHashAlgos[j] = sigAndHashAlgos[j], sigAndHashAlgos[i]
})
sigAndHash := SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: sigAndHashAlgos}
status := StatusRequestExtension{}
@ -556,11 +568,11 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
points := SupportedPointsExtension{SupportedPoints: []byte{pointFormatUncompressed}}
curveIDs := []CurveID{}
if tossBiasedCoin(0.71) || p.TLSVersMax == VersionTLS13 {
if r.FlipWeightedCoin(0.71) || p.TLSVersMax == VersionTLS13 {
curveIDs = append(curveIDs, X25519)
}
curveIDs = append(curveIDs, CurveP256, CurveP384)
if tossBiasedCoin(0.46) {
if r.FlipWeightedCoin(0.46) {
curveIDs = append(curveIDs, CurveP521)
}
@ -586,28 +598,28 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
p.Extensions = append(p.Extensions, &alpn)
}
if tossBiasedCoin(0.62) || p.TLSVersMax == VersionTLS13 {
if r.FlipWeightedCoin(0.62) || p.TLSVersMax == VersionTLS13 {
// always include for TLS 1.3, since TLS 1.3 ClientHellos are often over 256 bytes
// and that's when padding is required to work around buggy middleboxes
p.Extensions = append(p.Extensions, &padding)
}
if tossBiasedCoin(0.74) {
if r.FlipWeightedCoin(0.74) {
p.Extensions = append(p.Extensions, &status)
}
if tossBiasedCoin(0.46) {
if r.FlipWeightedCoin(0.46) {
p.Extensions = append(p.Extensions, &sct)
}
if tossBiasedCoin(0.75) {
if r.FlipWeightedCoin(0.75) {
p.Extensions = append(p.Extensions, &reneg)
}
if tossBiasedCoin(0.77) {
if r.FlipWeightedCoin(0.77) {
p.Extensions = append(p.Extensions, &ems)
}
if p.TLSVersMax == VersionTLS13 {
ks := KeyShareExtension{[]KeyShare{
{Group: X25519}, // the key for the group will be generated later
}}
if tossBiasedCoin(0.25) {
if r.FlipWeightedCoin(0.25) {
// do not ADD second keyShare because crypto/tls does not support multiple ecdheParams
// TODO: add it back when they implement multiple keyShares, or implement it oursevles
// ks.KeyShares = append(ks.KeyShares, KeyShare{Group: CurveP256})
@ -619,10 +631,9 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
}
p.Extensions = append(p.Extensions, &ks, &pskExchangeModes, &supportedVersionsExt)
}
err = shuffleTLSExtensions(p.Extensions)
if err != nil {
return p, err
}
r.rand.Shuffle(len(p.Extensions), func(i, j int) {
p.Extensions[i], p.Extensions[j] = p.Extensions[j], p.Extensions[i]
})
err = uconn.SetTLSVers(p.TLSVersMin, p.TLSVersMax)
if err != nil {
return p, err
@ -631,26 +642,7 @@ func (uconn *UConn) generateRandomizedSpec(WithALPN bool) (ClientHelloSpec, erro
return p, nil
}
func tossBiasedCoin(probability float32) bool {
// probability is expected to be in [0,1]
// this function never returns errors for ease of use
const precision = 0xffff
threshold := float32(precision) * probability
value, err := getRandInt(precision)
if err != nil {
// I doubt that this code will ever actually be used, as other functions are expected to complain
// about used source of entropy. Nonetheless, this is more than enough for given purpose
return ((time.Now().Unix() & 1) == 0)
}
if float32(value) <= threshold {
return true
} else {
return false
}
}
func removeRandomCiphers(s []uint16, maxRemovalProbability float32) []uint16 {
func removeRandomCiphers(r *prng, s []uint16, maxRemovalProbability float64) []uint16 {
// removes elements in place
// probability to remove increases for further elements
// never remove first cipher
@ -659,10 +651,10 @@ func removeRandomCiphers(s []uint16, maxRemovalProbability float32) []uint16 {
}
// remove random elements
floatLen := float32(len(s))
floatLen := float64(len(s))
sliceLen := len(s)
for i := 1; i < sliceLen; i++ {
if tossBiasedCoin(maxRemovalProbability * float32(i) / floatLen) {
if r.FlipWeightedCoin(maxRemovalProbability * float64(i) / floatLen) {
s = append(s[:i], s[i+1:]...)
sliceLen--
i--
@ -671,46 +663,9 @@ func removeRandomCiphers(s []uint16, maxRemovalProbability float32) []uint16 {
return s[:sliceLen]
}
func removeRC4Ciphers(s []uint16) []uint16 {
// removes elements in place
sliceLen := len(s)
for i := 0; i < sliceLen; i++ {
cipher := s[i]
if cipher == TLS_ECDHE_ECDSA_WITH_RC4_128_SHA ||
cipher == TLS_ECDHE_RSA_WITH_RC4_128_SHA ||
cipher == TLS_RSA_WITH_RC4_128_SHA {
s = append(s[:i], s[i+1:]...)
sliceLen--
i--
}
}
return s[:sliceLen]
}
func getRandInt(max int) (int, error) {
bigInt, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
return int(bigInt.Int64()), err
}
func getRandPerm(n int) ([]int, error) {
permArray := make([]int, n)
for i := 1; i < n; i++ {
j, err := getRandInt(i + 1)
if err != nil {
return permArray, err
}
permArray[i] = permArray[j]
permArray[j] = i
}
return permArray, nil
}
func shuffledCiphers() ([]uint16, error) {
func shuffledCiphers(r *prng) ([]uint16, error) {
ciphers := make(sortableCiphers, len(cipherSuites))
perm, err := getRandPerm(len(cipherSuites))
if err != nil {
return nil, err
}
perm := r.Perm(len(cipherSuites))
for i, suite := range cipherSuites {
ciphers[i] = sortableCipher{suite: suite.id,
isObsolete: ((suite.flags & suiteTLS12) == 0),
@ -754,41 +709,18 @@ func (ciphers sortableCiphers) GetCiphers() []uint16 {
return cipherIDs
}
// so much for generics
func shuffleTLSExtensions(s []TLSExtension) error {
// shuffles array in place
perm, err := getRandPerm(len(s))
if err != nil {
return err
func removeRC4Ciphers(s []uint16) []uint16 {
// removes elements in place
sliceLen := len(s)
for i := 0; i < sliceLen; i++ {
cipher := s[i]
if cipher == TLS_ECDHE_ECDSA_WITH_RC4_128_SHA ||
cipher == TLS_ECDHE_RSA_WITH_RC4_128_SHA ||
cipher == TLS_RSA_WITH_RC4_128_SHA {
s = append(s[:i], s[i+1:]...)
sliceLen--
i--
}
}
for i := range s {
s[i], s[perm[i]] = s[perm[i]], s[i]
}
return nil
}
// so much for generics
func shuffleSignatures(s []SignatureScheme) error {
// shuffles array in place
perm, err := getRandPerm(len(s))
if err != nil {
return err
}
for i := range s {
s[i], s[perm[i]] = s[perm[i]], s[i]
}
return nil
}
// so much for generics
func shuffleUInts16(s []uint16) error {
// shuffles array in place
perm, err := getRandPerm(len(s))
if err != nil {
return err
}
for i := range s {
s[i], s[perm[i]] = s[perm[i]], s[i]
}
return nil
return s[:sliceLen]
}

202
u_prng.go Normal file
View file

@ -0,0 +1,202 @@
/*
* Copyright (c) 2019, Psiphon Inc.
* All rights reserved.
*
* Released under utls licence:
* https://github.com/refraction-networking/utls/blob/master/LICENSE
*/
// This code is a pared down version of:
// https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/158caea562287284cc3fa5fcd1b3c97b1addf659/psiphon/common/prng/prng.go
package tls
import (
crypto_rand "crypto/rand"
"encoding/binary"
"math"
"math/rand"
"sync"
"github.com/Yawning/chacha20"
)
const (
PRNGSeedLength = 32
)
// PRNGSeed is a PRNG seed.
type PRNGSeed [PRNGSeedLength]byte
// NewPRNGSeed creates a new PRNG seed using crypto/rand.Read.
func NewPRNGSeed() (*PRNGSeed, error) {
seed := new(PRNGSeed)
_, err := crypto_rand.Read(seed[:])
if err != nil {
return nil, err
}
return seed, nil
}
// prng is a seeded, unbiased PRNG based on chacha20. that is suitable for use
// cases such as obfuscation.
//
// Seeding is based on crypto/rand.Read and the PRNG stream is provided by
// chacha20.
//
// This PRNG is _not_ for security use cases including production cryptographic
// key generation.
//
// Limitations: there is a cycle in the PRNG stream, after roughly 2^64 * 2^38-64
// bytes.
//
// It is safe to make concurrent calls to a PRNG instance.
//
// PRNG conforms to io.Reader and math/rand.Source, with additional helper
// functions.
type prng struct {
rand *rand.Rand
randomStreamMutex sync.Mutex
randomStreamSeed *PRNGSeed
randomStream *chacha20.Cipher
randomStreamUsed uint64
randomStreamRekeyCount uint64
}
// newPRNG generates a seed and creates a PRNG with that seed.
func newPRNG() (*prng, error) {
seed, err := NewPRNGSeed()
if err != nil {
return nil, err
}
return newPRNGWithSeed(seed), nil
}
// newPRNGWithSeed initializes a new PRNG using an existing seed.
func newPRNGWithSeed(seed *PRNGSeed) *prng {
p := &prng{
randomStreamSeed: seed,
}
p.rekey()
p.rand = rand.New(p)
return p
}
// Read reads random bytes from the PRNG stream into b. Read conforms to
// io.Reader and always returns len(p), nil.
func (p *prng) Read(b []byte) (int, error) {
p.randomStreamMutex.Lock()
defer p.randomStreamMutex.Unlock()
// Re-key before reaching the 2^38-64 chacha20 key stream limit.
if p.randomStreamUsed+uint64(len(b)) >= uint64(1<<38-64) {
p.rekey()
}
p.randomStream.KeyStream(b)
p.randomStreamUsed += uint64(len(b))
return len(b), nil
}
func (p *prng) rekey() {
// chacha20 has a stream limit of 2^38-64. Before that limit is reached,
// the cipher must be rekeyed. To rekey without changing the seed, we use
// a counter for the nonce.
//
// Limitation: the counter wraps at 2^64, which produces a cycle in the
// PRNG after 2^64 * 2^38-64 bytes.
//
// TODO: this could be extended by using all 2^96 bits of the nonce for
// the counter; and even further by using the 24 byte XChaCha20 nonce.
var randomKeyNonce [12]byte
binary.BigEndian.PutUint64(randomKeyNonce[0:8], p.randomStreamRekeyCount)
var err error
p.randomStream, err = chacha20.NewCipher(
p.randomStreamSeed[:], randomKeyNonce[:])
if err != nil {
// Functions returning random values, which may call rekey, don't
// return an error. As of github.com/Yawning/chacha20 rev. e3b1f968,
// the only possible errors from chacha20.NewCipher invalid key or
// nonce size, and since we use the correct sizes, there should never
// be an error here. So panic in this unexpected case.
panic(err)
}
p.randomStreamRekeyCount += 1
p.randomStreamUsed = 0
}
// Int63 is equivilent to math/read.Int63.
func (p *prng) Int63() int64 {
i := p.Uint64()
return int64(i & (1<<63 - 1))
}
// Int63 is equivilent to math/read.Uint64.
func (p *prng) Uint64() uint64 {
var b [8]byte
p.Read(b[:])
return binary.BigEndian.Uint64(b[:])
}
// Seed must exist in order to use a PRNG as a math/rand.Source. This call is
// not supported and ignored.
func (p *prng) Seed(_ int64) {
}
// FlipWeightedCoin returns the result of a weighted
// random coin flip. If the weight is 0.5, the outcome
// is equally likely to be true or false. If the weight
// is 1.0, the outcome is always true, and if the
// weight is 0.0, the outcome is always false.
//
// Input weights > 1.0 are treated as 1.0.
func (p *prng) FlipWeightedCoin(weight float64) bool {
if weight > 1.0 {
weight = 1.0
}
f := float64(p.Int63()) / float64(math.MaxInt64)
return f > 1.0-weight
}
// Intn is equivilent to math/read.Intn, except it returns 0 if n <= 0
// instead of panicking.
func (p *prng) Intn(n int) int {
if n <= 0 {
return 0
}
return p.rand.Intn(n)
}
// Int63n is equivilent to math/read.Int63n, except it returns 0 if n <= 0
// instead of panicking.
func (p *prng) Int63n(n int64) int64 {
if n <= 0 {
return 0
}
return p.rand.Int63n(n)
}
// Intn is equivilent to math/read.Perm.
func (p *prng) Perm(n int) []int {
return p.rand.Perm(n)
}
// Range selects a random integer in [min, max].
// If min < 0, min is set to 0. If max < min, min is returned.
func (p *prng) Range(min, max int) int {
if min < 0 {
min = 0
}
if max < min {
return min
}
n := p.Intn(max - min + 1)
n += min
return n
}

View file

@ -12,21 +12,21 @@ type Roller struct {
WorkingHelloID *ClientHelloID
TcpDialTimeout time.Duration
TlsHandshakeTimeout time.Duration
r *prng
}
// NewRoller creates Roller object with default range of HelloIDs to cycle through until a
// working/unblocked one is found.
func NewRoller() (*Roller, error) {
tcpDialTimeoutInc, err := getRandInt(14)
r, err := newPRNG()
if err != nil {
return nil, err
}
tcpDialTimeoutInc := r.Intn(14)
tcpDialTimeoutInc = 7 + tcpDialTimeoutInc
tlsHandshakeTimeoutInc, err := getRandInt(20)
if err != nil {
return nil, err
}
tlsHandshakeTimeoutInc := r.Intn(20)
tlsHandshakeTimeoutInc = 11 + tlsHandshakeTimeoutInc
return &Roller{
@ -38,6 +38,7 @@ func NewRoller() (*Roller, error) {
},
TcpDialTimeout: time.Second * time.Duration(tcpDialTimeoutInc),
TlsHandshakeTimeout: time.Second * time.Duration(tlsHandshakeTimeoutInc),
r: r,
}, nil
}
@ -49,25 +50,32 @@ func NewRoller() (*Roller, error) {
// Dial("tcp4", "google.com:443", "google.com")
// Dial("tcp", "10.23.144.22:443", "mywebserver.org")
func (c *Roller) Dial(network, addr, serverName string) (*UConn, error) {
helloIDs, err := shuffleClientHelloIDs(c.HelloIDs)
if err != nil {
return nil, err
}
helloIDs := make([]ClientHelloID, len(c.HelloIDs))
copy(helloIDs, c.HelloIDs)
c.r.rand.Shuffle(len(c.HelloIDs), func(i, j int) {
helloIDs[i], helloIDs[j] = helloIDs[j], helloIDs[i]
})
c.HelloIDMu.Lock()
workingHelloId := c.WorkingHelloID // keep using same helloID, if it works
c.HelloIDMu.Unlock()
if workingHelloId != nil {
helloIDFound := false
for i, ID := range helloIDs {
if ID == *workingHelloId {
helloIDs[i] = helloIDs[0]
helloIDs[0] = *workingHelloId // push working hello ID first
helloIDFound = true
break
}
}
if !helloIDFound {
helloIDs = append([]ClientHelloID{*workingHelloId}, helloIDs...)
}
}
var tcpConn net.Conn
var err error
for _, helloID := range helloIDs {
tcpConn, err = net.DialTimeout(network, addr, c.TcpDialTimeout)
if err != nil {
@ -84,23 +92,9 @@ func (c *Roller) Dial(network, addr, serverName string) (*UConn, error) {
}
c.HelloIDMu.Lock()
c.WorkingHelloID = &helloID
c.WorkingHelloID = &client.ClientHelloID
c.HelloIDMu.Unlock()
return client, err
}
return nil, err
}
// returns a shuffled copy of input
func shuffleClientHelloIDs(helloIDs []ClientHelloID) ([]ClientHelloID, error) {
perm, err := getRandPerm(len(helloIDs))
if err != nil {
return nil, err
}
shuffled := make([]ClientHelloID, len(helloIDs))
for i, randI := range perm {
shuffled[i] = helloIDs[randI]
}
return shuffled, nil
}