Browse Source

Add support for indistingushable TLS

* Use OpenSSL in common configurations instead of Go TLS
* Go's TLS ClientHello is highly distinct and uncommon
* Currently only used for meek
* Currently only available on Android
Rod Hynes 10 years ago
parent
commit
14865e71ac

+ 2 - 0
SampleApps/Psibot/app/src/main/java/ca/psiphon/PsiphonTunnel.java

@@ -319,6 +319,8 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
 
         json.put("EmitBytesTransferred", true);
 
+        json.put("UseIndistinguishableTLS", true);
+
         if (mLocalSocksProxyPort != 0) {
             // When mLocalSocksProxyPort is set, tun2socks is already configured
             // to use that port value. So we force use of the same port.

+ 1 - 1
openssl/README.md

@@ -14,7 +14,7 @@ This directory contains source and scripts to build OpenSSL libraries that can b
 statically linked with Psiphon Tunnel Core.
 
 Mimicking stock TLS implementations is done both at compile time (no-heartbeats)
-and at [runtime](TODO-link) (specific CipherSuites and options).
+and at [runtime](psiphon/opensslConn.go) (specific cipher suites and options).
 
 Android
 --------------------------------------------------------------------------------

+ 1 - 1
psiphon/LookupIP_nobind.go

@@ -27,7 +27,7 @@ import (
 )
 
 // LookupIP resolves a hostname. When BindToDevice is not required, it
-// simply uses net.LookuIP.
+// simply uses net.LookupIP.
 func LookupIP(host string, config *DialConfig) (addrs []net.IP, err error) {
 	if config.DeviceBinder != nil {
 		return nil, ContextError(errors.New("LookupIP with DeviceBinder not supported on this platform"))

+ 1 - 0
psiphon/config.go

@@ -99,6 +99,7 @@ type Config struct {
 	UpgradeDownloadUrl                  string
 	UpgradeDownloadFilename             string
 	EmitBytesTransferred                bool
+	UseIndistinguishableTLS             bool
 }
 
 // LoadConfig parses and validates a JSON format Psiphon config JSON

+ 6 - 5
psiphon/meekConn.go

@@ -162,11 +162,12 @@ func DialMeek(
 
 		dialer = NewCustomTLSDialer(
 			&CustomTLSConfig{
-				Dial:           NewTCPDialer(meekConfig),
-				Timeout:        meekConfig.ConnectTimeout,
-				FrontingAddr:   fmt.Sprintf("%s:%d", frontingAddress, 443),
-				SendServerName: false,
-				SkipVerify:     true,
+				Dial:                    NewTCPDialer(meekConfig),
+				Timeout:                 meekConfig.ConnectTimeout,
+				FrontingAddr:            fmt.Sprintf("%s:%d", frontingAddress, 443),
+				SendServerName:          false,
+				SkipVerify:              true,
+				UseIndistinguishableTLS: config.UseIndistinguishableTLS,
 			})
 	} else {
 		// In the unfronted case, host is both what is dialed and what ends up in the HTTP Host header

+ 6 - 0
psiphon/net.go

@@ -64,6 +64,12 @@ type DialConfig struct {
 	// current active untunneled network DNS server.
 	DeviceBinder    DeviceBinder
 	DnsServerGetter DnsServerGetter
+
+	// UseIndistinguishableTLS specifies whether to try to use an
+	// alternative stack for TLS. From a circumvention perspective,
+	// Go's TLS has a distinct fingerprint that may be used for blocking.
+	// Only applies to TLS connections.
+	UseIndistinguishableTLS bool
 }
 
 // DeviceBinder defines the interface to the external BindToDevice provider

+ 89 - 0
psiphon/opensslConn.go

@@ -0,0 +1,89 @@
+// +build android
+
+/*
+ * Copyright (c) 2015, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package psiphon
+
+import (
+	"errors"
+	"net"
+	"strings"
+
+	"github.com/Psiphon-Inc/openssl"
+)
+
+// newOpenSSLConn wraps a connection with TLS which mimicks stock Android TLS.
+// This facility is used as a circumvention measure to ensure Psiphon client
+// TLS ClientHello messages match common TLS ClientHellos vs. the more
+// distinguishable (blockable) Go TLS ClientHello.
+func newOpenSSLConn(rawConn net.Conn, config *CustomTLSConfig) (handshakeConn, error) {
+
+	if !config.SkipVerify {
+		return nil, ContextError(errors.New("opensslDial certificate verification not supported"))
+	}
+	if config.SendServerName {
+		return nil, ContextError(errors.New("opensslDial server name not supported"))
+	}
+
+	ctx, err := openssl.NewCtx()
+	if err != nil {
+		return nil, ContextError(err)
+	}
+
+	// Use the same cipher suites, in the same priority order, as stock Android TLS.
+	// Based on: https://android.googlesource.com/platform/external/conscrypt/+/master/src/main/java/org/conscrypt/NativeCrypto.java
+	// This list includes include recently retired DSS suites: https://android.googlesource.com/platform/external/conscrypt/+/e53baea9221be7f9828d0f338ede284e22f55722%5E!/#F0,
+	// as those are still commonly deployed.
+	ciphersuites := []string{
+		"ECDHE-ECDSA-AES128-GCM-SHA256",
+		"ECDHE-ECDSA-AES256-GCM-SHA384",
+		"ECDHE-RSA-AES128-GCM-SHA256",
+		"ECDHE-RSA-AES256-GCM-SHA384",
+		"DHE-RSA-AES128-GCM-SHA256",
+		"DHE-RSA-AES256-GCM-SHA384",
+		"ECDHE-ECDSA-AES128-SHA",
+		"ECDHE-ECDSA-AES256-SHA",
+		"ECDHE-RSA-AES128-SHA",
+		"ECDHE-RSA-AES256-SHA",
+		"DHE-RSA-AES128-SHA",
+		"DHE-RSA-AES256-SHA",
+		"DHE-DSS-AES128-SHA",
+		"DHE-DSS-AES256-SHA",
+		"ECDHE-ECDSA-RC4-SHA",
+		"ECDHE-RSA-RC4-SHA",
+		"AES128-GCM-SHA256",
+		"AES256-GCM-SHA384",
+		"AES128-SHA",
+		"AES256-SHA",
+		"RC4-SHA",
+	}
+	ctx.SetCipherList(strings.Join(ciphersuites, ":"))
+
+	// Mimic extensions used by stock Android.
+	// NOTE: Heartbeat extension is disabled at compile time.
+	ctx.SetOptions(openssl.NoSessionResumptionOrRenegotiation | openssl.NoTicket)
+
+	conn, err := openssl.Client(rawConn, ctx)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+
+	return conn, nil
+}

+ 32 - 0
psiphon/opensslConn_unsupported.go

@@ -0,0 +1,32 @@
+// +build !android
+
+/*
+ * Copyright (c) 2015, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package psiphon
+
+import (
+	"errors"
+	"net"
+)
+
+// newOpenSSLConn simply returns an error when used on an unsupported platform.
+func newOpenSSLConn(rawConn net.Conn, config *CustomTLSConfig) (handshakeConn, error) {
+	return nil, ContextError(errors.New("newOpenSSLConn not supported on this platform"))
+}

+ 32 - 6
psiphon/tlsDialer.go

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, Psiphon Inc.
+ * Copyright (c) 2015, Psiphon Inc.
  * All rights reserved.
  *
  * This program is free software: you can redistribute it and/or modify
@@ -110,6 +110,11 @@ type CustomTLSConfig struct {
 	// TlsConfig is a tls.Config to use in the
 	// non-verifyLegacyCertificate case.
 	TlsConfig *tls.Config
+
+	// UseIndistinguishableTLS specifies whether to try to use an
+	// alternative stack for TLS. From a circumvention perspective,
+	// Go's TLS has a distinct fingerprint that may be used for blocking.
+	UseIndistinguishableTLS bool
 }
 
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
@@ -118,13 +123,19 @@ func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
 	}
 }
 
+// handshakeConn is a net.Conn that can perform a TLS handshake
+type handshakeConn interface {
+	net.Conn
+	Handshake() error
+}
+
 // CustomTLSDialWithDialer is a customized replacement for tls.Dial.
 // Based on tlsdialer.DialWithDialer which is based on crypto/tls.DialWithDialer.
 //
 // tlsdialer comment:
 //   Note - if sendServerName is false, the VerifiedChains field on the
 //   connection's ConnectionState will never get populated.
-func CustomTLSDial(network, addr string, config *CustomTLSConfig) (*tls.Conn, error) {
+func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, error) {
 
 	// We want the Timeout and Deadline values from dialer to cover the
 	// whole process: TCP connection and TLS handshake. This means that we
@@ -149,6 +160,7 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (*tls.Conn, er
 
 	hostname, _, err := net.SplitHostPort(dialAddr)
 	if err != nil {
+		rawConn.Close()
 		return nil, ContextError(err)
 	}
 
@@ -178,7 +190,19 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (*tls.Conn, er
 		tlsConfigCopy.InsecureSkipVerify = true
 	}
 
-	conn := tls.Client(rawConn, tlsConfigCopy)
+	var conn handshakeConn
+
+	// When supported, use OpenSSL TLS as a more indistinguishable TLS.
+	// TODO: add SNI and cert verification support for OpenSSL conns
+	if config.UseIndistinguishableTLS && !config.SendServerName && config.SkipVerify {
+		conn, err = newOpenSSLConn(rawConn, config)
+		if err != nil {
+			rawConn.Close()
+			return nil, ContextError(err)
+		}
+	} else {
+		conn = tls.Client(rawConn, tlsConfigCopy)
+	}
 
 	if config.Timeout == 0 {
 		err = conn.Handshake()
@@ -189,12 +213,14 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (*tls.Conn, er
 		err = <-errChannel
 	}
 
-	if !config.SkipVerify {
+	// TODO: replace type conversion with handshakeConn interface for verification
+	tlsConn, isTlsConn := conn.(*tls.Conn)
+	if !config.SkipVerify && isTlsConn {
 		if err == nil && config.VerifyLegacyCertificate != nil {
-			err = verifyLegacyCertificate(conn, config.VerifyLegacyCertificate)
+			err = verifyLegacyCertificate(tlsConn, config.VerifyLegacyCertificate)
 		} else if err == nil && !config.SendServerName && !tlsConfig.InsecureSkipVerify {
 			// Manually verify certificates
-			err = verifyServerCerts(conn, serverName, tlsConfigCopy)
+			err = verifyServerCerts(tlsConn, serverName, tlsConfigCopy)
 		}
 	}
 

+ 6 - 5
psiphon/tunnel.go

@@ -370,11 +370,12 @@ func dialSsh(
 
 	// Create the base transport: meek or direct connection
 	dialConfig := &DialConfig{
-		UpstreamProxyUrl: config.UpstreamProxyUrl,
-		ConnectTimeout:   TUNNEL_CONNECT_TIMEOUT,
-		PendingConns:     pendingConns,
-		DeviceBinder:     config.DeviceBinder,
-		DnsServerGetter:  config.DnsServerGetter,
+		UpstreamProxyUrl:        config.UpstreamProxyUrl,
+		ConnectTimeout:          TUNNEL_CONNECT_TIMEOUT,
+		PendingConns:            pendingConns,
+		DeviceBinder:            config.DeviceBinder,
+		DnsServerGetter:         config.DnsServerGetter,
+		UseIndistinguishableTLS: config.UseIndistinguishableTLS,
 	}
 	if useMeek {
 		conn, err = DialMeek(serverEntry, sessionId, frontingAddress, dialConfig)