Sfoglia il codice sorgente

Add TLS session ticket obfuscation protocol

Rod Hynes 9 anni fa
parent
commit
67a17982dd

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

@@ -24,12 +24,13 @@ import (
 )
 )
 
 
 const (
 const (
-	TUNNEL_PROTOCOL_SSH                  = "SSH"
-	TUNNEL_PROTOCOL_OBFUSCATED_SSH       = "OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK       = "UNFRONTED-MEEK-OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS = "UNFRONTED-MEEK-HTTPS-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_MEEK         = "FRONTED-MEEK-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP    = "FRONTED-MEEK-HTTP-OSSH"
+	TUNNEL_PROTOCOL_SSH                           = "SSH"
+	TUNNEL_PROTOCOL_OBFUSCATED_SSH                = "OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK                = "UNFRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS          = "UNFRONTED-MEEK-HTTPS-OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET = "UNFRONTED-MEEK-SESSION-TICKET-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
 
 
 	SERVER_ENTRY_SOURCE_EMBEDDED  = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_EMBEDDED  = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_REMOTE    = "REMOTE"
 	SERVER_ENTRY_SOURCE_REMOTE    = "REMOTE"
@@ -58,6 +59,7 @@ var SupportedTunnelProtocols = []string{
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
 	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_SSH,
 	TUNNEL_PROTOCOL_SSH,
 }
 }
@@ -84,7 +86,12 @@ func TunnelProtocolUsesMeekHTTP(protocol string) bool {
 
 
 func TunnelProtocolUsesMeekHTTPS(protocol string) bool {
 func TunnelProtocolUsesMeekHTTPS(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
 	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
-		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS
+		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS ||
+		protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET
+}
+
+func TunnelProtocolUsesObfuscatedSessionTickets(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET
 }
 }
 
 
 type HandshakeResponse struct {
 type HandshakeResponse struct {

+ 5 - 0
psiphon/common/tls/README.md

@@ -0,0 +1,5 @@
+This is a fork of go 1.7.3 `crypto/tls`. Changes are almost entirely contained in two new files, `obfuscated.go` and `obfuscated_test.go`, which implement obfuscated session tickets, a network obfuscation protocol based on TLS.
+
+The obfuscated session tickets protocol is implemented as an optional mode enabled through the `Config`. The implementation requires access to `crypto.tls` internals.
+
+Apart from this optional mode, this is a stock `crypto/tls`.

+ 129 - 0
psiphon/common/tls/obfuscated.go

@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2016, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package tls
+
+import (
+	"crypto/rand"
+)
+
+// Obfuscated Session Tickets
+//
+// Obfuscated session tickets is a network traffic obfuscation protocol that appears
+// to be valid TLS using session tickets. The client actually generates the session
+// ticket and encrypts it with a shared secret, enabling a TLS session that entirely
+// skips the most fingerprintable aspects of TLS.
+// The scheme is described here:
+// https://lists.torproject.org/pipermail/tor-dev/2016-September/011354.html
+//
+// Circumvention notes:
+//  - TLS session ticket implementations are widespread:
+//    https://istlsfastyet.com/#cdn-paas.
+//  - An adversary cannot easily block session ticket capability, as this requires
+//    a downgrade attack against TLS.
+//  - Anti-probing defence is provided, as the adversary must use the correct obfuscation
+//    shared secret to form valid obfuscation session ticket; otherwise server offers
+//    standard session tickets.
+//  - Limitation: TLS protocol and session ticket size correspond to golang implementation
+//    and not more common OpenSSL.
+//  - Limitation: an adversary with the obfuscation shared secret can decrypt the session
+//    ticket and observe the plaintext traffic. It's assumed that the adversary will not
+//    learn the obfuscated shared secret without also learning the address of the TLS
+//    server and blocking it anyway; it's also assumed that the TLS payload is not
+//    plaintext but is protected with some other security layer (e.g., SSH).
+//
+// Implementation notes:
+//   - Client should set its ClientSessionCache to a NewObfuscatedTLSClientSessionCache.
+//     This cache ignores the session key and always produces obfuscated session tickets.
+//   - The TLS ClientHello includes an SNI field, even when using session tickets, so
+//     the client should populate the ServerName.
+//   - Server should set its SetSessionTicketKeys with first a standard key, followed by
+//     the obfuscation shared secret.
+//   - Since the client creates the session ticket, it selects parameters that were not
+//     negotiated with the server, such as the cipher suite. It's implicitly assumed that
+//     the server can support the selected parameters.
+//
+func NewObfuscatedClientSessionCache(sharedSecret [32]byte) ClientSessionCache {
+	return &obfuscatedClientSessionCache{
+		sharedSecret: sharedSecret,
+		realTickets:  NewLRUClientSessionCache(-1),
+	}
+}
+
+type obfuscatedClientSessionCache struct {
+	sharedSecret [32]byte
+	realTickets  ClientSessionCache
+}
+
+func (cache *obfuscatedClientSessionCache) Put(key string, state *ClientSessionState) {
+	// When new, real session tickets are issued, use them.
+	cache.realTickets.Put(key, state)
+}
+
+func (cache *obfuscatedClientSessionCache) Get(key string) (*ClientSessionState, bool) {
+	clientSessionState, ok := cache.realTickets.Get(key)
+	if ok {
+		return clientSessionState, true
+	}
+	// Bootstrap with an obfuscated session ticket.
+	clientSessionState, err := newObfuscatedClientSessionState(cache.sharedSecret)
+	if err != nil {
+		// TODO: log error
+		// This will fall back to regular TLS
+		return nil, false
+	}
+	return clientSessionState, true
+}
+
+func newObfuscatedClientSessionState(sharedSecret [32]byte) (*ClientSessionState, error) {
+
+	// Create a session ticket that wasn't actually issued by the server.
+	vers := uint16(VersionTLS12)
+	cipherSuite := TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+	masterSecret := make([]byte, masterSecretLength)
+	_, err := rand.Read(masterSecret)
+	if err != nil {
+		return nil, err
+	}
+	serverState := &sessionState{
+		vers:         vers,
+		cipherSuite:  cipherSuite,
+		masterSecret: masterSecret,
+		certificates: nil,
+	}
+	c := &Conn{
+		config: &Config{
+			sessionTicketKeys: []ticketKey{ticketKeyFromBytes(sharedSecret)},
+		},
+	}
+	sessionTicket, err := c.encryptTicket(serverState)
+	if err != nil {
+		return nil, err
+	}
+
+	// Pretend we got that session ticket from the server.
+	clientState := &ClientSessionState{
+		sessionTicket: sessionTicket,
+		vers:          vers,
+		cipherSuite:   cipherSuite,
+		masterSecret:  masterSecret,
+	}
+
+	return clientState, nil
+}

+ 182 - 0
psiphon/common/tls/obfuscated_test.go

@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2016, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package tls
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha1"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"errors"
+	"io"
+	"math/big"
+	"net"
+	"testing"
+	"time"
+)
+
+func TestObfuscatedSessionTicket(t *testing.T) {
+
+	var standardSessionTicketKey [32]byte
+	rand.Read(standardSessionTicketKey[:])
+
+	var obfuscatedSessionTicketSharedSecret [32]byte
+	rand.Read(obfuscatedSessionTicketSharedSecret[:])
+
+	// Note: SNI and certificate CN don't match
+	clientConfig := &Config{
+		ServerName: "www.example.com",
+		ClientSessionCache: NewObfuscatedClientSessionCache(
+			obfuscatedSessionTicketSharedSecret),
+	}
+
+	certificate, err := generateCertificate()
+	if err != nil {
+		t.Fatalf("generateCertificate failed: %s", err)
+	}
+
+	serverConfig := &Config{
+		Certificates:     []Certificate{*certificate},
+		NextProtos:       []string{"http/1.1"},
+		MinVersion:       VersionTLS10,
+		SessionTicketKey: obfuscatedSessionTicketSharedSecret,
+	}
+
+	serverConfig.SetSessionTicketKeys([][32]byte{
+		standardSessionTicketKey, obfuscatedSessionTicketSharedSecret})
+
+	serverAddress := ":8443"
+
+	testMessage := "test"
+
+	result := make(chan error, 1)
+
+	go func() {
+
+		listener, err := Listen("tcp", serverAddress, serverConfig)
+
+		var conn net.Conn
+		if err == nil {
+			conn, err = listener.Accept()
+		}
+
+		recv := make([]byte, len(testMessage))
+		if err == nil {
+			defer conn.Close()
+
+			_, err = io.ReadFull(conn, recv)
+		}
+		if err == nil {
+			if string(recv) != testMessage {
+				err = errors.New("unexpected payload")
+			}
+		}
+
+		// Sends nil on success
+		select {
+		case result <- err:
+		default:
+		}
+
+	}()
+
+	go func() {
+
+		conn, err := Dial("tcp", serverAddress, clientConfig)
+
+		if err == nil {
+			defer conn.Close()
+			_, err = conn.Write([]byte(testMessage))
+		}
+
+		if err != nil {
+			select {
+			case result <- err:
+			default:
+			}
+		}
+	}()
+
+	err = <-result
+	if err != nil {
+		t.Fatalf("connect failed: %s", err)
+	}
+}
+
+func generateCertificate() (*Certificate, error) {
+
+	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, err
+	}
+
+	publicKeyBytes, err := x509.MarshalPKIXPublicKey(rsaKey.Public())
+	if err != nil {
+		return nil, err
+	}
+	subjectKeyID := sha1.Sum(publicKeyBytes)
+
+	template := x509.Certificate{
+		SerialNumber:          big.NewInt(1),
+		Subject:               pkix.Name{CommonName: "www.example.org"},
+		NotBefore:             time.Now().Add(-1 * time.Hour).UTC(),
+		NotAfter:              time.Now().Add(time.Hour).UTC(),
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		IsCA:         true,
+		SubjectKeyId: subjectKeyID[:],
+		MaxPathLen:   1,
+		Version:      2,
+	}
+
+	derCert, err := x509.CreateCertificate(
+		rand.Reader,
+		&template,
+		&template,
+		rsaKey.Public(),
+		rsaKey)
+	if err != nil {
+		return nil, err
+	}
+
+	certificate := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: derCert,
+		},
+	)
+
+	privateKey := pem.EncodeToMemory(
+		&pem.Block{
+			Type:  "RSA PRIVATE KEY",
+			Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
+		},
+	)
+
+	keyPair, err := X509KeyPair(certificate, privateKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return &keyPair, nil
+}

+ 12 - 2
psiphon/meekConn.go

@@ -73,6 +73,10 @@ type MeekConfig struct {
 	// UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
 	// UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
 	UseHTTPS bool
 	UseHTTPS bool
 
 
+	// UseObfuscatedSessionTickets indicates whether to use obfuscated
+	// session tickets. Assumes UseHTTPS is true.
+	UseObfuscatedSessionTickets bool
+
 	// SNIServerName is the value to place in the TLS SNI server_name
 	// SNIServerName is the value to place in the TLS SNI server_name
 	// field when HTTPS is used.
 	// field when HTTPS is used.
 	SNIServerName string
 	SNIServerName string
@@ -190,7 +194,7 @@ func DialMeek(
 		// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
 		// exclusively connect to non-MiM CDNs); then the adversary kills the underlying TCP connection after
 		// some short period. This is mitigated by the "impaired" protocol classification mechanism.
 		// some short period. This is mitigated by the "impaired" protocol classification mechanism.
 
 
-		dialer := NewCustomTLSDialer(&CustomTLSConfig{
+		tlsConfig := &CustomTLSConfig{
 			DialAddr:                      meekConfig.DialAddress,
 			DialAddr:                      meekConfig.DialAddress,
 			Dial:                          NewTCPDialer(meekDialConfig),
 			Dial:                          NewTCPDialer(meekDialConfig),
 			Timeout:                       meekDialConfig.ConnectTimeout,
 			Timeout:                       meekDialConfig.ConnectTimeout,
@@ -198,7 +202,13 @@ func DialMeek(
 			SkipVerify:                    true,
 			SkipVerify:                    true,
 			UseIndistinguishableTLS:       meekDialConfig.UseIndistinguishableTLS,
 			UseIndistinguishableTLS:       meekDialConfig.UseIndistinguishableTLS,
 			TrustedCACertificatesFilename: meekDialConfig.TrustedCACertificatesFilename,
 			TrustedCACertificatesFilename: meekDialConfig.TrustedCACertificatesFilename,
-		})
+		}
+
+		if meekConfig.UseObfuscatedSessionTickets {
+			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
+		}
+
+		dialer := NewCustomTLSDialer(tlsConfig)
 
 
 		// TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip
 		// TODO: wrap in an http.Client and use http.Client.Timeout which actually covers round trip
 		transport = &http.Transport{
 		transport = &http.Transport{

+ 3 - 0
psiphon/server/config.go

@@ -583,6 +583,9 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 	if meekPort == 0 {
 	if meekPort == 0 {
 		meekPort = params.TunnelProtocolPorts["UNFRONTED-MEEK-HTTPS-OSSH"]
 		meekPort = params.TunnelProtocolPorts["UNFRONTED-MEEK-HTTPS-OSSH"]
 	}
 	}
+	if meekPort == 0 {
+		meekPort = params.TunnelProtocolPorts["UNFRONTED-MEEK-SESSION-TICKET-OSSH"]
+	}
 
 
 	// Note: fronting params are a stub; this server entry will exercise
 	// Note: fronting params are a stub; this server entry will exercise
 	// client and server fronting code paths, but not actually traverse
 	// client and server fronting code paths, but not actually traverse

+ 46 - 9
psiphon/server/meek.go

@@ -21,8 +21,10 @@ package server
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"crypto/tls"
+	"crypto/rand"
+	golangtls "crypto/tls"
 	"encoding/base64"
 	"encoding/base64"
+	"encoding/hex"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"io"
 	"io"
@@ -36,6 +38,7 @@ import (
 	"github.com/Psiphon-Inc/crypto/nacl/box"
 	"github.com/Psiphon-Inc/crypto/nacl/box"
 	"github.com/Psiphon-Inc/goarista/monotime"
 	"github.com/Psiphon-Inc/goarista/monotime"
 	"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/tls"
 )
 )
 
 
 // MeekServer is based on meek-server.go from Tor and Psiphon:
 // MeekServer is based on meek-server.go from Tor and Psiphon:
@@ -93,7 +96,7 @@ type MeekServer struct {
 func NewMeekServer(
 func NewMeekServer(
 	support *SupportServices,
 	support *SupportServices,
 	listener net.Listener,
 	listener net.Listener,
-	useTLS bool,
+	useTLS, useObfuscatedSessionTickets bool,
 	clientHandler func(clientConn net.Conn),
 	clientHandler func(clientConn net.Conn),
 	stopBroadcast <-chan struct{}) (*MeekServer, error) {
 	stopBroadcast <-chan struct{}) (*MeekServer, error) {
 
 
@@ -107,7 +110,8 @@ func NewMeekServer(
 	}
 	}
 
 
 	if useTLS {
 	if useTLS {
-		tlsConfig, err := makeMeekTLSConfig(support)
+		tlsConfig, err := makeMeekTLSConfig(
+			support, useObfuscatedSessionTickets)
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
@@ -162,15 +166,14 @@ func (server *MeekServer) Run() error {
 		ConnState:    server.httpConnStateCallback,
 		ConnState:    server.httpConnStateCallback,
 
 
 		// Disable auto HTTP/2 (https://golang.org/doc/go1.6)
 		// Disable auto HTTP/2 (https://golang.org/doc/go1.6)
-		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
+		TLSNextProto: make(map[string]func(*http.Server, *golangtls.Conn, http.Handler)),
 	}
 	}
 
 
 	// Note: Serve() will be interrupted by listener.Close() call
 	// Note: Serve() will be interrupted by listener.Close() call
 	var err error
 	var err error
 	if server.tlsConfig != nil {
 	if server.tlsConfig != nil {
-		httpServer.TLSConfig = server.tlsConfig
 		httpsServer := HTTPSServer{Server: *httpServer}
 		httpsServer := HTTPSServer{Server: *httpServer}
-		err = httpsServer.ServeTLS(server.listener)
+		err = httpsServer.ServeTLS(server.listener, server.tlsConfig)
 	} else {
 	} else {
 		err = httpServer.Serve(server.listener)
 		err = httpServer.Serve(server.listener)
 	}
 	}
@@ -467,7 +470,9 @@ func (session *meekSession) expired() bool {
 // Currently, this config is optimized for fronted meek where the nature
 // Currently, this config is optimized for fronted meek where the nature
 // of the connection is non-circumvention; it's optimized for performance
 // of the connection is non-circumvention; it's optimized for performance
 // assuming the peer is an uncensored CDN.
 // assuming the peer is an uncensored CDN.
-func makeMeekTLSConfig(support *SupportServices) (*tls.Config, error) {
+func makeMeekTLSConfig(
+	support *SupportServices,
+	useObfuscatedSessionTickets bool) (*tls.Config, error) {
 
 
 	certificate, privateKey, err := GenerateWebServerCertificate(
 	certificate, privateKey, err := GenerateWebServerCertificate(
 		support.Config.MeekCertificateCommonName)
 		support.Config.MeekCertificateCommonName)
@@ -481,7 +486,7 @@ func makeMeekTLSConfig(support *SupportServices) (*tls.Config, error) {
 		return nil, common.ContextError(err)
 		return nil, common.ContextError(err)
 	}
 	}
 
 
-	return &tls.Config{
+	config := &tls.Config{
 		Certificates: []tls.Certificate{tlsCertificate},
 		Certificates: []tls.Certificate{tlsCertificate},
 		NextProtos:   []string{"http/1.1"},
 		NextProtos:   []string{"http/1.1"},
 		MinVersion:   tls.VersionTLS10,
 		MinVersion:   tls.VersionTLS10,
@@ -512,7 +517,39 @@ func makeMeekTLSConfig(support *SupportServices) (*tls.Config, error) {
 			tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
 			tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
 		},
 		},
 		PreferServerCipherSuites: true,
 		PreferServerCipherSuites: true,
-	}, nil
+	}
+
+	if useObfuscatedSessionTickets {
+
+		// See obfuscated session ticket overview
+		// in tls.NewObfuscatedClientSessionCache
+
+		var obfuscatedSessionTicketKey [32]byte
+		key, err := hex.DecodeString(support.Config.MeekObfuscatedKey)
+		if err == nil && len(key) != 32 {
+			err = errors.New("invalid obfuscated session key length")
+		}
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+		copy(obfuscatedSessionTicketKey[:], key)
+
+		var standardSessionTicketKey [32]byte
+		_, err = rand.Read(standardSessionTicketKey[:])
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+
+		// Note: SessionTicketKey needs to be set, or else, it appears,
+		// tls.Config.serverInit() will clobber the value set by
+		// SetSessionTicketKeys.
+		config.SessionTicketKey = obfuscatedSessionTicketKey
+		config.SetSessionTicketKeys([][32]byte{
+			standardSessionTicketKey,
+			obfuscatedSessionTicketKey})
+	}
+
+	return config, nil
 }
 }
 
 
 // getMeekCookiePayload extracts the payload from a meek cookie. The cookie
 // getMeekCookiePayload extracts the payload from a meek cookie. The cookie

+ 10 - 4
psiphon/server/net.go

@@ -51,10 +51,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 package server
 package server
 
 
 import (
 import (
-	"crypto/tls"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"time"
 	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tls"
 )
 )
 
 
 // HTTPSServer is a wrapper around http.Server which adds the
 // HTTPSServer is a wrapper around http.Server which adds the
@@ -63,16 +64,21 @@ type HTTPSServer struct {
 	http.Server
 	http.Server
 }
 }
 
 
-// ServeTLS is a offers the equivalent interface as http.Serve.
+// ServeTLS is similar to http.Serve, but uses TLS.
+//
 // The http package has both ListenAndServe and ListenAndServeTLS higher-
 // The http package has both ListenAndServe and ListenAndServeTLS higher-
 // level interfaces, but only Serve (not TLS) offers a lower-level interface that
 // level interfaces, but only Serve (not TLS) offers a lower-level interface that
 // allows the caller to keep a refererence to the Listener, allowing for external
 // allows the caller to keep a refererence to the Listener, allowing for external
 // 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
+// psiphon/common/tls.Config parameter is used intead.
+//
 // tcpKeepAliveListener is used in http.ListenAndServeTLS but not exported,
 // tcpKeepAliveListener is used in http.ListenAndServeTLS but not exported,
 // so we use a copy from https://golang.org/src/net/http/server.go.
 // so we use a copy from https://golang.org/src/net/http/server.go.
-func (server *HTTPSServer) ServeTLS(listener net.Listener) error {
-	tlsListener := tls.NewListener(tcpKeepAliveListener{listener.(*net.TCPListener)}, server.TLSConfig)
+func (server *HTTPSServer) ServeTLS(listener net.Listener, config *tls.Config) error {
+	tlsListener := tls.NewListener(tcpKeepAliveListener{listener.(*net.TCPListener)}, config)
 	return server.Serve(tlsListener)
 	return server.Serve(tlsListener)
 }
 }
 
 

+ 13 - 0
psiphon/server/server_test.go

@@ -118,6 +118,19 @@ func TestUnfrontedMeekHTTPS(t *testing.T) {
 		})
 		})
 }
 }
 
 
+func TestUnfrontedMeekSessionTicket(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			denyTrafficRules:     false,
+			doClientVerification: false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
 func TestWebTransportAPIRequests(t *testing.T) {
 func TestWebTransportAPIRequests(t *testing.T) {
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{

+ 1 - 0
psiphon/server/tunnelServer.go

@@ -324,6 +324,7 @@ func (sshServer *sshServer) runListener(
 			sshServer.support,
 			sshServer.support,
 			listener,
 			listener,
 			protocol.TunnelProtocolUsesMeekHTTPS(tunnelProtocol),
 			protocol.TunnelProtocolUsesMeekHTTPS(tunnelProtocol),
+			protocol.TunnelProtocolUsesObfuscatedSessionTickets(tunnelProtocol),
 			handleClient,
 			handleClient,
 			sshServer.shutdownBroadcast)
 			sshServer.shutdownBroadcast)
 		if err != nil {
 		if err != nil {

+ 4 - 4
psiphon/server/webServer.go

@@ -20,7 +20,7 @@
 package server
 package server
 
 
 import (
 import (
-	"crypto/tls"
+	golangtls "crypto/tls"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
@@ -32,6 +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"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tls"
 )
 )
 
 
 const WEB_SERVER_IO_TIMEOUT = 10 * time.Second
 const WEB_SERVER_IO_TIMEOUT = 10 * time.Second
@@ -92,13 +93,12 @@ func RunWebServer(
 		http.Server{
 		http.Server{
 			MaxHeaderBytes: MAX_API_PARAMS_SIZE,
 			MaxHeaderBytes: MAX_API_PARAMS_SIZE,
 			Handler:        serveMux,
 			Handler:        serveMux,
-			TLSConfig:      tlsConfig,
 			ReadTimeout:    WEB_SERVER_IO_TIMEOUT,
 			ReadTimeout:    WEB_SERVER_IO_TIMEOUT,
 			WriteTimeout:   WEB_SERVER_IO_TIMEOUT,
 			WriteTimeout:   WEB_SERVER_IO_TIMEOUT,
 			ErrorLog:       golanglog.New(logWriter, "", 0),
 			ErrorLog:       golanglog.New(logWriter, "", 0),
 
 
 			// Disable auto HTTP/2 (https://golang.org/doc/go1.6)
 			// Disable auto HTTP/2 (https://golang.org/doc/go1.6)
-			TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
+			TLSNextProto: make(map[string]func(*http.Server, *golangtls.Conn, http.Handler)),
 		},
 		},
 	}
 	}
 
 
@@ -122,7 +122,7 @@ func RunWebServer(
 		defer waitGroup.Done()
 		defer waitGroup.Done()
 
 
 		// Note: will be interrupted by listener.Close()
 		// Note: will be interrupted by listener.Close()
-		err := server.ServeTLS(listener)
+		err := server.ServeTLS(listener, tlsConfig)
 
 
 		// Can't check for the exact error that Close() will cause in Accept(),
 		// Can't check for the exact error that Close() will cause in Accept(),
 		// (see: https://code.google.com/p/go/issues/detail?id=4373). So using an
 		// (see: https://code.google.com/p/go/issues/detail?id=4373). So using an

+ 27 - 1
psiphon/tlsDialer.go

@@ -72,13 +72,14 @@ package psiphon
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
+	"encoding/hex"
 	"errors"
 	"errors"
 	"net"
 	"net"
 	"time"
 	"time"
 
 
 	"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/tls"
 )
 )
 
 
 // CustomTLSConfig contains parameters to determine the behavior
 // CustomTLSConfig contains parameters to determine the behavior
@@ -121,6 +122,10 @@ type CustomTLSConfig struct {
 	// SSL_CTX_load_verify_locations
 	// SSL_CTX_load_verify_locations
 	// Only applies to UseIndistinguishableTLS connections.
 	// Only applies to UseIndistinguishableTLS connections.
 	TrustedCACertificatesFilename string
 	TrustedCACertificatesFilename string
+
+	// ObfuscatedSessionTicketKey enables obfuscated session tickets
+	// using the specified key.
+	ObfuscatedSessionTicketKey string
 }
 }
 
 
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
@@ -190,6 +195,27 @@ func CustomTLSDial(network, addr string, config *CustomTLSConfig) (net.Conn, err
 		tlsConfig.InsecureSkipVerify = true
 		tlsConfig.InsecureSkipVerify = true
 	}
 	}
 
 
+	if config.ObfuscatedSessionTicketKey != "" {
+
+		// TODO: OpenSSL obfuscated session tickets
+
+		// See obfuscated session ticket overview
+		// in tls.NewObfuscatedClientSessionCache
+
+		var obfuscatedSessionTicketKey [32]byte
+		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
+		if err == nil && len(key) != 32 {
+			err = errors.New("invalid obfuscated session key length")
+		}
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+		copy(obfuscatedSessionTicketKey[:], key)
+
+		tlsConfig.ClientSessionCache = tls.NewObfuscatedClientSessionCache(
+			obfuscatedSessionTicketKey)
+	}
+
 	var conn handshakeConn
 	var conn handshakeConn
 
 
 	// When supported, use OpenSSL TLS as a more indistinguishable TLS.
 	// When supported, use OpenSSL TLS as a more indistinguishable TLS.

+ 11 - 1
psiphon/tunnel.go

@@ -458,11 +458,13 @@ func initMeekConfig(
 
 
 	var dialAddress string
 	var dialAddress string
 	useHTTPS := false
 	useHTTPS := false
+	useObfuscatedSessionTickets := false
 	var SNIServerName, hostHeader string
 	var SNIServerName, hostHeader string
 	transformedHostName := false
 	transformedHostName := false
 
 
 	switch selectedProtocol {
 	switch selectedProtocol {
 	case protocol.TUNNEL_PROTOCOL_FRONTED_MEEK:
 	case protocol.TUNNEL_PROTOCOL_FRONTED_MEEK:
+
 		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
 		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
@@ -476,6 +478,7 @@ func initMeekConfig(
 		hostHeader = frontingHost
 		hostHeader = frontingHost
 
 
 	case protocol.TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP:
 	case protocol.TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP:
+
 		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
 		frontingAddress, frontingHost, err := selectFrontingParameters(serverEntry)
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
@@ -484,6 +487,7 @@ func initMeekConfig(
 		hostHeader = frontingHost
 		hostHeader = frontingHost
 
 
 	case protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK:
 	case protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK:
+
 		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
 		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
 		hostname := serverEntry.IpAddress
 		hostname := serverEntry.IpAddress
 		hostname, transformedHostName = config.HostNameTransformer.TransformHostName(hostname)
 		hostname, transformedHostName = config.HostNameTransformer.TransformHostName(hostname)
@@ -493,9 +497,14 @@ func initMeekConfig(
 			hostHeader = fmt.Sprintf("%s:%d", hostname, serverEntry.MeekServerPort)
 			hostHeader = fmt.Sprintf("%s:%d", hostname, serverEntry.MeekServerPort)
 		}
 		}
 
 
-	case protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS:
+	case protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS,
+		protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET:
+
 		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
 		dialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.MeekServerPort)
 		useHTTPS = true
 		useHTTPS = true
+		if selectedProtocol == protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET {
+			useObfuscatedSessionTickets = true
+		}
 		SNIServerName, transformedHostName =
 		SNIServerName, transformedHostName =
 			config.HostNameTransformer.TransformHostName(serverEntry.IpAddress)
 			config.HostNameTransformer.TransformHostName(serverEntry.IpAddress)
 		if serverEntry.MeekServerPort == 443 {
 		if serverEntry.MeekServerPort == 443 {
@@ -517,6 +526,7 @@ func initMeekConfig(
 	return &MeekConfig{
 	return &MeekConfig{
 		DialAddress:                   dialAddress,
 		DialAddress:                   dialAddress,
 		UseHTTPS:                      useHTTPS,
 		UseHTTPS:                      useHTTPS,
+		UseObfuscatedSessionTickets:   useObfuscatedSessionTickets,
 		SNIServerName:                 SNIServerName,
 		SNIServerName:                 SNIServerName,
 		HostHeader:                    hostHeader,
 		HostHeader:                    hostHeader,
 		TransformedHostName:           transformedHostName,
 		TransformedHostName:           transformedHostName,