Sfoglia il codice sorgente

Add VerifyServerName and VerifyPins to CustomTLSDial

- Custom server certificate verification is now done
  in the VerifyPeerCertificate callback.

- Add ceritificate verification test cases.
Rod Hynes 5 anni fa
parent
commit
0319e36361
2 ha cambiato i file con 674 aggiunte e 268 eliminazioni
  1. 375 268
      psiphon/tlsDialer.go
  2. 299 0
      psiphon/tlsDialer_test.go

+ 375 - 268
psiphon/tlsDialer.go

@@ -47,20 +47,20 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
-// Based on https://github.com/getlantern/tlsdialer (http://gopkg.in/getlantern/tlsdialer.v1)
-// which itself is a "Fork of crypto/tls.Dial and DialWithDialer"
+// Originally based on https://gopkg.in/getlantern/tlsdialer.v1.
 
 package psiphon
 
 import (
 	"bytes"
 	"context"
+	"crypto/sha256"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/hex"
 	std_errors "errors"
 	"io/ioutil"
 	"net"
-	"time"
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
@@ -71,16 +71,16 @@ import (
 	utls "github.com/refraction-networking/utls"
 )
 
-// CustomTLSConfig contains parameters to determine the behavior
-// of CustomTLSDial.
+// CustomTLSConfig specifies the parameters for a CustomTLSDial, supporting
+// many TLS-related network obfuscation mechanisms.
 type CustomTLSConfig struct {
 
 	// Parameters is the active set of parameters.Parameters to use for the TLS
-	// dial.
+	// dial. Must not be nil.
 	Parameters *parameters.Parameters
 
-	// Dial is the network connection dialer. TLS is layered on
-	// top of a new network connection created with dialer.
+	// Dial is the network connection dialer. TLS is layered on top of a new
+	// network connection created with dialer. Must not be nil.
 	Dial common.Dialer
 
 	// DialAddr overrides the "addr" input to Dial when specified
@@ -98,15 +98,35 @@ type CustomTLSConfig struct {
 	// SNIServerName is ignored when UseDialAddrSNI is true.
 	SNIServerName string
 
-	// SkipVerify completely disables server certificate verification.
-	SkipVerify bool
+	// VerifyServerName specifies a domain name that must appear in the server
+	// certificate. When specified, certificate verification checks for
+	// VerifyServerName in the server certificate, in place of the dial or SNI
+	// hostname.
+	VerifyServerName string
+
+	// VerifyPins specifies one or more certificate pin values, one of which must
+	// appear in the verified server certificate chain. A pin value is the
+	// base64-encoded SHA2 digest of a certificate's public key. When specified,
+	// at least one pin must match at least one certificate in the chain, at any
+	// position; e.g., the root CA may be pinned, or the server certificate,
+	// etc.
+	VerifyPins []string
 
 	// 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. SNI is disbled when this is set.
+	// specified certificate.
+	//
+	// When VerifyLegacyCertificate is set, none of VerifyServerName, VerifyPins,
+	// SkipVerify may be set.
 	VerifyLegacyCertificate *x509.Certificate
 
+	// SkipVerify completely disables server certificate verification.
+	//
+	// When SkipVerify is set, none of VerifyServerName, VerifyPins,
+	// VerifyLegacyCertificate may be set.
+	SkipVerify bool
+
 	// TLSProfile specifies a particular indistinguishable TLS profile to use for
 	// the TLS dial. Setting TLSProfile allows the caller to pin the selection so
 	// all TLS connections in a certain context (e.g. a single meek connection)
@@ -159,209 +179,6 @@ func (config *CustomTLSConfig) EnableClientSessionCache() {
 	}
 }
 
-// SelectTLSProfile picks a TLS profile at random from the available candidates.
-func SelectTLSProfile(
-	requireTLS12SessionTickets bool,
-	isFronted bool,
-	frontingProviderID string,
-	p parameters.ParametersAccessor) string {
-
-	// Two TLS profile lists are constructed, subject to limit constraints:
-	// stock, fixed parrots (non-randomized SupportedTLSProfiles) and custom
-	// parrots (CustomTLSProfileNames); and randomized. If one list is empty, the
-	// non-empty list is used. Otherwise SelectRandomizedTLSProfileProbability
-	// determines which list is used.
-	//
-	// Note that LimitTLSProfiles is not applied to CustomTLSProfiles; the
-	// presence of a candidate in CustomTLSProfiles is treated as explicit
-	// enabling.
-	//
-	// UseOnlyCustomTLSProfiles may be used to disable all stock TLS profiles and
-	// use only CustomTLSProfiles; UseOnlyCustomTLSProfiles is ignored if
-	// CustomTLSProfiles is empty.
-	//
-	// For fronted servers, DisableFrontingProviderTLSProfiles may be used
-	// to disable TLS profiles which are incompatible with the TLS stack used
-	// by the front. For example, if a utls parrot doesn't fully support all
-	// of the capabilities in the ClientHello. Unlike the LimitTLSProfiles case,
-	// DisableFrontingProviderTLSProfiles may disable CustomTLSProfiles.
-
-	limitTLSProfiles := p.TLSProfiles(parameters.LimitTLSProfiles)
-	var disableTLSProfiles protocol.TLSProfiles
-
-	if isFronted && frontingProviderID != "" {
-		disableTLSProfiles = p.LabeledTLSProfiles(
-			parameters.DisableFrontingProviderTLSProfiles, frontingProviderID)
-	}
-
-	randomizedTLSProfiles := make([]string, 0)
-	parrotTLSProfiles := make([]string, 0)
-
-	for _, tlsProfile := range p.CustomTLSProfileNames() {
-		if !common.Contains(disableTLSProfiles, tlsProfile) {
-			parrotTLSProfiles = append(parrotTLSProfiles, tlsProfile)
-		}
-	}
-
-	useOnlyCustomTLSProfiles := p.Bool(parameters.UseOnlyCustomTLSProfiles)
-	if useOnlyCustomTLSProfiles && len(parrotTLSProfiles) == 0 {
-		useOnlyCustomTLSProfiles = false
-	}
-
-	if !useOnlyCustomTLSProfiles {
-		for _, tlsProfile := range protocol.SupportedTLSProfiles {
-
-			if len(limitTLSProfiles) > 0 &&
-				!common.Contains(limitTLSProfiles, tlsProfile) {
-				continue
-			}
-
-			if common.Contains(disableTLSProfiles, tlsProfile) {
-				continue
-			}
-
-			// requireTLS12SessionTickets is specified for
-			// UNFRONTED-MEEK-SESSION-TICKET-OSSH, a protocol which depends on using
-			// obfuscated session tickets to ensure that the server doesn't send its
-			// certificate in the TLS handshake. TLS 1.2 profiles which omit session
-			// tickets should not be selected. As TLS 1.3 encrypts the server
-			// certificate message, there's no exclusion for TLS 1.3.
-
-			if requireTLS12SessionTickets &&
-				protocol.TLS12ProfileOmitsSessionTickets(tlsProfile) {
-				continue
-			}
-
-			if protocol.TLSProfileIsRandomized(tlsProfile) {
-				randomizedTLSProfiles = append(randomizedTLSProfiles, tlsProfile)
-			} else {
-				parrotTLSProfiles = append(parrotTLSProfiles, tlsProfile)
-			}
-		}
-	}
-
-	if len(randomizedTLSProfiles) > 0 &&
-		(len(parrotTLSProfiles) == 0 ||
-			p.WeightedCoinFlip(parameters.SelectRandomizedTLSProfileProbability)) {
-
-		return randomizedTLSProfiles[prng.Intn(len(randomizedTLSProfiles))]
-	}
-
-	if len(parrotTLSProfiles) == 0 {
-		return ""
-	}
-
-	return parrotTLSProfiles[prng.Intn(len(parrotTLSProfiles))]
-}
-
-func getUTLSClientHelloID(
-	p parameters.ParametersAccessor,
-	tlsProfile string) (utls.ClientHelloID, *utls.ClientHelloSpec, error) {
-
-	switch tlsProfile {
-	case protocol.TLS_PROFILE_IOS_111:
-		return utls.HelloIOS_11_1, nil, nil
-	case protocol.TLS_PROFILE_IOS_121:
-		return utls.HelloIOS_12_1, nil, nil
-	case protocol.TLS_PROFILE_CHROME_58:
-		return utls.HelloChrome_58, nil, nil
-	case protocol.TLS_PROFILE_CHROME_62:
-		return utls.HelloChrome_62, nil, nil
-	case protocol.TLS_PROFILE_CHROME_70:
-		return utls.HelloChrome_70, nil, nil
-	case protocol.TLS_PROFILE_CHROME_72:
-		return utls.HelloChrome_72, nil, nil
-	case protocol.TLS_PROFILE_CHROME_83:
-		return utls.HelloChrome_83, nil, nil
-	case protocol.TLS_PROFILE_FIREFOX_55:
-		return utls.HelloFirefox_55, nil, nil
-	case protocol.TLS_PROFILE_FIREFOX_56:
-		return utls.HelloFirefox_56, nil, nil
-	case protocol.TLS_PROFILE_FIREFOX_65:
-		return utls.HelloFirefox_65, nil, nil
-	case protocol.TLS_PROFILE_RANDOMIZED:
-		return utls.HelloRandomized, nil, nil
-	}
-
-	// utls.HelloCustom with a utls.ClientHelloSpec is used for
-	// CustomTLSProfiles.
-
-	customTLSProfile := p.CustomTLSProfile(tlsProfile)
-	if customTLSProfile == nil {
-		return utls.HelloCustom,
-			nil,
-			errors.Tracef("unknown TLS profile: %s", tlsProfile)
-	}
-
-	utlsClientHelloSpec, err := customTLSProfile.GetClientHelloSpec()
-	if err != nil {
-		return utls.ClientHelloID{}, nil, errors.Trace(err)
-	}
-
-	return utls.HelloCustom, utlsClientHelloSpec, nil
-}
-
-func getClientHelloVersion(
-	utlsClientHelloID utls.ClientHelloID,
-	utlsClientHelloSpec *utls.ClientHelloSpec) (string, error) {
-
-	switch utlsClientHelloID {
-
-	case utls.HelloIOS_11_1, utls.HelloIOS_12_1, utls.HelloChrome_58,
-		utls.HelloChrome_62, utls.HelloFirefox_55, utls.HelloFirefox_56:
-		return protocol.TLS_VERSION_12, nil
-
-	case utls.HelloChrome_70, utls.HelloChrome_72, utls.HelloChrome_83,
-		utls.HelloFirefox_65, utls.HelloGolang:
-		return protocol.TLS_VERSION_13, nil
-	}
-
-	// As utls.HelloRandomized/Custom may be either TLS 1.2 or TLS 1.3, we cannot
-	// perform a simple ClientHello ID check. BuildHandshakeState is run, which
-	// constructs the entire ClientHello.
-	//
-	// Assumes utlsClientHelloID.Seed has been set; otherwise the result is
-	// ephemeral.
-	//
-	// BenchmarkRandomizedGetClientHelloVersion indicates that this operation
-	// takes on the order of 0.05ms and allocates ~8KB for randomized client
-	// hellos.
-
-	conn := utls.UClient(
-		nil,
-		&utls.Config{InsecureSkipVerify: true},
-		utlsClientHelloID)
-
-	if utlsClientHelloSpec != nil {
-		err := conn.ApplyPreset(utlsClientHelloSpec)
-		if err != nil {
-			return "", errors.Trace(err)
-		}
-	}
-
-	err := conn.BuildHandshakeState()
-	if err != nil {
-		return "", errors.Trace(err)
-	}
-
-	for _, v := range conn.HandshakeState.Hello.SupportedVersions {
-		if v == utls.VersionTLS13 {
-			return protocol.TLS_VERSION_13, nil
-		}
-	}
-
-	return protocol.TLS_VERSION_12, nil
-}
-
-func IsTLSConnUsingHTTP2(conn net.Conn) bool {
-	if c, ok := conn.(*utls.UConn); ok {
-		state := c.ConnectionState()
-		return state.NegotiatedProtocolIsMutual &&
-			state.NegotiatedProtocol == "h2"
-	}
-	return false
-}
-
 // NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
 func NewCustomTLSDialer(config *CustomTLSConfig) common.Dialer {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
@@ -369,21 +186,28 @@ func NewCustomTLSDialer(config *CustomTLSConfig) common.Dialer {
 	}
 }
 
-// CustomTLSDial is a customized replacement for tls.Dial.
-// Based on tlsdialer.DialWithDialer which is based on crypto/tls.DialWithDialer.
-//
-// To ensure optimal TLS profile selection when using CustomTLSDial for tunnel
-// protocols, call SelectTLSProfile first and set its result into
-// config.TLSProfile.
+// CustomTLSDial dials a new TLS connection using the parameters set in
+// CustomTLSConfig.
 //
-// tlsdialer comment:
-//   Note - if sendServerName is false, the VerifiedChains field on the
-//   connection's ConnectionState will never get populated.
+// The dial aborts if ctx becomes Done before the dial completes.
 func CustomTLSDial(
 	ctx context.Context,
 	network, addr string,
 	config *CustomTLSConfig) (net.Conn, error) {
 
+	if (config.SkipVerify &&
+		(config.VerifyLegacyCertificate != nil ||
+			len(config.VerifyServerName) > 0 ||
+			len(config.VerifyPins) > 0)) ||
+
+		(config.VerifyLegacyCertificate != nil &&
+			(config.SkipVerify ||
+				len(config.VerifyServerName) > 0 ||
+				len(config.VerifyPins) > 0)) {
+
+		return nil, errors.TraceNew("incompatible certification verification parameters")
+	}
+
 	p := config.Parameters.Get()
 
 	dialAddr := addr
@@ -402,51 +226,116 @@ func CustomTLSDial(
 		return nil, errors.Trace(err)
 	}
 
-	selectedTLSProfile := config.TLSProfile
+	var tlsConfigRootCAs *x509.CertPool
+	if !config.SkipVerify &&
+		config.VerifyLegacyCertificate == nil &&
+		config.TrustedCACertificatesFilename != "" {
 
-	if selectedTLSProfile == "" {
-		selectedTLSProfile = SelectTLSProfile(false, false, "", p)
+		tlsConfigRootCAs = x509.NewCertPool()
+		certData, err := ioutil.ReadFile(config.TrustedCACertificatesFilename)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		tlsConfigRootCAs.AppendCertsFromPEM(certData)
 	}
 
+	// In some cases, config.SkipVerify is false, but
+	// utls.Config.InsecureSkipVerify will be set to true to disable verification
+	// in utls that will otherwise fail: when SNI is omitted, and when
+	// VerifyServerName differs from SNI. In these cases, the certificate chain
+	// is verified in VerifyPeerCertificate.
+
 	tlsConfigInsecureSkipVerify := false
 	tlsConfigServerName := ""
+	verifyServerName := hostname
 
 	if config.SkipVerify {
 		tlsConfigInsecureSkipVerify = true
 	}
 
 	if config.UseDialAddrSNI {
+
+		// Set SNI to match the dial hostname. This is the standard case.
 		tlsConfigServerName = hostname
-	} else if config.SNIServerName != "" && config.VerifyLegacyCertificate == nil {
-		// Set the ServerName and rely on the usual logic in
-		// tls.Conn.Handshake() to do its verification.
-		// Note: Go TLS will automatically omit this ServerName when it's an IP address
+
+	} else if config.SNIServerName != "" {
+
+		// Set a custom SNI value. If this value doesn't match the server
+		// certificate, SkipVerify and/or VerifyServerName may need to be
+		// configured; but by itself this case doesn't necessarily require
+		// custom certificate verification.
 		tlsConfigServerName = config.SNIServerName
+
 	} else {
-		// No SNI.
-		// Disable verification in tls.Conn.Handshake().  We'll verify manually
-		// after handshaking
+
+		// Omit SNI. If SkipVerify is not set, this case requires custom certificate
+		// verification, which will check that the server certificate matches either
+		// the dial hostname or VerifyServerName, as if the SNI were set to one of
+		// those values.
 		tlsConfigInsecureSkipVerify = true
 	}
 
-	var tlsRootCAs *x509.CertPool
+	// When VerifyServerName does not match the SNI, custom certificate
+	// verification is necessary.
+	if config.VerifyServerName != "" && config.VerifyServerName != tlsConfigServerName {
+		verifyServerName = config.VerifyServerName
+		tlsConfigInsecureSkipVerify = true
+	}
 
-	if !config.SkipVerify &&
-		config.VerifyLegacyCertificate == nil &&
-		config.TrustedCACertificatesFilename != "" {
+	// With the VerifyPeerCertificate callback, we perform any custom certificate
+	// verification at the same point in the TLS handshake as standard utls
+	// verification; and abort the handshake at the same point, if custom
+	// verification fails.
+	var tlsConfigVerifyPeerCertificate func([][]byte, [][]*x509.Certificate) error
+	if !config.SkipVerify {
+		tlsConfigVerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
 
-		tlsRootCAs = x509.NewCertPool()
-		certData, err := ioutil.ReadFile(config.TrustedCACertificatesFilename)
-		if err != nil {
-			return nil, errors.Trace(err)
+			if config.VerifyLegacyCertificate != nil {
+				return verifyLegacyCertificate(
+					rawCerts, config.VerifyLegacyCertificate)
+			}
+
+			if tlsConfigInsecureSkipVerify {
+
+				// Limitation: this verification path does not set the utls.Conn's
+				// ConnectionState certificate information.
+
+				if len(verifiedChains) > 0 {
+					return errors.TraceNew("unexpected verified chains")
+				}
+				var err error
+				verifiedChains, err = verifyServerCertificate(
+					tlsConfigRootCAs, rawCerts, verifyServerName)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+
+			if len(config.VerifyPins) > 0 {
+				err := verifyCertificatePins(
+					config.VerifyPins, verifiedChains)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+
+			return nil
 		}
-		tlsRootCAs.AppendCertsFromPEM(certData)
 	}
 
+	// Note: utls will automatically omit SNI when ServerName is an IP address.
+
 	tlsConfig := &utls.Config{
-		RootCAs:            tlsRootCAs,
-		InsecureSkipVerify: tlsConfigInsecureSkipVerify,
-		ServerName:         tlsConfigServerName,
+		RootCAs:               tlsConfigRootCAs,
+		InsecureSkipVerify:    tlsConfigInsecureSkipVerify,
+		ServerName:            tlsConfigServerName,
+		VerifyPeerCertificate: tlsConfigVerifyPeerCertificate,
+	}
+
+	selectedTLSProfile := config.TLSProfile
+
+	if selectedTLSProfile == "" {
+		selectedTLSProfile = SelectTLSProfile(false, false, "", p)
 	}
 
 	utlsClientHelloID, utlsClientHelloSpec, err := getUTLSClientHelloID(
@@ -697,16 +586,6 @@ func CustomTLSDial(
 		<-resultChannel
 	}
 
-	if err == nil && !config.SkipVerify && tlsConfigInsecureSkipVerify {
-
-		if config.VerifyLegacyCertificate != nil {
-			err = verifyLegacyCertificate(conn, config.VerifyLegacyCertificate)
-		} else {
-			// Manually verify certificates
-			err = verifyServerCerts(conn, hostname)
-		}
-	}
-
 	if err != nil {
 		rawConn.Close()
 		return nil, errors.Trace(err)
@@ -715,24 +594,33 @@ func CustomTLSDial(
 	return conn, nil
 }
 
-func verifyLegacyCertificate(conn *utls.UConn, expectedCertificate *x509.Certificate) error {
-	certs := conn.ConnectionState().PeerCertificates
-	if len(certs) < 1 {
-		return errors.TraceNew("no certificate to verify")
+func verifyLegacyCertificate(rawCerts [][]byte, expectedCertificate *x509.Certificate) error {
+	if len(rawCerts) < 1 {
+		return errors.TraceNew("missing certificate")
 	}
-	if !bytes.Equal(certs[0].Raw, expectedCertificate.Raw) {
+	if !bytes.Equal(rawCerts[0], expectedCertificate.Raw) {
 		return errors.TraceNew("unexpected certificate")
 	}
 	return nil
 }
 
-func verifyServerCerts(conn *utls.UConn, hostname string) error {
-	certs := conn.ConnectionState().PeerCertificates
+func verifyServerCertificate(
+	rootCAs *x509.CertPool, rawCerts [][]byte, verifyServerName string) ([][]*x509.Certificate, error) {
+
+	// This duplicates the verification logic in utls (and standard crypto/tls).
+
+	certs := make([]*x509.Certificate, len(rawCerts))
+	for i, rawCert := range rawCerts {
+		cert, err := x509.ParseCertificate(rawCert)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		certs[i] = cert
+	}
 
 	opts := x509.VerifyOptions{
-		Roots:         nil, // Use host's root CAs
-		CurrentTime:   time.Now(),
-		DNSName:       hostname,
+		Roots:         rootCAs,
+		DNSName:       verifyServerName,
 		Intermediates: x509.NewCertPool(),
 	}
 
@@ -743,11 +631,230 @@ func verifyServerCerts(conn *utls.UConn, hostname string) error {
 		opts.Intermediates.AddCert(cert)
 	}
 
-	_, err := certs[0].Verify(opts)
+	verifiedChains, err := certs[0].Verify(opts)
 	if err != nil {
-		return errors.Trace(err)
+		return nil, errors.Trace(err)
 	}
-	return nil
+
+	return verifiedChains, nil
+}
+
+func verifyCertificatePins(pins []string, verifiedChains [][]*x509.Certificate) error {
+	for _, chain := range verifiedChains {
+		for _, cert := range chain {
+			publicKeyDigest := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
+			expectedPin := base64.StdEncoding.EncodeToString(publicKeyDigest[:])
+			if common.Contains(pins, expectedPin) {
+				// Return success on the first match of any certificate public key to any
+				// pin.
+				return nil
+			}
+		}
+	}
+	return errors.TraceNew("no pin found")
+}
+
+func IsTLSConnUsingHTTP2(conn net.Conn) bool {
+	if c, ok := conn.(*utls.UConn); ok {
+		state := c.ConnectionState()
+		return state.NegotiatedProtocolIsMutual &&
+			state.NegotiatedProtocol == "h2"
+	}
+	return false
+}
+
+// SelectTLSProfile picks a TLS profile at random from the available candidates.
+func SelectTLSProfile(
+	requireTLS12SessionTickets bool,
+	isFronted bool,
+	frontingProviderID string,
+	p parameters.ParametersAccessor) string {
+
+	// Two TLS profile lists are constructed, subject to limit constraints:
+	// stock, fixed parrots (non-randomized SupportedTLSProfiles) and custom
+	// parrots (CustomTLSProfileNames); and randomized. If one list is empty, the
+	// non-empty list is used. Otherwise SelectRandomizedTLSProfileProbability
+	// determines which list is used.
+	//
+	// Note that LimitTLSProfiles is not applied to CustomTLSProfiles; the
+	// presence of a candidate in CustomTLSProfiles is treated as explicit
+	// enabling.
+	//
+	// UseOnlyCustomTLSProfiles may be used to disable all stock TLS profiles and
+	// use only CustomTLSProfiles; UseOnlyCustomTLSProfiles is ignored if
+	// CustomTLSProfiles is empty.
+	//
+	// For fronted servers, DisableFrontingProviderTLSProfiles may be used
+	// to disable TLS profiles which are incompatible with the TLS stack used
+	// by the front. For example, if a utls parrot doesn't fully support all
+	// of the capabilities in the ClientHello. Unlike the LimitTLSProfiles case,
+	// DisableFrontingProviderTLSProfiles may disable CustomTLSProfiles.
+
+	limitTLSProfiles := p.TLSProfiles(parameters.LimitTLSProfiles)
+	var disableTLSProfiles protocol.TLSProfiles
+
+	if isFronted && frontingProviderID != "" {
+		disableTLSProfiles = p.LabeledTLSProfiles(
+			parameters.DisableFrontingProviderTLSProfiles, frontingProviderID)
+	}
+
+	randomizedTLSProfiles := make([]string, 0)
+	parrotTLSProfiles := make([]string, 0)
+
+	for _, tlsProfile := range p.CustomTLSProfileNames() {
+		if !common.Contains(disableTLSProfiles, tlsProfile) {
+			parrotTLSProfiles = append(parrotTLSProfiles, tlsProfile)
+		}
+	}
+
+	useOnlyCustomTLSProfiles := p.Bool(parameters.UseOnlyCustomTLSProfiles)
+	if useOnlyCustomTLSProfiles && len(parrotTLSProfiles) == 0 {
+		useOnlyCustomTLSProfiles = false
+	}
+
+	if !useOnlyCustomTLSProfiles {
+		for _, tlsProfile := range protocol.SupportedTLSProfiles {
+
+			if len(limitTLSProfiles) > 0 &&
+				!common.Contains(limitTLSProfiles, tlsProfile) {
+				continue
+			}
+
+			if common.Contains(disableTLSProfiles, tlsProfile) {
+				continue
+			}
+
+			// requireTLS12SessionTickets is specified for
+			// UNFRONTED-MEEK-SESSION-TICKET-OSSH, a protocol which depends on using
+			// obfuscated session tickets to ensure that the server doesn't send its
+			// certificate in the TLS handshake. TLS 1.2 profiles which omit session
+			// tickets should not be selected. As TLS 1.3 encrypts the server
+			// certificate message, there's no exclusion for TLS 1.3.
+
+			if requireTLS12SessionTickets &&
+				protocol.TLS12ProfileOmitsSessionTickets(tlsProfile) {
+				continue
+			}
+
+			if protocol.TLSProfileIsRandomized(tlsProfile) {
+				randomizedTLSProfiles = append(randomizedTLSProfiles, tlsProfile)
+			} else {
+				parrotTLSProfiles = append(parrotTLSProfiles, tlsProfile)
+			}
+		}
+	}
+
+	if len(randomizedTLSProfiles) > 0 &&
+		(len(parrotTLSProfiles) == 0 ||
+			p.WeightedCoinFlip(parameters.SelectRandomizedTLSProfileProbability)) {
+
+		return randomizedTLSProfiles[prng.Intn(len(randomizedTLSProfiles))]
+	}
+
+	if len(parrotTLSProfiles) == 0 {
+		return ""
+	}
+
+	return parrotTLSProfiles[prng.Intn(len(parrotTLSProfiles))]
+}
+
+func getUTLSClientHelloID(
+	p parameters.ParametersAccessor,
+	tlsProfile string) (utls.ClientHelloID, *utls.ClientHelloSpec, error) {
+
+	switch tlsProfile {
+	case protocol.TLS_PROFILE_IOS_111:
+		return utls.HelloIOS_11_1, nil, nil
+	case protocol.TLS_PROFILE_IOS_121:
+		return utls.HelloIOS_12_1, nil, nil
+	case protocol.TLS_PROFILE_CHROME_58:
+		return utls.HelloChrome_58, nil, nil
+	case protocol.TLS_PROFILE_CHROME_62:
+		return utls.HelloChrome_62, nil, nil
+	case protocol.TLS_PROFILE_CHROME_70:
+		return utls.HelloChrome_70, nil, nil
+	case protocol.TLS_PROFILE_CHROME_72:
+		return utls.HelloChrome_72, nil, nil
+	case protocol.TLS_PROFILE_CHROME_83:
+		return utls.HelloChrome_83, nil, nil
+	case protocol.TLS_PROFILE_FIREFOX_55:
+		return utls.HelloFirefox_55, nil, nil
+	case protocol.TLS_PROFILE_FIREFOX_56:
+		return utls.HelloFirefox_56, nil, nil
+	case protocol.TLS_PROFILE_FIREFOX_65:
+		return utls.HelloFirefox_65, nil, nil
+	case protocol.TLS_PROFILE_RANDOMIZED:
+		return utls.HelloRandomized, nil, nil
+	}
+
+	// utls.HelloCustom with a utls.ClientHelloSpec is used for
+	// CustomTLSProfiles.
+
+	customTLSProfile := p.CustomTLSProfile(tlsProfile)
+	if customTLSProfile == nil {
+		return utls.HelloCustom,
+			nil,
+			errors.Tracef("unknown TLS profile: %s", tlsProfile)
+	}
+
+	utlsClientHelloSpec, err := customTLSProfile.GetClientHelloSpec()
+	if err != nil {
+		return utls.ClientHelloID{}, nil, errors.Trace(err)
+	}
+
+	return utls.HelloCustom, utlsClientHelloSpec, nil
+}
+
+func getClientHelloVersion(
+	utlsClientHelloID utls.ClientHelloID,
+	utlsClientHelloSpec *utls.ClientHelloSpec) (string, error) {
+
+	switch utlsClientHelloID {
+
+	case utls.HelloIOS_11_1, utls.HelloIOS_12_1, utls.HelloChrome_58,
+		utls.HelloChrome_62, utls.HelloFirefox_55, utls.HelloFirefox_56:
+		return protocol.TLS_VERSION_12, nil
+
+	case utls.HelloChrome_70, utls.HelloChrome_72, utls.HelloChrome_83,
+		utls.HelloFirefox_65, utls.HelloGolang:
+		return protocol.TLS_VERSION_13, nil
+	}
+
+	// As utls.HelloRandomized/Custom may be either TLS 1.2 or TLS 1.3, we cannot
+	// perform a simple ClientHello ID check. BuildHandshakeState is run, which
+	// constructs the entire ClientHello.
+	//
+	// Assumes utlsClientHelloID.Seed has been set; otherwise the result is
+	// ephemeral.
+	//
+	// BenchmarkRandomizedGetClientHelloVersion indicates that this operation
+	// takes on the order of 0.05ms and allocates ~8KB for randomized client
+	// hellos.
+
+	conn := utls.UClient(
+		nil,
+		&utls.Config{InsecureSkipVerify: true},
+		utlsClientHelloID)
+
+	if utlsClientHelloSpec != nil {
+		err := conn.ApplyPreset(utlsClientHelloSpec)
+		if err != nil {
+			return "", errors.Trace(err)
+		}
+	}
+
+	err := conn.BuildHandshakeState()
+	if err != nil {
+		return "", errors.Trace(err)
+	}
+
+	for _, v := range conn.HandshakeState.Hello.SupportedVersions {
+		if v == utls.VersionTLS13 {
+			return protocol.TLS_VERSION_13, nil
+		}
+	}
+
+	return protocol.TLS_VERSION_12, nil
 }
 
 func init() {

+ 299 - 0
psiphon/tlsDialer_test.go

@@ -21,11 +21,24 @@ package psiphon
 
 import (
 	"context"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/base64"
 	"encoding/json"
+	"encoding/pem"
 	"fmt"
 	"io/ioutil"
+	"math/big"
 	"net"
+	"net/http"
+	"os"
+	"path/filepath"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
@@ -37,6 +50,292 @@ import (
 	utls "github.com/refraction-networking/utls"
 )
 
+func TestTLSCertificateVerification(t *testing.T) {
+
+	testDataDirName, err := ioutil.TempDir("", "psiphon-tls-certificate-verification-test")
+	if err != nil {
+		t.Fatalf("TempDir failed: %s\n", err)
+	}
+	defer os.RemoveAll(testDataDirName)
+
+	// Generate a root CA certificate.
+
+	rootCACertificate := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject: pkix.Name{
+			Organization: []string{"test"},
+		},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(1, 0, 0),
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+
+	rootCAPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey failed: %s", err)
+	}
+
+	rootCACertificateBytes, err := x509.CreateCertificate(
+		rand.Reader,
+		rootCACertificate,
+		rootCACertificate,
+		&rootCAPrivateKey.PublicKey,
+		rootCAPrivateKey)
+	if err != nil {
+		t.Fatalf("x509.CreateCertificate failed: %s", err)
+	}
+
+	pemRootCACertificate := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: rootCACertificateBytes,
+		})
+
+	// Generate a server certificate.
+
+	serverName := "example.org"
+
+	serverCertificate := &x509.Certificate{
+		SerialNumber: big.NewInt(2),
+		Subject: pkix.Name{
+			Organization: []string{"test"},
+		},
+		DNSNames:    []string{serverName},
+		NotBefore:   time.Now(),
+		NotAfter:    time.Now().AddDate(1, 0, 0),
+		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		KeyUsage:    x509.KeyUsageDigitalSignature,
+	}
+
+	serverPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		t.Fatalf("rsa.GenerateKey failed: %s", err)
+	}
+
+	serverCertificateBytes, err := x509.CreateCertificate(
+		rand.Reader,
+		serverCertificate,
+		rootCACertificate,
+		&serverPrivateKey.PublicKey,
+		rootCAPrivateKey)
+	if err != nil {
+		t.Fatalf("x509.CreateCertificate failed: %s", err)
+	}
+
+	pemServerCertificate := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: serverCertificateBytes,
+		})
+
+	pemServerPrivateKey := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "RSA PRIVATE KEY",
+			Bytes: x509.MarshalPKCS1PrivateKey(serverPrivateKey),
+		})
+
+	// Run an HTTPS server with the server certificate.
+
+	dialAddr := "127.0.0.1:8000"
+	serverAddr := fmt.Sprintf("%s:8000", serverName)
+
+	serverKeyPair, err := tls.X509KeyPair(
+		pemServerCertificate, pemServerPrivateKey)
+	if err != nil {
+		t.Fatalf("tls.X509KeyPair failed: %s", err)
+	}
+
+	server := &http.Server{
+		Addr: dialAddr,
+		TLSConfig: &tls.Config{
+			Certificates: []tls.Certificate{serverKeyPair},
+		},
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		wg.Done()
+		server.ListenAndServeTLS("", "")
+	}()
+
+	defer func() {
+		server.Shutdown(context.Background())
+		wg.Wait()
+	}()
+
+	// Test: without custom RootCAs, the TLS dial fails.
+
+	params, err := parameters.NewParameters(nil)
+	if err != nil {
+		t.Fatalf("parameters.NewParameters failed: %s", err)
+	}
+
+	dialer := func(ctx context.Context, network, address string) (net.Conn, error) {
+		d := &net.Dialer{}
+		// Ignore the address input, which will be serverAddr, and dial dialAddr, as
+		// if the serverName in serverAddr had been resolved to "127.0.0.1".
+		return d.DialContext(ctx, network, dialAddr)
+	}
+
+	conn, err := CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters: params,
+			Dial:       dialer,
+		})
+
+	if err == nil {
+		conn.Close()
+		t.Errorf("unexpected success without custom RootCAs")
+	}
+
+	// Test: without custom RootCAs and with SkipVerify, the TLS dial succeeds.
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters: params,
+			Dial:       dialer,
+			SkipVerify: true,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+
+	// Test: with custom RootCAs, the TLS dial succeeds.
+
+	rootCAsFileName := filepath.Join(testDataDirName, "RootCAs.pem")
+	err = ioutil.WriteFile(rootCAsFileName, pemRootCACertificate, 0600)
+	if err != nil {
+		t.Fatalf("WriteFile failed: %s", err)
+	}
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+
+	// Test: with SNI changed and VerifyServerName set, the TLS dial succeeds.
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			SNIServerName:                 "not-" + serverName,
+			VerifyServerName:              serverName,
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+
+	// Test: with an invalid pin, the TLS dial fails.
+
+	invalidPin := base64.StdEncoding.EncodeToString(make([]byte, 32))
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			VerifyPins:                    []string{invalidPin},
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err == nil {
+		conn.Close()
+		t.Errorf("unexpected success without invalid pin")
+	}
+
+	// Test: with the root CA certirficate pinned, the TLS dial succeeds.
+
+	parsedCertificate, err := x509.ParseCertificate(rootCACertificateBytes)
+	if err != nil {
+		t.Fatalf("x509.ParseCertificate failed: %s", err)
+	}
+	publicKeyDigest := sha256.Sum256(parsedCertificate.RawSubjectPublicKeyInfo)
+	rootCACertificatePin := base64.StdEncoding.EncodeToString(publicKeyDigest[:])
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			VerifyPins:                    []string{rootCACertificatePin},
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+
+	// Test: with the server certificate pinned, the TLS dial succeeds.
+
+	parsedCertificate, err = x509.ParseCertificate(serverCertificateBytes)
+	if err != nil {
+		t.Fatalf("x509.ParseCertificate failed: %s", err)
+	}
+	publicKeyDigest = sha256.Sum256(parsedCertificate.RawSubjectPublicKeyInfo)
+	serverCertificatePin := base64.StdEncoding.EncodeToString(publicKeyDigest[:])
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			VerifyPins:                    []string{serverCertificatePin},
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+
+	// Test: with SNI changed, VerifyServerName set, and pinning the TLS dial
+	// succeeds.
+
+	conn, err = CustomTLSDial(
+		context.Background(), "tcp", serverAddr,
+		&CustomTLSConfig{
+			Parameters:                    params,
+			Dial:                          dialer,
+			SNIServerName:                 "not-" + serverName,
+			VerifyServerName:              serverName,
+			VerifyPins:                    []string{rootCACertificatePin},
+			TrustedCACertificatesFilename: rootCAsFileName,
+		})
+
+	if err != nil {
+		t.Errorf("CustomTLSDial failed: %s", err)
+	} else {
+		conn.Close()
+	}
+}
+
 func TestTLSDialerCompatibility(t *testing.T) {
 
 	// This test checks that each TLS profile can successfully complete a TLS