Kaynağa Gözat

Make ClientHello extensions match modern Chrome
* Add Config.EmulateChrome option to enable Chrome-like
ClientHello
* Add "[Psiphon]" tag to all significant code changes
in "tls" package fork
* Re-order marshaled extensions to match modern Chrome
* Add/remove/reconfigure extensions to match Chrome
* Stub extended master secret and channel ID extensions
will suffice as long as server doesn't use these
extensions

Rod Hynes 9 yıl önce
ebeveyn
işleme
ad279ef755

+ 5 - 58
psiphon/common/tls/cipher_suites.go

@@ -9,13 +9,11 @@ import (
 	"crypto/cipher"
 	"crypto/des"
 	"crypto/hmac"
-	"crypto/rand"
 	"crypto/rc4"
 	"crypto/sha1"
 	"crypto/sha256"
 	"crypto/x509"
 	"hash"
-	"math/big"
 
 	"github.com/Psiphon-Inc/crypto/chacha20poly1305"
 )
@@ -105,69 +103,18 @@ var cipherSuites = []*cipherSuite{
 	{TLS_ECDHE_RSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheRSAKA, suiteECDHE | suiteDefaultOff, cipherRC4, macSHA1, nil},
 	{TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 16, 20, 0, ecdheECDSAKA, suiteECDHE | suiteECDSA | suiteDefaultOff, cipherRC4, macSHA1, nil},
 
+	// [Psiphon]
+	// TLS_..._CHACHA20_POLY1305_OLD are required for EmulateChrome.
 	{TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_OLD, 32, 0, 12, ecdheRSAKA, suiteDefaultOff | suiteECDHE | suiteTLS12, nil, nil, aeadChaCha20Poly1305},
 	{TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_OLD, 32, 0, 12, ecdheECDSAKA, suiteDefaultOff | suiteECDHE | suiteECDSA | suiteTLS12, nil, nil, aeadChaCha20Poly1305},
-	{TLS_GREASE_0A0A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_1A1A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_2A2A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_3A3A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_4A4A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_5A5A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_6A6A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_7A7A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_8A8A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_9A9A, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_AAAA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_BABA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_CACA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_DADA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_EAEA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
-	{TLS_GREASE_FAFA, 0, 0, 0, nil, suiteDefaultOff, nil, nil, nil},
 }
 
+// [Psiphon]
+// The following are not stock golang cipher suites and must be ignored
+// when running automated tests against pre-recorded "testdata".
 var ignoreCipherSuites = []uint16{
 	TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_OLD,
 	TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_OLD,
-	TLS_GREASE_0A0A,
-	TLS_GREASE_1A1A,
-	TLS_GREASE_2A2A,
-	TLS_GREASE_3A3A,
-	TLS_GREASE_4A4A,
-	TLS_GREASE_5A5A,
-	TLS_GREASE_6A6A,
-	TLS_GREASE_7A7A,
-	TLS_GREASE_8A8A,
-	TLS_GREASE_9A9A,
-	TLS_GREASE_AAAA,
-	TLS_GREASE_BABA,
-	TLS_GREASE_CACA,
-	TLS_GREASE_DADA,
-	TLS_GREASE_EAEA,
-	TLS_GREASE_FAFA,
-}
-
-func RandomGREASESuite() uint16 {
-	suites := []uint16{
-		TLS_GREASE_0A0A,
-		TLS_GREASE_1A1A,
-		TLS_GREASE_2A2A,
-		TLS_GREASE_3A3A,
-		TLS_GREASE_4A4A,
-		TLS_GREASE_5A5A,
-		TLS_GREASE_6A6A,
-		TLS_GREASE_7A7A,
-		TLS_GREASE_8A8A,
-		TLS_GREASE_9A9A,
-		TLS_GREASE_AAAA,
-		TLS_GREASE_BABA,
-		TLS_GREASE_CACA,
-		TLS_GREASE_DADA,
-		TLS_GREASE_EAEA,
-		TLS_GREASE_FAFA,
-	}
-
-	i, _ := rand.Int(rand.Reader, big.NewInt(int64(len(suites))))
-	return suites[int(i.Int64())]
 }
 
 func cipherRC4(key, iv []byte, isRead bool) interface{} {

+ 17 - 0
psiphon/common/tls/common.go

@@ -83,6 +83,11 @@ const (
 	extensionSessionTicket       uint16 = 35
 	extensionNextProtoNeg        uint16 = 13172 // not IANA assigned
 	extensionRenegotiationInfo   uint16 = 0xff01
+
+	// [Psiphon]
+	// Additional extensions required for EmulateChrome.
+	extensionExtendedMasterSecret uint16 = 23
+	extensionChannelID            uint16 = 30032 // not IANA assigned
 )
 
 // TLS signaling cipher suite values
@@ -132,6 +137,10 @@ const (
 	hashSHA1   uint8 = 2
 	hashSHA256 uint8 = 4
 	hashSHA384 uint8 = 5
+
+	// [Psiphon]
+	// hashSHA512 is required for EmulateChrome.
+	hashSHA512 uint8 = 6
 )
 
 // Signature algorithms for TLS 1.2 (See RFC 5246, section A.4.1)
@@ -507,6 +516,14 @@ type Config struct {
 	// used for debugging.
 	KeyLogWriter io.Writer
 
+	// [Psiphon]
+	// EmulateChrome enables a network traffic obfuscation facility that
+	// configures the client hello to match the traffic signature of modern
+	// Chrome browsers using BoringSSL. This affects the selection and
+	// preference order of ciphersuites, and selection and order of extentions.
+	// CipherSuites is ignored when EmulateChrome is on.
+	EmulateChrome bool
+
 	serverInitOnce sync.Once // guards calling (*Config).serverInit
 
 	// mutex protects sessionTicketKeys and originalConfig.

+ 3 - 0
psiphon/common/tls/example_test.go

@@ -73,6 +73,9 @@ yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx
 	conn.Close()
 }
 
+// [Psiphon]
+// Disable test due to TLSClientConfig type mismatch
+
 /*
 func ExampleConfig_keyLogWriter() {
 	// Debugging TLS applications by decrypting a network traffic capture.

+ 88 - 0
psiphon/common/tls/handshake_client.go

@@ -7,6 +7,9 @@ package tls
 import (
 	"bytes"
 	"crypto"
+	"crypto/rand"
+	"math/big"
+
 	"crypto/ecdsa"
 	"crypto/rsa"
 	"crypto/subtle"
@@ -152,6 +155,85 @@ NextCipherSuite:
 		}
 	}
 
+	// [Psiphon]
+	// Re-configure extensions as required for EmulateChrome.
+	if c.config.EmulateChrome {
+
+		hello.emulateChrome = true
+
+		// Sanity check that expected and required configuration is present
+		if len(hello.compressionMethods) != 1 ||
+			hello.compressionMethods[0] != compressionNone ||
+			!hello.ticketSupported ||
+			!hello.ocspStapling ||
+			!hello.scts ||
+			len(hello.supportedPoints) != 1 ||
+			hello.supportedPoints[0] != pointFormatUncompressed ||
+			!hello.secureRenegotiationSupported {
+
+			return errors.New("tls: unexpected configuration for EmulateChrome")
+		}
+
+		hello.supportedCurves = []CurveID{
+			CurveID(randomGREASEValue()),
+			X25519,
+			CurveP256, // secp256r1
+			CurveP384, // secp384r1
+		}
+
+		hello.cipherSuites = []uint16{
+			randomGREASEValue(),
+			TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+			TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+			TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_OLD,
+			TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_OLD,
+			TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+			TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+			TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+			TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			TLS_RSA_WITH_AES_128_GCM_SHA256,
+			TLS_RSA_WITH_AES_256_GCM_SHA384,
+			TLS_RSA_WITH_AES_128_CBC_SHA,
+			TLS_RSA_WITH_AES_256_CBC_SHA,
+			TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+		}
+
+		if hello.vers >= VersionTLS12 {
+			hello.signatureAndHashes = []signatureAndHash{
+				{hashSHA512, signatureRSA},
+				{hashSHA512, signatureECDSA},
+				{hashSHA256, signatureRSA},
+				{hashSHA256, signatureECDSA},
+				{hashSHA384, signatureRSA},
+				{hashSHA384, signatureECDSA},
+				{hashSHA1, signatureRSA},
+				{hashSHA1, signatureECDSA},
+			}
+		}
+
+		hello.nextProtoNeg = false
+
+		hello.alpnProtocols = []string{"h2", "http/1.1"}
+
+		// The extended master secret and channel ID extensions
+		// code is from:
+		//
+		// https://github.com/google/boringssl/tree/master/ssl/test/runner
+		// https://github.com/google/boringssl/blob/master/LICENSE
+
+		hello.extendedMasterSecretSupported = true
+		// TODO: implement actual support, in case negotiated
+		// https://github.com/google/boringssl/commit/7571292eaca1745f3ecda2374ba1e8163b58c3b5
+
+		hello.channelIDSupported = true
+		// TODO: implement actual support, in case negotiated
+		// https://github.com/google/boringssl/commit/d30a990850457657e3209cb0c27fbe89b3df7ad2
+	}
+
 	if _, err := c.writeRecord(recordTypeHandshake, hello.marshal()); err != nil {
 		return err
 	}
@@ -256,6 +338,12 @@ NextCipherSuite:
 	return nil
 }
 
+func randomGREASEValue() uint16 {
+	values := []uint16{0x0A0A, 0x1A1A, 0x2A2A, 0x3A3A, 0x4A4A, 0x5A5A, 0x6A6A, 0x7A7A, 0x8A8A, 0x9A9A, 0xAAAA, 0xBABA, 0xCACA, 0xDADA, 0xEAEA, 0xFAFA}
+	i, _ := rand.Int(rand.Reader, big.NewInt(int64(len(values))))
+	return values[int(i.Int64())]
+}
+
 func (hs *clientHandshakeState) doFullHandshake() error {
 	c := hs.c
 

+ 145 - 16
psiphon/common/tls/handshake_messages.go

@@ -7,6 +7,13 @@ package tls
 import "bytes"
 
 type clientHelloMsg struct {
+	// [Psiphon]
+	// emulateChrome indicates whether to use Chrome/BoringSSL-like
+	// extension order. This order is used only when emulateChrome
+	// is set to ensure the automated tests run against pre-recorded
+	// "testdata".
+	emulateChrome bool
+
 	raw                          []byte
 	vers                         uint16
 	random                       []byte
@@ -25,6 +32,12 @@ type clientHelloMsg struct {
 	secureRenegotiation          []byte
 	secureRenegotiationSupported bool
 	alpnProtocols                []string
+
+	// [Psiphon]
+	// Additional extensions required for EmulateChrome.
+	// Note: omitted from clientHelloMsg.equal()
+	extendedMasterSecretSupported bool
+	channelIDSupported            bool
 }
 
 func (m *clientHelloMsg) equal(i interface{}) bool {
@@ -106,6 +119,16 @@ func (m *clientHelloMsg) marshal() []byte {
 	if m.scts {
 		numExtensions++
 	}
+
+	// [Psiphon]
+	// Additional extensions required for EmulateChrome.
+	if m.extendedMasterSecretSupported {
+		numExtensions++
+	}
+	if m.channelIDSupported {
+		numExtensions++
+	}
+
 	if numExtensions > 0 {
 		extensionsLength += 4 * numExtensions
 		length += 2 + extensionsLength
@@ -132,19 +155,17 @@ func (m *clientHelloMsg) marshal() []byte {
 	z[0] = uint8(len(m.compressionMethods))
 	copy(z[1:], m.compressionMethods)
 
-	z = z[1+len(m.compressionMethods):]
-	if numExtensions > 0 {
-		z[0] = byte(extensionsLength >> 8)
-		z[1] = byte(extensionsLength)
-		z = z[2:]
-	}
-	if m.nextProtoNeg {
+	// [Psiphon]
+	// The extension marshal order changes as required for EmulateChrome.
+
+	marshalNextProtoNeg := func() {
 		z[0] = byte(extensionNextProtoNeg >> 8)
 		z[1] = byte(extensionNextProtoNeg & 0xff)
 		// The length is always 0
 		z = z[4:]
 	}
-	if len(m.serverName) > 0 {
+
+	marshalServerName := func() {
 		z[0] = byte(extensionServerName >> 8)
 		z[1] = byte(extensionServerName & 0xff)
 		l := len(m.serverName) + 5
@@ -178,7 +199,8 @@ func (m *clientHelloMsg) marshal() []byte {
 		copy(z[5:], []byte(m.serverName))
 		z = z[l:]
 	}
-	if m.ocspStapling {
+
+	marshalStatusRequest := func() {
 		// RFC 4366, section 3.6
 		z[0] = byte(extensionStatusRequest >> 8)
 		z[1] = byte(extensionStatusRequest)
@@ -188,7 +210,8 @@ func (m *clientHelloMsg) marshal() []byte {
 		// Two zero valued uint16s for the two lengths.
 		z = z[9:]
 	}
-	if len(m.supportedCurves) > 0 {
+
+	marshalSupportedCurves := func() {
 		// http://tools.ietf.org/html/rfc4492#section-5.5.1
 		z[0] = byte(extensionSupportedCurves >> 8)
 		z[1] = byte(extensionSupportedCurves)
@@ -205,7 +228,8 @@ func (m *clientHelloMsg) marshal() []byte {
 			z = z[2:]
 		}
 	}
-	if len(m.supportedPoints) > 0 {
+
+	marshalSupportedPoints := func() {
 		// http://tools.ietf.org/html/rfc4492#section-5.5.2
 		z[0] = byte(extensionSupportedPoints >> 8)
 		z[1] = byte(extensionSupportedPoints)
@@ -220,7 +244,8 @@ func (m *clientHelloMsg) marshal() []byte {
 			z = z[1:]
 		}
 	}
-	if m.ticketSupported {
+
+	marshalSessionTicket := func() {
 		// http://tools.ietf.org/html/rfc5077#section-3.2
 		z[0] = byte(extensionSessionTicket >> 8)
 		z[1] = byte(extensionSessionTicket)
@@ -231,7 +256,8 @@ func (m *clientHelloMsg) marshal() []byte {
 		copy(z, m.sessionTicket)
 		z = z[len(m.sessionTicket):]
 	}
-	if len(m.signatureAndHashes) > 0 {
+
+	marshalSignatureAlgorithms := func() {
 		// https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1
 		z[0] = byte(extensionSignatureAlgorithms >> 8)
 		z[1] = byte(extensionSignatureAlgorithms)
@@ -250,7 +276,8 @@ func (m *clientHelloMsg) marshal() []byte {
 			z = z[2:]
 		}
 	}
-	if m.secureRenegotiationSupported {
+
+	marshalRenegotiationInfo := func() {
 		z[0] = byte(extensionRenegotiationInfo >> 8)
 		z[1] = byte(extensionRenegotiationInfo & 0xff)
 		z[2] = 0
@@ -260,7 +287,8 @@ func (m *clientHelloMsg) marshal() []byte {
 		copy(z, m.secureRenegotiation)
 		z = z[len(m.secureRenegotiation):]
 	}
-	if len(m.alpnProtocols) > 0 {
+
+	marshalALPN := func() {
 		z[0] = byte(extensionALPN >> 8)
 		z[1] = byte(extensionALPN & 0xff)
 		lengths := z[2:]
@@ -281,7 +309,8 @@ func (m *clientHelloMsg) marshal() []byte {
 		lengths[0] = byte(stringsLength >> 8)
 		lengths[1] = byte(stringsLength)
 	}
-	if m.scts {
+
+	marshalSCT := func() {
 		// https://tools.ietf.org/html/rfc6962#section-3.3.1
 		z[0] = byte(extensionSCT >> 8)
 		z[1] = byte(extensionSCT)
@@ -289,6 +318,106 @@ func (m *clientHelloMsg) marshal() []byte {
 		z = z[4:]
 	}
 
+	// [Psiphon]
+	// Additional extensions required for EmulateChrome.
+	marshalExtendedMasterSecret := func() {
+		// https://tools.ietf.org/html/draft-ietf-tls-session-hash-01
+		z[0] = byte(extensionExtendedMasterSecret >> 8)
+		z[1] = byte(extensionExtendedMasterSecret & 0xff)
+		z = z[4:]
+	}
+	marshalChannelID := func() {
+		if m.channelIDSupported {
+			z[0] = byte(extensionChannelID >> 8)
+			z[1] = byte(extensionChannelID & 0xff)
+			z = z[4:]
+		}
+	}
+
+	z = z[1+len(m.compressionMethods):]
+	if numExtensions > 0 {
+		z[0] = byte(extensionsLength >> 8)
+		z[1] = byte(extensionsLength)
+		z = z[2:]
+	}
+
+	if m.emulateChrome {
+
+		// [Psiphon]
+		// This code handles extension ordering only; configuration
+		// of extensions as required for EmulateChrome is handled
+		// in Conn.clientHandshae().
+
+		if m.secureRenegotiationSupported {
+			marshalRenegotiationInfo()
+		}
+		if len(m.serverName) > 0 {
+			marshalServerName()
+		}
+		if m.extendedMasterSecretSupported {
+			marshalExtendedMasterSecret()
+		}
+		if m.ticketSupported {
+			marshalSessionTicket()
+		}
+		if len(m.signatureAndHashes) > 0 {
+			marshalSignatureAlgorithms()
+		}
+		if m.ocspStapling {
+			marshalStatusRequest()
+		}
+		if m.scts {
+			marshalSCT()
+		}
+		if m.nextProtoNeg {
+			marshalNextProtoNeg()
+		}
+		if len(m.alpnProtocols) > 0 {
+			marshalALPN()
+		}
+		if m.channelIDSupported {
+			marshalChannelID()
+		}
+		if len(m.supportedPoints) > 0 {
+			marshalSupportedPoints()
+		}
+		if len(m.supportedCurves) > 0 {
+			marshalSupportedCurves()
+		}
+
+	} else {
+		if m.nextProtoNeg {
+			marshalNextProtoNeg()
+		}
+		if len(m.serverName) > 0 {
+			marshalServerName()
+		}
+		if m.ocspStapling {
+			marshalStatusRequest()
+		}
+		if len(m.supportedCurves) > 0 {
+			marshalSupportedCurves()
+		}
+		if len(m.supportedPoints) > 0 {
+			marshalSupportedPoints()
+		}
+		if m.ticketSupported {
+			marshalSessionTicket()
+		}
+		if len(m.signatureAndHashes) > 0 {
+			marshalSignatureAlgorithms()
+		}
+		if m.secureRenegotiationSupported {
+			marshalRenegotiationInfo()
+		}
+		if len(m.alpnProtocols) > 0 {
+			marshalALPN()
+		}
+		if m.scts {
+			marshalSCT()
+		}
+	}
+
 	m.raw = x
 
 	return x

+ 3 - 0
psiphon/common/tls/handshake_server_test.go

@@ -39,6 +39,9 @@ var testConfig *Config
 
 func allCipherSuites() []uint16 {
 
+	// [Psiphon]
+	// Ignore cipher suites added for EmulateChrome.
+
 	//ids := make([]uint16, len(cipherSuites))
 	//for i, suite := range cipherSuites {
 	//	ids[i] = suite.id

+ 1 - 0
psiphon/common/tls/obfuscated.go

@@ -23,6 +23,7 @@ import (
 	"crypto/rand"
 )
 
+// [Psiphon]
 // Obfuscated Session Tickets
 //
 // Obfuscated session tickets is a network traffic obfuscation protocol that appears

+ 2 - 0
psiphon/common/tls/obfuscated_test.go

@@ -34,6 +34,8 @@ import (
 	"time"
 )
 
+// [Psiphon]
+// TestObfuscatedSessionTicket exercises the Obfuscated Session Tickets facility.
 func TestObfuscatedSessionTicket(t *testing.T) {
 
 	var standardSessionTicketKey [32]byte

+ 4 - 0
psiphon/common/tls/prf.go

@@ -189,6 +189,10 @@ func lookupTLSHash(hash uint8) (crypto.Hash, error) {
 		return crypto.SHA256, nil
 	case hashSHA384:
 		return crypto.SHA384, nil
+	// [Psiphon]
+	// hashSHA512 is required for EmulateChrome.
+	case hashSHA512:
+		return crypto.SHA512, nil
 	default:
 		return 0, errors.New("tls: unsupported hash algorithm")
 	}

+ 4 - 0
psiphon/common/tls/tls_test.go

@@ -333,6 +333,8 @@ func TestTLSUniqueMatches(t *testing.T) {
 }
 
 func TestVerifyHostname(t *testing.T) {
+	// [Psiphon]
+	// Disable due to internal import
 	//testenv.MustHaveExternalNetwork(t)
 
 	c, err := Dial("tcp", "www.google.com:https", nil)
@@ -359,6 +361,8 @@ func TestVerifyHostname(t *testing.T) {
 }
 
 func TestVerifyHostnameResumed(t *testing.T) {
+	// [Psiphon]
+	// Disable due to internal import
 	//testenv.MustHaveExternalNetwork(t)
 
 	config := &Config{