Browse Source

Preliminary TLS 1.3 support

- Client uses github.com/cloudflare/tls-tris for
  TLS 1.3 profiles and continues to use utls
  for all other profiles.

- Server now uses tris for both TLS 1.3 and older
  support.
Rod Hynes 7 years ago
parent
commit
a6fab6ab49

+ 2 - 0
README.md

@@ -99,6 +99,8 @@ Psiphon Tunnel Core uses:
 * [codahale/sss](https://github.com/codahale/sss)
 * [codahale/sss](https://github.com/codahale/sss)
 * [marusama/semaphore](https://github.com/marusama/semaphore)
 * [marusama/semaphore](https://github.com/marusama/semaphore)
 * [utls](https://github.com/refraction-networking/utls)
 * [utls](https://github.com/refraction-networking/utls)
+* [quic-go](https://github.com/lucas-clemente/quic-go)
+* [tls-tris](https://github.com/cloudflare/tls-tris)
 
 
 Licensing
 Licensing
 --------------------------------------------------------------------------------
 --------------------------------------------------------------------------------

+ 9 - 7
psiphon/common/protocol/protocol.go

@@ -183,13 +183,14 @@ func UseClientTunnelProtocol(
 }
 }
 
 
 const (
 const (
-	TLS_PROFILE_IOS_1131   = "iOS-Safari-11.3.1"
-	TLS_PROFILE_ANDROID_60 = "Android-6.0"
-	TLS_PROFILE_ANDROID_51 = "Android-5.1"
-	TLS_PROFILE_CHROME_58  = "Chrome-58"
-	TLS_PROFILE_CHROME_57  = "Chrome-57"
-	TLS_PROFILE_FIREFOX_56 = "Firefox-56"
-	TLS_PROFILE_RANDOMIZED = "Randomized"
+	TLS_PROFILE_IOS_1131         = "iOS-Safari-11.3.1"
+	TLS_PROFILE_ANDROID_60       = "Android-6.0"
+	TLS_PROFILE_ANDROID_51       = "Android-5.1"
+	TLS_PROFILE_CHROME_58        = "Chrome-58"
+	TLS_PROFILE_CHROME_57        = "Chrome-57"
+	TLS_PROFILE_FIREFOX_56       = "Firefox-56"
+	TLS_PROFILE_RANDOMIZED       = "Randomized"
+	TLS_PROFILE_TLS13_RANDOMIZED = "TLS-1.3-Randomized"
 )
 )
 
 
 var SupportedTLSProfiles = TLSProfiles{
 var SupportedTLSProfiles = TLSProfiles{
@@ -200,6 +201,7 @@ var SupportedTLSProfiles = TLSProfiles{
 	TLS_PROFILE_CHROME_57,
 	TLS_PROFILE_CHROME_57,
 	TLS_PROFILE_FIREFOX_56,
 	TLS_PROFILE_FIREFOX_56,
 	TLS_PROFILE_RANDOMIZED,
 	TLS_PROFILE_RANDOMIZED,
+	TLS_PROFILE_TLS13_RANDOMIZED,
 }
 }
 
 
 type TLSProfiles []string
 type TLSProfiles []string

+ 10 - 0
psiphon/config.go

@@ -154,6 +154,12 @@ type Config struct {
 	// For the default, 0, InitialLimitTunnelProtocols is off.
 	// For the default, 0, InitialLimitTunnelProtocols is off.
 	InitialLimitTunnelProtocolsCandidateCount int
 	InitialLimitTunnelProtocolsCandidateCount int
 
 
+	// LimitTLSProfiles indicates which TLS profiles to select from. Valid
+	// values are listed in protocols.SupportedTLSProfiles.
+	// For the default, an empty list, all profiles are candidates for
+	// selection.
+	LimitTLSProfiles []string
+
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to
 	// halt the core tunnel controller if no tunnel has been established. The
 	// halt the core tunnel controller if no tunnel has been established. The
 	// default is parameters.EstablishTunnelTimeoutSeconds.
 	// default is parameters.EstablishTunnelTimeoutSeconds.
@@ -819,6 +825,10 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.InitialLimitTunnelProtocolsCandidateCount] = config.InitialLimitTunnelProtocolsCandidateCount
 		applyParameters[parameters.InitialLimitTunnelProtocolsCandidateCount] = config.InitialLimitTunnelProtocolsCandidateCount
 	}
 	}
 
 
+	if len(config.LimitTLSProfiles) > 0 {
+		applyParameters[parameters.LimitTLSProfiles] = protocol.TunnelProtocols(config.LimitTLSProfiles)
+	}
+
 	if config.EstablishTunnelTimeoutSeconds != nil {
 	if config.EstablishTunnelTimeoutSeconds != nil {
 		applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
 		applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
 	}
 	}

+ 2 - 12
psiphon/meekConn.go

@@ -45,7 +45,6 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
-	utls "github.com/Psiphon-Labs/utls"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2"
 )
 )
 
 
@@ -249,8 +248,8 @@ func DialMeek(
 			SkipVerify:                    true,
 			SkipVerify:                    true,
 			TLSProfile:                    meekConfig.TLSProfile,
 			TLSProfile:                    meekConfig.TLSProfile,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
-			ClientSessionCache:            utls.NewLRUClientSessionCache(0),
 		}
 		}
+		tlsConfig.EnableClientSessionCache(meekConfig.ClientParameters)
 
 
 		if meekConfig.UseObfuscatedSessionTickets {
 		if meekConfig.UseObfuscatedSessionTickets {
 			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
 			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
@@ -302,18 +301,9 @@ func DialMeek(
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
 
 
-		isHTTP2 := false
-		if tlsConn, ok := preConn.(*utls.UConn); ok {
-			state := tlsConn.ConnectionState()
-			if state.NegotiatedProtocolIsMutual &&
-				state.NegotiatedProtocol == "h2" {
-				isHTTP2 = true
-			}
-		}
-
 		cachedTLSDialer = newCachedTLSDialer(preConn, tlsDialer)
 		cachedTLSDialer = newCachedTLSDialer(preConn, tlsDialer)
 
 
-		if isHTTP2 {
+		if IsTLSConnUsingHTTP2(preConn) {
 			NoticeInfo("negotiated HTTP/2 for %s", meekConfig.DialAddress)
 			NoticeInfo("negotiated HTTP/2 for %s", meekConfig.DialAddress)
 			transport = &http2.Transport{
 			transport = &http2.Transport{
 				DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
 				DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {

+ 14 - 14
psiphon/net.go

@@ -35,7 +35,6 @@ import (
 
 
 	"github.com/Psiphon-Labs/dns"
 	"github.com/Psiphon-Labs/dns"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
-	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
 const DNS_PORT = 53
 const DNS_PORT = 53
@@ -268,19 +267,20 @@ func MakeUntunneledHTTPClient(
 
 
 	dialer := NewTCPDialer(untunneledDialConfig)
 	dialer := NewTCPDialer(untunneledDialConfig)
 
 
-	tlsDialer := NewCustomTLSDialer(
-		// Note: when verifyLegacyCertificate is not nil, some
-		// of the other CustomTLSConfig is overridden.
-		&CustomTLSConfig{
-			ClientParameters: config.clientParameters,
-			Dial:             dialer,
-			VerifyLegacyCertificate:       verifyLegacyCertificate,
-			UseDialAddrSNI:                true,
-			SNIServerName:                 "",
-			SkipVerify:                    skipVerify,
-			TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
-			ClientSessionCache:            utls.NewLRUClientSessionCache(0),
-		})
+	// Note: when verifyLegacyCertificate is not nil, some
+	// of the other CustomTLSConfig is overridden.
+	tlsConfig := &CustomTLSConfig{
+		ClientParameters: config.clientParameters,
+		Dial:             dialer,
+		VerifyLegacyCertificate:       verifyLegacyCertificate,
+		UseDialAddrSNI:                true,
+		SNIServerName:                 "",
+		SkipVerify:                    skipVerify,
+		TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
+	}
+	tlsConfig.EnableClientSessionCache(config.clientParameters)
+
+	tlsDialer := NewCustomTLSDialer(tlsConfig)
 
 
 	transport := &http.Transport{
 	transport := &http.Transport{
 		Dial: func(network, addr string) (net.Conn, error) {
 		Dial: func(network, addr string) (net.Conn, error) {

+ 26 - 26
psiphon/server/meek.go

@@ -43,7 +43,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 )
 
 
 // MeekServer is based on meek-server.go from Tor and Psiphon:
 // MeekServer is based on meek-server.go from Tor and Psiphon:
@@ -96,7 +96,7 @@ const (
 type MeekServer struct {
 type MeekServer struct {
 	support           *SupportServices
 	support           *SupportServices
 	listener          net.Listener
 	listener          net.Listener
-	tlsConfig         *utls.Config
+	tlsConfig         *tris.Config
 	clientHandler     func(clientTunnelProtocol string, clientConn net.Conn)
 	clientHandler     func(clientTunnelProtocol string, clientConn net.Conn)
 	openConns         *common.Conns
 	openConns         *common.Conns
 	stopBroadcast     <-chan struct{}
 	stopBroadcast     <-chan struct{}
@@ -923,23 +923,23 @@ func (session *meekSession) GetMetrics() LogFields {
 // assuming the peer is an uncensored CDN.
 // assuming the peer is an uncensored CDN.
 func makeMeekTLSConfig(
 func makeMeekTLSConfig(
 	support *SupportServices,
 	support *SupportServices,
-	useObfuscatedSessionTickets bool) (*utls.Config, error) {
+	useObfuscatedSessionTickets bool) (*tris.Config, error) {
 
 
 	certificate, privateKey, err := common.GenerateWebServerCertificate(common.GenerateHostName())
 	certificate, privateKey, err := common.GenerateWebServerCertificate(common.GenerateHostName())
 	if err != nil {
 	if err != nil {
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	tlsCertificate, err := utls.X509KeyPair(
+	tlsCertificate, err := tris.X509KeyPair(
 		[]byte(certificate), []byte(privateKey))
 		[]byte(certificate), []byte(privateKey))
 	if err != nil {
 	if err != nil {
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	config := &utls.Config{
-		Certificates: []utls.Certificate{tlsCertificate},
+	config := &tris.Config{
+		Certificates: []tris.Certificate{tlsCertificate},
 		NextProtos:   []string{"http/1.1"},
 		NextProtos:   []string{"http/1.1"},
-		MinVersion:   utls.VersionTLS10,
+		MinVersion:   tris.VersionTLS10,
 
 
 		// This is a reordering of the supported CipherSuites in golang 1.6. Non-ephemeral key
 		// This is a reordering of the supported CipherSuites in golang 1.6. Non-ephemeral key
 		// CipherSuites greatly reduce server load, and we try to select these since the meek
 		// CipherSuites greatly reduce server load, and we try to select these since the meek
@@ -948,23 +948,23 @@ func makeMeekTLSConfig(
 		// by ephemeral key CipherSuites.
 		// by ephemeral key CipherSuites.
 		// https://github.com/golang/go/blob/1cb3044c9fcd88e1557eca1bf35845a4108bc1db/src/crypto/tls/cipher_suites.go#L75
 		// https://github.com/golang/go/blob/1cb3044c9fcd88e1557eca1bf35845a4108bc1db/src/crypto/tls/cipher_suites.go#L75
 		CipherSuites: []uint16{
 		CipherSuites: []uint16{
-			utls.TLS_RSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_RSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_RSA_WITH_RC4_128_SHA,
-			utls.TLS_RSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_RSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+			tris.TLS_RSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_RSA_WITH_RC4_128_SHA,
+			tris.TLS_RSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_RSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
 		},
 		},
 		PreferServerCipherSuites: true,
 		PreferServerCipherSuites: true,
 	}
 	}
@@ -972,7 +972,7 @@ func makeMeekTLSConfig(
 	if useObfuscatedSessionTickets {
 	if useObfuscatedSessionTickets {
 
 
 		// See obfuscated session ticket overview
 		// See obfuscated session ticket overview
-		// in utls.NewObfuscatedClientSessionCache
+		// in NewObfuscatedClientSessionCache.
 
 
 		var obfuscatedSessionTicketKey [32]byte
 		var obfuscatedSessionTicketKey [32]byte
 		key, err := hex.DecodeString(support.Config.MeekObfuscatedKey)
 		key, err := hex.DecodeString(support.Config.MeekObfuscatedKey)
@@ -991,7 +991,7 @@ func makeMeekTLSConfig(
 		}
 		}
 
 
 		// Note: SessionTicketKey needs to be set, or else, it appears,
 		// Note: SessionTicketKey needs to be set, or else, it appears,
-		// utls.Config.serverInit() will clobber the value set by
+		// tris.Config.serverInit() will clobber the value set by
 		// SetSessionTicketKeys.
 		// SetSessionTicketKeys.
 		config.SessionTicketKey = obfuscatedSessionTicketKey
 		config.SessionTicketKey = obfuscatedSessionTicketKey
 		config.SetSessionTicketKeys([][32]byte{
 		config.SetSessionTicketKeys([][32]byte{

+ 4 - 4
psiphon/server/net.go

@@ -54,7 +54,7 @@ import (
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 
 
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 )
 
 
 // HTTPSServer is a wrapper around http.Server which adds the
 // HTTPSServer is a wrapper around http.Server which adds the
@@ -71,9 +71,9 @@ type HTTPSServer struct {
 // shutdown. ListenAndServeTLS also requires the TLS cert and key to be in files
 // shutdown. ListenAndServeTLS also requires the TLS cert and key to be in files
 // and we avoid that here.
 // and we avoid that here.
 //
 //
-// Note that the http.Server.TLSConfig field is ignored and the utls.Config
+// Note that the http.Server.TLSConfig field is ignored and the tris.Config
 // parameter is used intead.
 // parameter is used intead.
-func (server *HTTPSServer) ServeTLS(listener net.Listener, config *utls.Config) error {
-	tlsListener := utls.NewListener(listener, config)
+func (server *HTTPSServer) ServeTLS(listener net.Listener, config *tris.Config) error {
+	tlsListener := tris.NewListener(listener, config)
 	return server.Serve(tlsListener)
 	return server.Serve(tlsListener)
 }
 }

+ 42 - 1
psiphon/server/server_test.go

@@ -166,6 +166,23 @@ func TestUnfrontedMeekHTTPS(t *testing.T) {
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{
 			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
 			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_RANDOMIZED,
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
+func TestUnfrontedMeekHTTPSTLS13(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_TLS13_RANDOMIZED,
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
 			doDefaultSponsorID:   false,
 			doDefaultSponsorID:   false,
@@ -181,6 +198,23 @@ func TestUnfrontedMeekSessionTicket(t *testing.T) {
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{
 			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
 			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_RANDOMIZED,
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
+func TestUnfrontedMeekSessionTicketTLS13(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_TLS13_RANDOMIZED,
 			enableSSHAPIRequests: true,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doHotReload:          false,
 			doDefaultSponsorID:   false,
 			doDefaultSponsorID:   false,
@@ -362,6 +396,7 @@ func TestUDPOnlySLOK(t *testing.T) {
 
 
 type runServerConfig struct {
 type runServerConfig struct {
 	tunnelProtocol       string
 	tunnelProtocol       string
+	tlsProfile           string
 	enableSSHAPIRequests bool
 	enableSSHAPIRequests bool
 	doHotReload          bool
 	doHotReload          bool
 	doDefaultSponsorID   bool
 	doDefaultSponsorID   bool
@@ -580,6 +615,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		jsonNetworkID = fmt.Sprintf(`,"NetworkID" : "%s-%s"`, prefix, "NETWORK1")
 		jsonNetworkID = fmt.Sprintf(`,"NetworkID" : "%s-%s"`, prefix, "NETWORK1")
 	}
 	}
 
 
+	jsonLimitTLSProfiles := ""
+	if runConfig.tlsProfile != "" {
+		jsonLimitTLSProfiles = fmt.Sprintf(`,"LimitTLSProfiles" : ["%s"]`, runConfig.tlsProfile)
+	}
+
 	clientConfigJSON := fmt.Sprintf(`
 	clientConfigJSON := fmt.Sprintf(`
     {
     {
         "ClientPlatform" : "Windows",
         "ClientPlatform" : "Windows",
@@ -591,7 +631,8 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
         "ConnectionWorkerPoolSize" : %d,
         "ConnectionWorkerPoolSize" : %d,
         "LimitTunnelProtocols" : ["%s"]
         "LimitTunnelProtocols" : ["%s"]
         %s
         %s
-    }`, numTunnels, runConfig.tunnelProtocol, jsonNetworkID)
+        %s
+    }`, numTunnels, runConfig.tunnelProtocol, jsonLimitTLSProfiles, jsonNetworkID)
 
 
 	clientConfig, err := psiphon.LoadConfig([]byte(clientConfigJSON))
 	clientConfig, err := psiphon.LoadConfig([]byte(clientConfigJSON))
 	if err != nil {
 	if err != nil {

+ 4 - 4
psiphon/server/webServer.go

@@ -32,7 +32,7 @@ import (
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 )
 
 
 const WEB_SERVER_IO_TIMEOUT = 10 * time.Second
 const WEB_SERVER_IO_TIMEOUT = 10 * time.Second
@@ -70,15 +70,15 @@ func RunWebServer(
 	serveMux.HandleFunc("/connected", webServer.connectedHandler)
 	serveMux.HandleFunc("/connected", webServer.connectedHandler)
 	serveMux.HandleFunc("/status", webServer.statusHandler)
 	serveMux.HandleFunc("/status", webServer.statusHandler)
 
 
-	certificate, err := utls.X509KeyPair(
+	certificate, err := tris.X509KeyPair(
 		[]byte(support.Config.WebServerCertificate),
 		[]byte(support.Config.WebServerCertificate),
 		[]byte(support.Config.WebServerPrivateKey))
 		[]byte(support.Config.WebServerPrivateKey))
 	if err != nil {
 	if err != nil {
 		return common.ContextError(err)
 		return common.ContextError(err)
 	}
 	}
 
 
-	tlsConfig := &utls.Config{
-		Certificates: []utls.Certificate{certificate},
+	tlsConfig := &tris.Config{
+		Certificates: []tris.Certificate{certificate},
 	}
 	}
 
 
 	// TODO: inherits global log config?
 	// TODO: inherits global log config?

+ 150 - 56
psiphon/tlsDialer.go

@@ -47,27 +47,9 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 */
 
 
-// Fork of https://github.com/getlantern/tlsdialer (http://gopkg.in/getlantern/tlsdialer.v1)
+// Based on https://github.com/getlantern/tlsdialer (http://gopkg.in/getlantern/tlsdialer.v1)
 // which itself is a "Fork of crypto/tls.Dial and DialWithDialer"
 // which itself is a "Fork of crypto/tls.Dial and DialWithDialer"
 
 
-// Adds two capabilities to tlsdialer:
-//
-// 1. HTTP proxy support, so the dialer may be used with http.Transport.
-//
-// 2. Support for self-signed Psiphon server certificates, which Go's certificate
-//    verification rejects due to two short comings:
-//    - lack of IP address SANs.
-//      see: "...because it doesn't contain any IP SANs" case in crypto/x509/verify.go
-//    - non-compliant constraint configuration (RFC 5280, 4.2.1.9).
-//      see: CheckSignatureFrom() in crypto/x509/x509.go
-//    Since the client has to be able to handle existing Psiphon server certificates,
-//    we need to be able to perform some form of verification in these cases.
-
-// tlsdialer:
-// package tlsdialer contains a customized version of crypto/tls.Dial that
-// allows control over whether or not to send the ServerName extension in the
-// client handshake.
-
 package psiphon
 package psiphon
 
 
 import (
 import (
@@ -82,6 +64,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+	tris "github.com/Psiphon-Labs/tls-tris"
 	utls "github.com/Psiphon-Labs/utls"
 	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
@@ -138,14 +121,31 @@ type CustomTLSConfig struct {
 	// using the specified key.
 	// using the specified key.
 	ObfuscatedSessionTicketKey string
 	ObfuscatedSessionTicketKey string
 
 
-	// ClientSessionCache specifies a cache to use to persist session
-	// tickets, enabling TLS session resumability across multiple
-	// CustomTLSDial calls or dialers using the same CustomTLSConfig.
-	ClientSessionCache utls.ClientSessionCache
+	utlsClientSessionCache utls.ClientSessionCache
+	trisClientSessionCache tris.ClientSessionCache
+}
+
+// EnableClientSessionCache initializes a cache to use to persist session
+// tickets, enabling TLS session resumability across multiple
+// CustomTLSDial calls or dialers using the same CustomTLSConfig.
+//
+// TLSProfile must be set or will be auto-set via SelectTLSProfile.
+func (config *CustomTLSConfig) EnableClientSessionCache(
+	clientParameters *parameters.ClientParameters) {
+
+	if config.TLSProfile == "" {
+		config.TLSProfile = SelectTLSProfile(config.ClientParameters)
+	}
+
+	if useUTLS(config.TLSProfile) {
+		config.utlsClientSessionCache = utls.NewLRUClientSessionCache(0)
+	} else {
+		config.trisClientSessionCache = tris.NewLRUClientSessionCache(0)
+	}
 }
 }
 
 
+// SelectTLSProfile picks a random TLS profile from the available candidates.
 func SelectTLSProfile(
 func SelectTLSProfile(
-	tunnelProtocol string,
 	clientParameters *parameters.ClientParameters) string {
 	clientParameters *parameters.ClientParameters) string {
 
 
 	limitTLSProfiles := clientParameters.Get().TLSProfiles(parameters.LimitTLSProfiles)
 	limitTLSProfiles := clientParameters.Get().TLSProfiles(parameters.LimitTLSProfiles)
@@ -171,7 +171,11 @@ func SelectTLSProfile(
 	return tlsProfiles[choice]
 	return tlsProfiles[choice]
 }
 }
 
 
-func getClientHelloID(tlsProfile string) utls.ClientHelloID {
+func useUTLS(tlsProfile string) bool {
+	return tlsProfile != protocol.TLS_PROFILE_TLS13_RANDOMIZED
+}
+
+func getUTLSClientHelloID(tlsProfile string) utls.ClientHelloID {
 	switch tlsProfile {
 	switch tlsProfile {
 	case protocol.TLS_PROFILE_IOS_1131:
 	case protocol.TLS_PROFILE_IOS_1131:
 		return utls.HelloiOSSafari_11_3_1
 		return utls.HelloiOSSafari_11_3_1
@@ -192,6 +196,52 @@ func getClientHelloID(tlsProfile string) utls.ClientHelloID {
 	}
 	}
 }
 }
 
 
+// tlsConn provides a common interface for calling utls and tris methods. Both
+// utls and tris are derived from crypto/tls and have identical functions but
+// different types for return values etc.
+type tlsConn interface {
+	net.Conn
+	Handshake() error
+	GetPeerCertificates() []*x509.Certificate
+	IsHTTP2() bool
+}
+
+type utlsConn struct {
+	*utls.UConn
+}
+
+func (conn *utlsConn) GetPeerCertificates() []*x509.Certificate {
+	return conn.UConn.ConnectionState().PeerCertificates
+}
+
+func (conn *utlsConn) IsHTTP2() bool {
+	state := conn.UConn.ConnectionState()
+	return state.NegotiatedProtocolIsMutual &&
+		state.NegotiatedProtocol == "h2"
+}
+
+type trisConn struct {
+	*tris.Conn
+}
+
+func (conn *trisConn) GetPeerCertificates() []*x509.Certificate {
+	return conn.Conn.ConnectionState().PeerCertificates
+}
+
+func (conn *trisConn) IsHTTP2() bool {
+	state := conn.Conn.ConnectionState()
+	return state.NegotiatedProtocolIsMutual &&
+		state.NegotiatedProtocol == "h2"
+}
+
+func IsTLSConnUsingHTTP2(conn net.Conn) bool {
+	if c, ok := conn.(tlsConn); ok {
+		return c.IsHTTP2()
+	}
+	return false
+}
+
+// NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 		return CustomTLSDial(ctx, network, addr, config)
 		return CustomTLSDial(ctx, network, addr, config)
@@ -239,43 +289,37 @@ func CustomTLSDial(
 	selectedTLSProfile := config.TLSProfile
 	selectedTLSProfile := config.TLSProfile
 
 
 	if selectedTLSProfile == "" {
 	if selectedTLSProfile == "" {
-		selectedTLSProfile = SelectTLSProfile("", config.ClientParameters)
+		selectedTLSProfile = SelectTLSProfile(config.ClientParameters)
 	}
 	}
 
 
-	clientSessionCache := config.ClientSessionCache
-	if clientSessionCache == nil {
-		clientSessionCache = utls.NewLRUClientSessionCache(0)
-	}
-
-	tlsConfig := &utls.Config{
-		ClientSessionCache: clientSessionCache,
-	}
+	tlsConfigInsecureSkipVerify := false
+	tlsConfigServerName := ""
 
 
 	if config.SkipVerify {
 	if config.SkipVerify {
-		tlsConfig.InsecureSkipVerify = true
+		tlsConfigInsecureSkipVerify = true
 	}
 	}
 
 
 	if config.UseDialAddrSNI {
 	if config.UseDialAddrSNI {
-		tlsConfig.ServerName = hostname
+		tlsConfigServerName = hostname
 	} else if config.SNIServerName != "" && config.VerifyLegacyCertificate == nil {
 	} else if config.SNIServerName != "" && config.VerifyLegacyCertificate == nil {
 		// Set the ServerName and rely on the usual logic in
 		// Set the ServerName and rely on the usual logic in
 		// tls.Conn.Handshake() to do its verification.
 		// tls.Conn.Handshake() to do its verification.
 		// Note: Go TLS will automatically omit this ServerName when it's an IP address
 		// Note: Go TLS will automatically omit this ServerName when it's an IP address
-		tlsConfig.ServerName = config.SNIServerName
+		tlsConfigServerName = config.SNIServerName
 	} else {
 	} else {
 		// No SNI.
 		// No SNI.
 		// Disable verification in tls.Conn.Handshake().  We'll verify manually
 		// Disable verification in tls.Conn.Handshake().  We'll verify manually
 		// after handshaking
 		// after handshaking
-		tlsConfig.InsecureSkipVerify = true
+		tlsConfigInsecureSkipVerify = true
 	}
 	}
 
 
-	tlsConn := utls.UClient(rawConn, tlsConfig, getClientHelloID(selectedTLSProfile))
+	var obfuscatedSessionTicketKey [32]byte
 
 
 	if config.ObfuscatedSessionTicketKey != "" {
 	if config.ObfuscatedSessionTicketKey != "" {
 
 
-		// See obfuscated session ticket overview in NewObfuscatedClientSessionCache
+		// See obfuscated session ticket overview in
+		// NewObfuscatedClientSessionCache.
 
 
-		var obfuscatedSessionTicketKey [32]byte
 		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
 		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
 		if err == nil && len(key) != 32 {
 		if err == nil && len(key) != 32 {
 			err = errors.New("invalid obfuscated session key length")
 			err = errors.New("invalid obfuscated session key length")
@@ -284,20 +328,70 @@ func CustomTLSDial(
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
 		copy(obfuscatedSessionTicketKey[:], key)
 		copy(obfuscatedSessionTicketKey[:], key)
+	}
 
 
-		sessionState, err := utls.NewObfuscatedClientSessionState(
-			obfuscatedSessionTicketKey)
-		if err != nil {
-			return nil, common.ContextError(err)
+	// Depending on the selected TLS profile, the TLS provider will be tris
+	// (TLS 1.3) or utls (all other profiles).
+
+	var conn tlsConn
+
+	if useUTLS(selectedTLSProfile) {
+
+		clientSessionCache := config.utlsClientSessionCache
+		if clientSessionCache == nil {
+			clientSessionCache = utls.NewLRUClientSessionCache(0)
+		}
+
+		tlsConfig := &utls.Config{
+			InsecureSkipVerify: tlsConfigInsecureSkipVerify,
+			ServerName:         tlsConfigServerName,
+			ClientSessionCache: clientSessionCache,
+		}
+
+		uconn := utls.UClient(rawConn, tlsConfig, getUTLSClientHelloID(selectedTLSProfile))
+
+		if config.ObfuscatedSessionTicketKey != "" {
+			sessionState, err := utls.NewObfuscatedClientSessionState(
+				obfuscatedSessionTicketKey)
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
+			uconn.SetSessionState(sessionState)
+		}
+
+		conn = &utlsConn{
+			UConn: uconn,
+		}
+
+	} else {
+
+		var clientSessionCache tris.ClientSessionCache
+		if config.ObfuscatedSessionTicketKey != "" {
+			clientSessionCache = tris.NewObfuscatedClientSessionCache(
+				obfuscatedSessionTicketKey)
+		} else {
+			clientSessionCache = config.trisClientSessionCache
+			if clientSessionCache == nil {
+				clientSessionCache = tris.NewLRUClientSessionCache(0)
+			}
+		}
+
+		tlsConfig := &tris.Config{
+			InsecureSkipVerify: tlsConfigInsecureSkipVerify,
+			ServerName:         tlsConfigServerName,
+			ClientSessionCache: clientSessionCache,
+		}
+
+		conn = &trisConn{
+			Conn: tris.Client(rawConn, tlsConfig),
 		}
 		}
 
 
-		tlsConn.SetSessionState(sessionState)
 	}
 	}
 
 
 	resultChannel := make(chan error)
 	resultChannel := make(chan error)
 
 
 	go func() {
 	go func() {
-		resultChannel <- tlsConn.Handshake()
+		resultChannel <- conn.Handshake()
 	}()
 	}()
 
 
 	select {
 	select {
@@ -309,13 +403,13 @@ func CustomTLSDial(
 		<-resultChannel
 		<-resultChannel
 	}
 	}
 
 
-	if err == nil && !config.SkipVerify && tlsConfig.InsecureSkipVerify {
+	if err == nil && !config.SkipVerify && tlsConfigInsecureSkipVerify {
 
 
 		if config.VerifyLegacyCertificate != nil {
 		if config.VerifyLegacyCertificate != nil {
-			err = verifyLegacyCertificate(tlsConn, config.VerifyLegacyCertificate)
+			err = verifyLegacyCertificate(conn, config.VerifyLegacyCertificate)
 		} else {
 		} else {
 			// Manually verify certificates
 			// Manually verify certificates
-			err = verifyServerCerts(tlsConn, hostname, tlsConfig)
+			err = verifyServerCerts(conn, hostname)
 		}
 		}
 	}
 	}
 
 
@@ -324,11 +418,11 @@ func CustomTLSDial(
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	return tlsConn, nil
+	return conn, nil
 }
 }
 
 
-func verifyLegacyCertificate(tlsConn *utls.UConn, expectedCertificate *x509.Certificate) error {
-	certs := tlsConn.ConnectionState().PeerCertificates
+func verifyLegacyCertificate(conn tlsConn, expectedCertificate *x509.Certificate) error {
+	certs := conn.GetPeerCertificates()
 	if len(certs) < 1 {
 	if len(certs) < 1 {
 		return common.ContextError(errors.New("no certificate to verify"))
 		return common.ContextError(errors.New("no certificate to verify"))
 	}
 	}
@@ -338,11 +432,11 @@ func verifyLegacyCertificate(tlsConn *utls.UConn, expectedCertificate *x509.Cert
 	return nil
 	return nil
 }
 }
 
 
-func verifyServerCerts(tlsConn *utls.UConn, hostname string, tlsConfig *utls.Config) error {
-	certs := tlsConn.ConnectionState().PeerCertificates
+func verifyServerCerts(conn tlsConn, hostname string) error {
+	certs := conn.GetPeerCertificates()
 
 
 	opts := x509.VerifyOptions{
 	opts := x509.VerifyOptions{
-		Roots:         tlsConfig.RootCAs,
+		Roots:         nil, // Use host's root CAs
 		CurrentTime:   time.Now(),
 		CurrentTime:   time.Now(),
 		DNSName:       hostname,
 		DNSName:       hostname,
 		Intermediates: x509.NewCertPool(),
 		Intermediates: x509.NewCertPool(),

+ 1 - 3
psiphon/tunnel.go

@@ -671,9 +671,7 @@ func initMeekConfig(
 	// Pin the TLS profile for the entire meek connection.
 	// Pin the TLS profile for the entire meek connection.
 	selectedTLSProfile := ""
 	selectedTLSProfile := ""
 	if protocol.TunnelProtocolUsesMeekHTTPS(selectedProtocol) {
 	if protocol.TunnelProtocolUsesMeekHTTPS(selectedProtocol) {
-		selectedTLSProfile = SelectTLSProfile(
-			selectedProtocol,
-			config.clientParameters)
+		selectedTLSProfile = SelectTLSProfile(config.clientParameters)
 	}
 	}
 
 
 	return &MeekConfig{
 	return &MeekConfig{