Explorar el Código

SNI and cert verification support for opensslConn

* Add SystemCACertificateDirectory config param
  to use with OpenSSL for cert verification.

* Can now use indistinguishable TLS option with
  FetchRemoteServerList (not yet integrated).

* Refactored CustomTLSDial to better reflect
  all possible config combinations.

* Limitation: VerifyLegacyCertificate not yet
  supported by newOpenSSLConn.
Rod Hynes hace 10 años
padre
commit
20100c61e7

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

@@ -321,6 +321,10 @@ public class PsiphonTunnel extends Psi.PsiphonProvider.Stub {
 
         json.put("UseIndistinguishableTLS", true);
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            json.put("SystemCACertificateDirectory", "/system/etc/security/cacerts");
+        }
+
         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 - 0
psiphon/config.go

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

+ 7 - 6
psiphon/meekConn.go

@@ -162,12 +162,13 @@ func DialMeek(
 
 		dialer = NewCustomTLSDialer(
 			&CustomTLSConfig{
-				Dial:                    NewTCPDialer(meekConfig),
-				Timeout:                 meekConfig.ConnectTimeout,
-				FrontingAddr:            fmt.Sprintf("%s:%d", frontingAddress, 443),
-				SendServerName:          false,
-				SkipVerify:              true,
-				UseIndistinguishableTLS: config.UseIndistinguishableTLS,
+				Dial:                         NewTCPDialer(meekConfig),
+				Timeout:                      meekConfig.ConnectTimeout,
+				FrontingAddr:                 fmt.Sprintf("%s:%d", frontingAddress, 443),
+				SendServerName:               false,
+				SkipVerify:                   true,
+				UseIndistinguishableTLS:      config.UseIndistinguishableTLS,
+				SystemCACertificateDirectory: config.SystemCACertificateDirectory,
 			})
 	} 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

@@ -70,6 +70,12 @@ type DialConfig struct {
 	// Go's TLS has a distinct fingerprint that may be used for blocking.
 	// Only applies to TLS connections.
 	UseIndistinguishableTLS bool
+
+	// SystemCACertificateDirectory specifies a directory containing
+	// CA certs. Directory contents should be compatible with OpenSSL's
+	// SSL_CTX_load_verify_locations
+	// Only applies to UseIndistinguishableTLS connections.
+	SystemCACertificateDirectory string
 }
 
 // DeviceBinder defines the interface to the external BindToDevice provider

+ 26 - 8
psiphon/opensslConn.go

@@ -33,20 +33,31 @@ import (
 // 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"))
-	}
+func newOpenSSLConn(rawConn net.Conn, hostname string, config *CustomTLSConfig) (handshakeConn, error) {
 
 	ctx, err := openssl.NewCtx()
 	if err != nil {
 		return nil, ContextError(err)
 	}
 
+	if !config.SkipVerify {
+		ctx.SetVerifyMode(openssl.VerifyPeer)
+		if config.VerifyLegacyCertificate != nil {
+			// TODO: verify with VerifyLegacyCertificate
+			return nil, errors.New("newOpenSSLConn does not support VerifyLegacyCertificate")
+		} else {
+			if config.SystemCACertificateDirectory == "" {
+				return nil, errors.New("newOpenSSLConn cannot verify without SystemCACertificateDirectory")
+			}
+			err = ctx.LoadVerifyLocations("", config.SystemCACertificateDirectory)
+			if err != nil {
+				return nil, ContextError(err)
+			}
+		}
+	} else {
+		ctx.SetVerifyMode(openssl.VerifyNone)
+	}
+
 	// 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,
@@ -85,5 +96,12 @@ func newOpenSSLConn(rawConn net.Conn, config *CustomTLSConfig) (handshakeConn, e
 		return nil, ContextError(err)
 	}
 
+	if config.SendServerName {
+		err = conn.SetTlsExtHostName(hostname)
+		if err != nil {
+			return nil, ContextError(err)
+		}
+	}
+
 	return conn, nil
 }

+ 1 - 1
psiphon/opensslConn_unsupported.go

@@ -27,6 +27,6 @@ import (
 )
 
 // newOpenSSLConn simply returns an error when used on an unsupported platform.
-func newOpenSSLConn(rawConn net.Conn, config *CustomTLSConfig) (handshakeConn, error) {
+func newOpenSSLConn(rawConn net.Conn, hostname string, config *CustomTLSConfig) (handshakeConn, error) {
 	return nil, ContextError(errors.New("newOpenSSLConn not supported on this platform"))
 }

+ 0 - 1
psiphon/serverApi.go

@@ -367,7 +367,6 @@ func makePsiphonHttpsClient(tunnel *Tunnel) (httpsClient *http.Client, err error
 		&CustomTLSConfig{
 			Dial:                    tunneledDialer,
 			Timeout:                 PSIPHON_API_SERVER_TIMEOUT,
-			SendServerName:          false,
 			VerifyLegacyCertificate: certificate,
 		})
 	transport := &http.Transport{

+ 34 - 32
psiphon/tlsDialer.go

@@ -104,17 +104,19 @@ type CustomTLSConfig struct {
 	// VerifyLegacyCertificate is a special case self-signed server
 	// certificate case. Ignores IP SANs and basic constraints. No
 	// certificate chain. Just checks that the server presented the
-	// specified certificate.
+	// specified certificate. SNI is disbled when this is set.
 	VerifyLegacyCertificate *x509.Certificate
 
-	// 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
+
+	// SystemCACertificateDirectory specifies a directory containing
+	// CA certs. Directory contents should be compatible with OpenSSL's
+	// SSL_CTX_load_verify_locations
+	// Only applies to UseIndistinguishableTLS connections.
+	SystemCACertificateDirectory string
 }
 
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
@@ -164,44 +166,38 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 		return nil, ContextError(err)
 	}
 
-	tlsConfig := config.TlsConfig
-	if tlsConfig == nil {
-		tlsConfig = &tls.Config{}
-	}
-
-	// Copy config so we can tweak it
-	tlsConfigCopy := new(tls.Config)
-	*tlsConfigCopy = *tlsConfig
+	tlsConfig := &tls.Config{}
 
-	serverName := tlsConfig.ServerName
-	// If no ServerName is set, infer the ServerName
-	// from the hostname we're connecting to.
-	if serverName == "" {
-		serverName = hostname
+	if config.SkipVerify {
+		tlsConfig.InsecureSkipVerify = true
 	}
 
-	if config.SendServerName {
+	if config.SendServerName && config.VerifyLegacyCertificate == nil {
 		// Set the ServerName and rely on the usual logic in
 		// tls.Conn.Handshake() to do its verification
-		tlsConfigCopy.ServerName = serverName
+		tlsConfig.ServerName = hostname
 	} else {
+		// No SNI.
 		// Disable verification in tls.Conn.Handshake().  We'll verify manually
 		// after handshaking
-		tlsConfigCopy.InsecureSkipVerify = true
+		tlsConfig.InsecureSkipVerify = true
 	}
 
 	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 config.UseIndistinguishableTLS &&
+		(config.SkipVerify ||
+			// TODO: config.VerifyLegacyCertificate != nil ||
+			config.SystemCACertificateDirectory != "") {
+
+		conn, err = newOpenSSLConn(rawConn, hostname, config)
 		if err != nil {
 			rawConn.Close()
 			return nil, ContextError(err)
 		}
 	} else {
-		conn = tls.Client(rawConn, tlsConfigCopy)
+		conn = tls.Client(rawConn, tlsConfig)
 	}
 
 	if config.Timeout == 0 {
@@ -213,14 +209,20 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 		err = <-errChannel
 	}
 
-	// TODO: replace type conversion with handshakeConn interface for verification
+	// openSSLConns complete verification automatically. For Go TLS,
+	// we need to complete the process from crypto/tls.Dial.
+
+	// NOTE: for (config.SendServerName && !config.tlsConfig.InsecureSkipVerify),
+	// the tls.Conn.Handshake() does the complete verification, including host name.
 	tlsConn, isTlsConn := conn.(*tls.Conn)
-	if !config.SkipVerify && isTlsConn {
-		if err == nil && config.VerifyLegacyCertificate != nil {
+	if err == nil && isTlsConn &&
+		!config.SkipVerify && tlsConfig.InsecureSkipVerify {
+
+		if config.VerifyLegacyCertificate != nil {
 			err = verifyLegacyCertificate(tlsConn, config.VerifyLegacyCertificate)
-		} else if err == nil && !config.SendServerName && !tlsConfig.InsecureSkipVerify {
+		} else {
 			// Manually verify certificates
-			err = verifyServerCerts(tlsConn, serverName, tlsConfigCopy)
+			err = verifyServerCerts(tlsConn, hostname, tlsConfig)
 		}
 	}
 
@@ -243,13 +245,13 @@ func verifyLegacyCertificate(conn *tls.Conn, expectedCertificate *x509.Certifica
 	return nil
 }
 
-func verifyServerCerts(conn *tls.Conn, serverName string, config *tls.Config) error {
+func verifyServerCerts(conn *tls.Conn, hostname string, config *tls.Config) error {
 	certs := conn.ConnectionState().PeerCertificates
 
 	opts := x509.VerifyOptions{
 		Roots:         config.RootCAs,
 		CurrentTime:   time.Now(),
-		DNSName:       serverName,
+		DNSName:       hostname,
 		Intermediates: x509.NewCertPool(),
 	}
 

+ 7 - 6
psiphon/tunnel.go

@@ -370,12 +370,13 @@ 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,
-		UseIndistinguishableTLS: config.UseIndistinguishableTLS,
+		UpstreamProxyUrl:             config.UpstreamProxyUrl,
+		ConnectTimeout:               TUNNEL_CONNECT_TIMEOUT,
+		PendingConns:                 pendingConns,
+		DeviceBinder:                 config.DeviceBinder,
+		DnsServerGetter:              config.DnsServerGetter,
+		UseIndistinguishableTLS:      config.UseIndistinguishableTLS,
+		SystemCACertificateDirectory: config.SystemCACertificateDirectory,
 	}
 	if useMeek {
 		conn, err = DialMeek(serverEntry, sessionId, frontingAddress, dialConfig)