Эх сурвалжийг харах

Implemented work around for verifying legacy Psiphon server certificates; reuse custom https client throughout session

Rod Hynes 11 жил өмнө
parent
commit
02cd8ba16b
3 өөрчлөгдсөн 323 нэмэгдсэн , 54 устгасан
  1. 0 1
      README.md
  2. 53 53
      psiphon/serverApi.go
  3. 270 0
      psiphon/tlsDialer.go

+ 0 - 1
README.md

@@ -15,7 +15,6 @@ This project is currently at the proof-of-concept stage. Current production Psip
 
 ### TODO (proof-of-concept)
 
-* replace InsecureSkipVerify work-around for IP SANs issue (see serverApi.go) 
 * shutdown results in log noise: "use of closed network connection"
 * region preference
 * use ContextError in more places

+ 53 - 53
psiphon/serverApi.go

@@ -21,27 +21,25 @@ package psiphon
 
 import (
 	"bytes"
-	"crypto/tls"
-	"crypto/x509"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
+	"net"
 	"net/http"
-	"net/url"
 	"strconv"
 )
 
 // Session is a utility struct which holds all of the data associated
 // with a Psiphon session. In addition to the established tunnel, this
-// includes the session ID (used for Psiphon API requests) and the
-// address to use to make tunnelled HTTPS API requests.
+// includes the session ID (used for Psiphon API requests) and a http
+// client configured to make tunnelled Psiphon API requests.
 type Session struct {
-	sessionId             string
-	config                *Config
-	tunnel                *Tunnel
-	localHttpProxyAddress string
+	sessionId          string
+	config             *Config
+	tunnel             *Tunnel
+	psiphonHttpsClient *http.Client
 }
 
 // NewSession makes tunnelled handshake and connected requests to the
@@ -53,11 +51,15 @@ func NewSession(config *Config, tunnel *Tunnel, localHttpProxyAddress string) (s
 	if err != nil {
 		return nil, ContextError(err)
 	}
+	psiphonHttpsClient, err := makePsiphonHttpsClient(tunnel, localHttpProxyAddress)
+	if err != nil {
+		return nil, ContextError(err)
+	}
 	session = &Session{
-		sessionId:             sessionId,
-		config:                config,
-		tunnel:                tunnel,
-		localHttpProxyAddress: localHttpProxyAddress,
+		sessionId:          sessionId,
+		config:             config,
+		tunnel:             tunnel,
+		psiphonHttpsClient: psiphonHttpsClient,
 	}
 	// Sending two seperate requests is a legacy from when the handshake was
 	// performed before a tunnel was established and the connect was performed
@@ -92,8 +94,8 @@ func (session *Session) doHandshakeRequest() error {
 	for _, ipAddress := range serverEntryIpAddresses {
 		extraParams = append(extraParams, &ExtraParam{"known_server", ipAddress})
 	}
-	url := buildRequestUrl(session, "handshake", extraParams...)
-	responseBody, err := doGetRequest(session, url)
+	url := session.buildRequestUrl("handshake", extraParams...)
+	responseBody, err := session.doGetRequest(url)
 	if err != nil {
 		return ContextError(err)
 	}
@@ -169,12 +171,11 @@ func (session *Session) doConnectedRequest() error {
 	if lastConnected == "" {
 		lastConnected = "None"
 	}
-	url := buildRequestUrl(
-		session,
+	url := session.buildRequestUrl(
 		"connected",
 		&ExtraParam{"session_id", session.sessionId},
 		&ExtraParam{"last_connected", lastConnected})
-	responseBody, err := doGetRequest(session, url)
+	responseBody, err := session.doGetRequest(url)
 	if err != nil {
 		return ContextError(err)
 	}
@@ -197,9 +198,11 @@ type ExtraParam struct{ name, value string }
 // buildRequestUrl makes a URL containing all the common parameters
 // that are included with Psiphon API requests. These common parameters
 // are used for statistics.
-func buildRequestUrl(session *Session, path string, extraParams ...*ExtraParam) string {
+func (session *Session) buildRequestUrl(path string, extraParams ...*ExtraParam) string {
 	var requestUrl bytes.Buffer
-	requestUrl.WriteString("https://")
+	// Note: don't prefix with HTTPS scheme, see comment in doGetRequest.
+	// e.g., don't do this: requestUrl.WriteString("https://")
+	requestUrl.WriteString("http://")
 	requestUrl.WriteString(session.tunnel.serverEntry.IpAddress)
 	requestUrl.WriteString(":")
 	requestUrl.WriteString(session.tunnel.serverEntry.WebServerPort)
@@ -231,39 +234,9 @@ func buildRequestUrl(session *Session, path string, extraParams ...*ExtraParam)
 	return requestUrl.String()
 }
 
-// doGetRequest makes a tunneled HTTPS request, validating the server using the server
-// entry web server certificate. This function discards its http.Client after a single
-// use -- it is not intended for making many requests.
-func doGetRequest(session *Session, requestUrl string) (responseBody []byte, err error) {
-	proxyUrl, err := url.Parse(fmt.Sprintf("http://%s", session.localHttpProxyAddress))
-	if err != nil {
-		return nil, ContextError(err)
-	}
-	proxy := http.ProxyURL(proxyUrl)
-	certificate, err := DecodeCertificate(session.tunnel.serverEntry.WebServerCertificate)
-	if err != nil {
-		return nil, ContextError(err)
-	}
-	certPool := x509.NewCertPool()
-	certPool.AddCert(certificate)
-	// Copy default transport for its timeout values
-	transport := new(http.Transport)
-	*transport = *http.DefaultTransport.(*http.Transport)
-	// ****** SECURITY ISSUE ******
-	// TODO: temporarily using InsecureSkipVerify to work around hostname verification error:
-	// "x509: cannot validate certificate for " + h.Host + " because it doesn't contain any IP SANs"
-	// Notes:
-	// - Since Psiphon server self-signed certs don't have IP SANs, we need to disable that part
-	// of verification. The client has to be able to handle existing server certificates.
-	// - We can't easily supply a custom TLS dialer (e.g., such as https://github.com/getlantern/tlsdialer)
-	// since the dialer has to deal with HTTP proxying before talking TLS. See:
-	// dialConn in http://golang.org/src/pkg/net/http/transport.go
-	// - Mitigating factor: the InsecureSkipVerify TLS is done through the secure, authenticated tunnel
-	// and terminates at the tunnel server host.
-	transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true, RootCAs: certPool}
-	transport.Proxy = proxy
-	httpClient := &http.Client{Transport: transport}
-	response, err := httpClient.Get(requestUrl)
+// doGetRequest makes a tunneled HTTPS request and returns the response body.
+func (session *Session) doGetRequest(requestUrl string) (responseBody []byte, err error) {
+	response, err := session.psiphonHttpsClient.Get(requestUrl)
 	if err != nil {
 		return nil, ContextError(err)
 	}
@@ -277,3 +250,30 @@ func doGetRequest(session *Session, requestUrl string) (responseBody []byte, err
 	}
 	return body, nil
 }
+
+// makeHttpsClient creates a Psiphon HTTPS client that uses the local http proxy to tunnel
+// requests and which validates the web server using the Psiphon server entry web server certificate.
+// This is not a general purpose HTTPS client.
+// As the custom dialer makes an explicit TLS connection, URLs submitted to the returned
+// http.Client should use the "http://" scheme. Otherwise http.Transport will try to do another TLS
+// handshake inside the explicit TLS session.
+func makePsiphonHttpsClient(tunnel *Tunnel, localHttpProxyAddress string) (httpsClient *http.Client, err error) {
+	certificate, err := DecodeCertificate(tunnel.serverEntry.WebServerCertificate)
+	if err != nil {
+		return nil, ContextError(err)
+	}
+	dialer := func(network, addr string) (net.Conn, error) {
+		customTLSConfig := &CustomTLSConfig{
+			sendServerName:          false,
+			verifyLegacyCertificate: certificate,
+			httpProxyAddress:        localHttpProxyAddress,
+		}
+		return CustomTLSDial(network, addr, customTLSConfig)
+	}
+	// Copy default transport for its timeout values
+	transport := new(http.Transport)
+	*transport = *http.DefaultTransport.(*http.Transport)
+	transport.Dial = dialer
+	transport.Proxy = nil
+	return &http.Client{Transport: transport}, nil
+}

+ 270 - 0
psiphon/tlsDialer.go

@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2014, 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/>.
+ *
+ */
+
+/*
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+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)
+// 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
+
+import (
+	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"strings"
+	"time"
+)
+
+type timeoutError struct{}
+
+func (timeoutError) Error() string   { return "tls: DialWithDialer timed out" }
+func (timeoutError) Timeout() bool   { return true }
+func (timeoutError) Temporary() bool { return true }
+
+// CustomTLSConfig contains parameters to determine the behavior
+// of CustomTLSDial.
+// httpProxyAddress - use the specified HTTP proxy (HTTP CONNECT) if not blank
+// sendServerName - use SNI (tlsdialer functionality)
+// verifyLegacyCertificate - 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.
+// tlsConfig - a tls.Config use in the non-verifyLegacyCertificate case.
+type CustomTLSConfig struct {
+	httpProxyAddress        string
+	sendServerName          bool
+	verifyLegacyCertificate *x509.Certificate
+	tlsConfig               *tls.Config
+}
+
+// tlsdialer:
+// Like crypto/tls.Dial, but with the ability to control whether or not to
+// send the ServerName extension in client handshakes through the sendServerName
+// flag.
+//
+// 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) {
+	return CustomTLSDialWithDialer(new(net.Dialer), network, addr, config)
+}
+
+// tlsdialer:
+// Like crypto/tls.DialWithDialer, but with the ability to control whether or
+// not to send the ServerName extension in client handshakes through the
+// sendServerName flag.
+//
+// Note - if sendServerName is false, the VerifiedChains field on the
+// connection's ConnectionState will never get populated.
+func CustomTLSDialWithDialer(dialer *net.Dialer, network, addr string, config *CustomTLSConfig) (*tls.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
+	// also need to start our own timers now.
+	timeout := dialer.Timeout
+
+	if !dialer.Deadline.IsZero() {
+		deadlineTimeout := dialer.Deadline.Sub(time.Now())
+		if timeout == 0 || deadlineTimeout < timeout {
+			timeout = deadlineTimeout
+		}
+	}
+
+	var errChannel chan error
+
+	if timeout != 0 {
+		errChannel = make(chan error, 2)
+		time.AfterFunc(timeout, func() {
+			errChannel <- timeoutError{}
+		})
+	}
+
+	dialAddr := addr
+	if config.httpProxyAddress != "" {
+		dialAddr = config.httpProxyAddress
+	}
+
+	rawConn, err := dialer.Dial(network, dialAddr)
+	if err != nil {
+		return nil, err
+	}
+
+	colonPos := strings.LastIndex(addr, ":")
+	if colonPos == -1 {
+		colonPos = len(addr)
+	}
+	hostname := addr[:colonPos]
+
+	tlsConfig := config.tlsConfig
+	if tlsConfig == nil {
+		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
+	}
+
+	// copy config so we can tweak it
+	tlsConfigCopy := new(tls.Config)
+	*tlsConfigCopy = *tlsConfig
+
+	if config.sendServerName {
+		// Set the ServerName and rely on the usual logic in
+		// tls.Conn.Handshake() to do its verification
+		tlsConfigCopy.ServerName = serverName
+	} else {
+		// Disable verification in tls.Conn.Handshake().  We'll verify manually
+		// after handshaking
+		tlsConfigCopy.InsecureSkipVerify = true
+	}
+
+	conn := tls.Client(rawConn, tlsConfigCopy)
+
+	establishConnection := func(rawConn net.Conn, conn *tls.Conn) error {
+		// TODO: use the proxy request/response code from net/http/transport.go
+		if config.httpProxyAddress != "" {
+			connectRequest := fmt.Sprintf(
+				"CONNECT %s HTTP/1.1\r\nHost: %s\r\nConnection: Keep-Alive\r\n\r\n",
+				addr, hostname)
+			_, err := rawConn.Write([]byte(connectRequest))
+			if err != nil {
+				return err
+			}
+			expectedResponse := []byte("HTTP/1.1 200 OK\r\n\r\n")
+			readBuffer := make([]byte, len(expectedResponse))
+			_, err = io.ReadFull(rawConn, readBuffer)
+			if err != nil {
+				return err
+			}
+			if !bytes.Equal(readBuffer, expectedResponse) {
+				return fmt.Errorf("unexpected HTTP proxy response: %s", string(readBuffer))
+			}
+		}
+		return conn.Handshake()
+	}
+
+	if timeout == 0 {
+		err = establishConnection(rawConn, conn)
+	} else {
+		go func() {
+			errChannel <- establishConnection(rawConn, conn)
+		}()
+		err = <-errChannel
+	}
+
+	log.Printf("TEMP tlsDialer establishConnection done: %+v", conn.ConnectionState())
+
+	if err == nil && config.verifyLegacyCertificate != nil {
+		err = verifyLegacyCertificate(conn, config.verifyLegacyCertificate)
+	} else if err == nil && !config.sendServerName && !tlsConfig.InsecureSkipVerify {
+		// Manually verify certificates
+		err = verifyServerCerts(conn, serverName, tlsConfigCopy)
+	}
+
+	if err != nil {
+		rawConn.Close()
+		return nil, err
+	}
+
+	return conn, nil
+}
+
+func verifyLegacyCertificate(conn *tls.Conn, expectedCertificate *x509.Certificate) error {
+	certs := conn.ConnectionState().PeerCertificates
+	if len(certs) < 1 {
+		return errors.New("no certificate to verify")
+	}
+	if !bytes.Equal(certs[0].Raw, expectedCertificate.Raw) {
+		return errors.New("unexpected certificate")
+	}
+	return nil
+}
+
+func verifyServerCerts(conn *tls.Conn, serverName string, config *tls.Config) error {
+	certs := conn.ConnectionState().PeerCertificates
+
+	opts := x509.VerifyOptions{
+		Roots:         config.RootCAs,
+		CurrentTime:   time.Now(),
+		DNSName:       serverName,
+		Intermediates: x509.NewCertPool(),
+	}
+
+	for i, cert := range certs {
+		if i == 0 {
+			continue
+		}
+		opts.Intermediates.AddCert(cert)
+	}
+	_, err := certs[0].Verify(opts)
+	return err
+}