Преглед на файлове

Add QUIC Client Hello randomization

- Randomization is optional and supports replay.

- This commit makes changes directly in vendored dependencies
  so that the code change is easier to review. Forks and
  vendor updates to follow.
Rod Hynes преди 5 години
родител
ревизия
093b0b37c1

+ 11 - 5
psiphon/common/protocol/protocol.go

@@ -370,11 +370,12 @@ func (labeledProfiles LabeledTLSProfiles) PruneInvalid(customTLSProfiles []strin
 }
 
 const (
-	QUIC_VERSION_GQUIC39      = "gQUICv39"
-	QUIC_VERSION_GQUIC43      = "gQUICv43"
-	QUIC_VERSION_GQUIC44      = "gQUICv44"
-	QUIC_VERSION_OBFUSCATED   = "OBFUSCATED"
-	QUIC_VERSION_IETF_DRAFT29 = "IETF-draft-29"
+	QUIC_VERSION_GQUIC39                 = "gQUICv39"
+	QUIC_VERSION_GQUIC43                 = "gQUICv43"
+	QUIC_VERSION_GQUIC44                 = "gQUICv44"
+	QUIC_VERSION_OBFUSCATED              = "OBFUSCATED"
+	QUIC_VERSION_IETF_DRAFT29            = "IETF-draft-29"
+	QUIC_VERSION_RANDOMIZED_IETF_DRAFT29 = "Randomized-IETF-draft-29"
 )
 
 var SupportedQUICVersions = QUICVersions{
@@ -383,12 +384,17 @@ var SupportedQUICVersions = QUICVersions{
 	QUIC_VERSION_GQUIC44,
 	QUIC_VERSION_OBFUSCATED,
 	QUIC_VERSION_IETF_DRAFT29,
+	QUIC_VERSION_RANDOMIZED_IETF_DRAFT29,
 }
 
 var legacyQUICVersions = QUICVersions{
 	"IETF-draft-24",
 }
 
+func QUICVersionHasRandomizedClientHello(version string) bool {
+	return version == QUIC_VERSION_RANDOMIZED_IETF_DRAFT29
+}
+
 func QUICVersionIsObfuscated(version string) bool {
 	return version == QUIC_VERSION_OBFUSCATED
 }

+ 56 - 32
psiphon/common/quic/quic.go

@@ -80,17 +80,22 @@ func Enabled() bool {
 const ietfQUICDraft29VersionNumber = 0xff00001d
 
 var supportedVersionNumbers = map[string]uint32{
-	protocol.QUIC_VERSION_GQUIC39:      uint32(gquic.VersionGQUIC39),
-	protocol.QUIC_VERSION_GQUIC43:      uint32(gquic.VersionGQUIC43),
-	protocol.QUIC_VERSION_GQUIC44:      uint32(gquic.VersionGQUIC44),
-	protocol.QUIC_VERSION_OBFUSCATED:   uint32(gquic.VersionGQUIC43),
-	protocol.QUIC_VERSION_IETF_DRAFT29: ietfQUICDraft29VersionNumber,
+	protocol.QUIC_VERSION_GQUIC39:                 uint32(gquic.VersionGQUIC39),
+	protocol.QUIC_VERSION_GQUIC43:                 uint32(gquic.VersionGQUIC43),
+	protocol.QUIC_VERSION_GQUIC44:                 uint32(gquic.VersionGQUIC44),
+	protocol.QUIC_VERSION_OBFUSCATED:              uint32(gquic.VersionGQUIC43),
+	protocol.QUIC_VERSION_IETF_DRAFT29:            ietfQUICDraft29VersionNumber,
+	protocol.QUIC_VERSION_RANDOMIZED_IETF_DRAFT29: ietfQUICDraft29VersionNumber,
 }
 
 func isObfuscated(quicVersion string) bool {
 	return quicVersion == protocol.QUIC_VERSION_OBFUSCATED
 }
 
+func isClientHelloRandomized(quicVersion string) bool {
+	return quicVersion == protocol.QUIC_VERSION_RANDOMIZED_IETF_DRAFT29
+}
+
 func isIETFVersion(versionNumber uint32) bool {
 	return versionNumber == ietfQUICDraft29VersionNumber
 }
@@ -190,17 +195,26 @@ func Dial(
 	packetConn net.PacketConn,
 	remoteAddr *net.UDPAddr,
 	quicSNIAddress string,
-	negotiateQUICVersion string,
+	quicVersion string,
+	clientHelloSeed *prng.Seed,
 	obfuscationKey string,
 	obfuscationPaddingSeed *prng.Seed) (net.Conn, error) {
 
-	if negotiateQUICVersion == "" {
+	if quicVersion == "" {
 		return nil, errors.TraceNew("missing version")
 	}
 
-	versionNumber, ok := supportedVersionNumbers[negotiateQUICVersion]
+	if isClientHelloRandomized(quicVersion) && clientHelloSeed == nil {
+		return nil, errors.TraceNew("missing client hello randomization values")
+	}
+
+	if isObfuscated(quicVersion) && (obfuscationPaddingSeed == nil || obfuscationKey == "") {
+		return nil, errors.TraceNew("missing obfuscation values")
+	}
+
+	versionNumber, ok := supportedVersionNumbers[quicVersion]
 	if !ok {
-		return nil, errors.Tracef("unsupported version: %s", negotiateQUICVersion)
+		return nil, errors.Tracef("unsupported version: %s", quicVersion)
 	}
 
 	// Fail if the destination port is invalid. Network operations should fail
@@ -210,13 +224,13 @@ func Dial(
 		return nil, errors.Tracef("invalid destination port: %d", remoteAddr.Port)
 	}
 
-	if isObfuscated(negotiateQUICVersion) {
-		var err error
-		packetConn, err = NewObfuscatedPacketConn(
+	if isObfuscated(quicVersion) {
+		obfuscatedPacketConn, err := NewObfuscatedPacketConn(
 			packetConn, false, obfuscationKey, obfuscationPaddingSeed)
 		if err != nil {
 			return nil, errors.Trace(err)
 		}
+		packetConn = obfuscatedPacketConn
 	}
 
 	session, err := dialQUIC(
@@ -225,6 +239,7 @@ func Dial(
 		remoteAddr,
 		quicSNIAddress,
 		versionNumber,
+		clientHelloSeed,
 		false)
 	if err != nil {
 		packetConn.Close()
@@ -428,11 +443,12 @@ func (conn *Conn) SetWriteDeadline(t time.Time) error {
 // CloseIdleConnections.
 type QUICTransporter struct {
 	quicRoundTripper
-	noticeEmitter        func(string)
-	udpDialer            func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error)
-	quicSNIAddress       string
-	negotiateQUICVersion string
-	packetConn           atomic.Value
+	noticeEmitter   func(string)
+	udpDialer       func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error)
+	quicSNIAddress  string
+	quicVersion     string
+	clientHelloSeed *prng.Seed
+	packetConn      atomic.Value
 
 	mutex sync.Mutex
 	ctx   context.Context
@@ -444,19 +460,29 @@ func NewQUICTransporter(
 	noticeEmitter func(string),
 	udpDialer func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error),
 	quicSNIAddress string,
-	negotiateQUICVersion string) (*QUICTransporter, error) {
+	quicVersion string,
+	clientHelloSeed *prng.Seed) (*QUICTransporter, error) {
+
+	if quicVersion == "" {
+		return nil, errors.TraceNew("missing version")
+	}
 
-	versionNumber, ok := supportedVersionNumbers[negotiateQUICVersion]
+	versionNumber, ok := supportedVersionNumbers[quicVersion]
 	if !ok {
-		return nil, errors.Tracef("unsupported version: %s", negotiateQUICVersion)
+		return nil, errors.Tracef("unsupported version: %s", quicVersion)
+	}
+
+	if isClientHelloRandomized(quicVersion) && clientHelloSeed == nil {
+		return nil, errors.TraceNew("missing client hello randomization values")
 	}
 
 	t := &QUICTransporter{
-		noticeEmitter:        noticeEmitter,
-		udpDialer:            udpDialer,
-		quicSNIAddress:       quicSNIAddress,
-		negotiateQUICVersion: negotiateQUICVersion,
-		ctx:                  ctx,
+		noticeEmitter:   noticeEmitter,
+		udpDialer:       udpDialer,
+		quicSNIAddress:  quicSNIAddress,
+		quicVersion:     quicVersion,
+		clientHelloSeed: clientHelloSeed,
+		ctx:             ctx,
 	}
 
 	if isIETFVersion(versionNumber) {
@@ -522,13 +548,9 @@ func (t *QUICTransporter) dialQUIC() (retSession quicSession, retErr error) {
 		}
 	}()
 
-	if t.negotiateQUICVersion == "" {
-		return nil, errors.TraceNew("missing version")
-	}
-
-	versionNumber, ok := supportedVersionNumbers[t.negotiateQUICVersion]
+	versionNumber, ok := supportedVersionNumbers[t.quicVersion]
 	if !ok {
-		return nil, errors.Tracef("unsupported version: %s", t.negotiateQUICVersion)
+		return nil, errors.Tracef("unsupported version: %s", t.quicVersion)
 	}
 
 	t.mutex.Lock()
@@ -549,6 +571,7 @@ func (t *QUICTransporter) dialQUIC() (retSession quicSession, retErr error) {
 		remoteAddr,
 		t.quicSNIAddress,
 		versionNumber,
+		t.clientHelloSeed,
 		true)
 	if err != nil {
 		packetConn.Close()
@@ -709,16 +732,17 @@ func dialQUIC(
 	remoteAddr *net.UDPAddr,
 	quicSNIAddress string,
 	versionNumber uint32,
+	clientHelloSeed *prng.Seed,
 	dialEarly bool) (quicSession, error) {
 
 	if isIETFVersion(versionNumber) {
-
 		quicConfig := &ietf_quic.Config{
 			HandshakeTimeout: time.Duration(1<<63 - 1),
 			MaxIdleTimeout:   CLIENT_IDLE_TIMEOUT,
 			KeepAlive:        true,
 			Versions: []ietf_quic.VersionNumber{
 				ietf_quic.VersionNumber(versionNumber)},
+			ClientHelloSeed: clientHelloSeed,
 		}
 
 		deadline, ok := ctx.Deadline()

+ 4 - 2
psiphon/common/quic/quic_disabled.go

@@ -49,7 +49,8 @@ func Dial(
 	packetConn net.PacketConn,
 	remoteAddr *net.UDPAddr,
 	quicSNIAddress string,
-	negotiateQUICVersion string,
+	quicVersion string,
+	clientHelloSeed *prng.Seed,
 	obfuscationKey string,
 	obfuscationPaddingSeed *prng.Seed) (net.Conn, error) {
 
@@ -74,7 +75,8 @@ func NewQUICTransporter(
 	noticeEmitter func(string),
 	udpDialer func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error),
 	quicSNIAddress string,
-	negotiateQUICVersion string) (*QUICTransporter, error) {
+	quicVersion string,
+	clientHelloSeed *prng.Seed) (*QUICTransporter, error) {
 
 	return nil, errors.TraceNew("operation is not enabled")
 }

+ 14 - 5
psiphon/common/quic/quic_test.go

@@ -37,14 +37,14 @@ import (
 )
 
 func TestQUIC(t *testing.T) {
-	for negotiateQUICVersion := range supportedVersionNumbers {
-		t.Run(negotiateQUICVersion, func(t *testing.T) {
-			runQUIC(t, negotiateQUICVersion)
+	for quicVersion := range supportedVersionNumbers {
+		t.Run(quicVersion, func(t *testing.T) {
+			runQUIC(t, quicVersion)
 		})
 	}
 }
 
-func runQUIC(t *testing.T, negotiateQUICVersion string) {
+func runQUIC(t *testing.T, quicVersion string) {
 
 	initGoroutines := getGoroutines()
 
@@ -135,12 +135,21 @@ func runQUIC(t *testing.T, negotiateQUICVersion string) {
 				return errors.Trace(err)
 			}
 
+			var clientHelloSeed *prng.Seed
+			if isClientHelloRandomized(quicVersion) {
+				clientHelloSeed, err = prng.NewSeed()
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+
 			conn, err := Dial(
 				ctx,
 				packetConn,
 				remoteAddr,
 				serverAddress,
-				negotiateQUICVersion,
+				quicVersion,
+				clientHelloSeed,
 				obfuscationKey,
 				obfuscationPaddingSeed)
 			if err != nil {

+ 9 - 0
psiphon/dialParameters.go

@@ -111,6 +111,7 @@ type DialParameters struct {
 
 	QUICVersion               string
 	QUICDialSNIAddress        string
+	QUICClientHelloSeed       *prng.Seed
 	ObfuscatedQUICPaddingSeed *prng.Seed
 
 	ConjureDecoyRegistrarWidth int
@@ -492,6 +493,13 @@ func MakeDialParameters(
 
 		isFronted := protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol)
 		dialParams.QUICVersion = selectQUICVersion(isFronted, serverEntry.FrontingProviderID, p)
+
+		if protocol.QUICVersionHasRandomizedClientHello(dialParams.QUICVersion) {
+			dialParams.QUICClientHelloSeed, err = prng.NewSeed()
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
+		}
 	}
 
 	if (!isReplay || !replayObfuscatedQUIC) &&
@@ -705,6 +713,7 @@ func MakeDialParameters(
 			DialAddress:                   dialParams.MeekDialAddress,
 			UseQUIC:                       protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol),
 			QUICVersion:                   dialParams.QUICVersion,
+			QUICClientHelloSeed:           dialParams.QUICClientHelloSeed,
 			UseHTTPS:                      protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol),
 			TLSProfile:                    dialParams.TLSProfile,
 			NoDefaultTLSSessionID:         dialParams.NoDefaultTLSSessionID,

+ 5 - 1
psiphon/meekConn.go

@@ -83,6 +83,9 @@ type MeekConfig struct {
 	// QUICVersion indicates which QUIC version to use.
 	QUICVersion string
 
+	// QUICClientHelloSeed is used for randomized QUIC Client Hellos.
+	QUICClientHelloSeed *prng.Seed
+
 	// UseHTTPS indicates whether to use HTTPS (true) or HTTP (false).
 	// Ignored when UseQUIC is true.
 	UseHTTPS bool
@@ -295,7 +298,8 @@ func DialMeek(
 			},
 			udpDialer,
 			quicDialSNIAddress,
-			meekConfig.QUICVersion)
+			meekConfig.QUICVersion,
+			meekConfig.QUICClientHelloSeed)
 		if err != nil {
 			return nil, errors.Trace(err)
 		}

+ 1 - 0
psiphon/tunnel.go

@@ -725,6 +725,7 @@ func dialTunnel(
 			remoteAddr,
 			dialParams.QUICDialSNIAddress,
 			dialParams.QUICVersion,
+			dialParams.QUICClientHelloSeed,
 			dialParams.ServerEntry.SshObfuscatedKey,
 			dialParams.ObfuscatedQUICPaddingSeed)
 		if err != nil {

+ 3 - 0
vendor/github.com/Psiphon-Labs/quic-go/config.go

@@ -100,5 +100,8 @@ func populateConfig(config *Config) *Config {
 		TokenStore:                            config.TokenStore,
 		QuicTracer:                            config.QuicTracer,
 		Tracer:                                config.Tracer,
+
+		// [Psiphon]
+		ClientHelloSeed: config.ClientHelloSeed,
 	}
 }

+ 5 - 0
vendor/github.com/Psiphon-Labs/quic-go/interface.go

@@ -6,6 +6,7 @@ import (
 	"net"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/quic-go/internal/handshake"
 	"github.com/Psiphon-Labs/quic-go/internal/protocol"
 	"github.com/Psiphon-Labs/quic-go/logging"
@@ -267,6 +268,10 @@ type Config struct {
 	// It is disabled by default. Use the "quictrace" build tag to enable (e.g. go build -tags quictrace).
 	QuicTracer quictrace.Tracer
 	Tracer     logging.Tracer
+
+	// [Psiphon]
+	// ClientHelloSeed is used for TLS Client Hello randomization and replay.
+	ClientHelloSeed *prng.Seed
 }
 
 // A Listener for incoming QUIC connections

+ 24 - 1
vendor/github.com/Psiphon-Labs/quic-go/internal/handshake/crypto_setup.go

@@ -10,6 +10,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/quic-go/internal/protocol"
 	"github.com/Psiphon-Labs/quic-go/internal/qerr"
 	"github.com/Psiphon-Labs/quic-go/internal/qtls"
@@ -157,12 +158,23 @@ func NewCryptoSetupClient(
 	tp *wire.TransportParameters,
 	runner handshakeRunner,
 	tlsConf *tls.Config,
+	clientHelloSeed *prng.Seed,
 	enable0RTT bool,
 	rttStats *utils.RTTStats,
 	tracer logging.ConnectionTracer,
 	logger utils.Logger,
 	version protocol.VersionNumber,
 ) (CryptoSetup, <-chan *wire.TransportParameters /* ClientHello written. Receive nil for non-0-RTT */) {
+
+	// [Psiphon]
+	// Instantiate the PRNG here as it's used in sequence in two places:
+	// TransportParameters.Marshal, for the quic_transport_parameters extension;
+	// and then in qtls.clientHelloMsg.marshal.
+	var clientHelloPRNG *prng.PRNG
+	if clientHelloSeed != nil {
+		clientHelloPRNG = prng.NewPRNGWithSeed(clientHelloSeed)
+	}
+
 	cs, clientHelloWritten := newCryptoSetup(
 		initialStream,
 		handshakeStream,
@@ -175,7 +187,14 @@ func NewCryptoSetupClient(
 		tracer,
 		logger,
 		protocol.PerspectiveClient,
+
+		// [Psiphon]
+		clientHelloPRNG,
 	)
+
+	// [Psiphon]
+	cs.extraConf.ClientHelloPRNG = clientHelloPRNG
+
 	cs.conn = qtls.Client(newConn(localAddr, remoteAddr, version), cs.tlsConf, cs.extraConf)
 	return cs, clientHelloWritten
 }
@@ -208,6 +227,9 @@ func NewCryptoSetupServer(
 		tracer,
 		logger,
 		protocol.PerspectiveServer,
+
+		// [Psiphon]
+		nil,
 	)
 	cs.conn = qtls.Server(newConn(localAddr, remoteAddr, version), cs.tlsConf, cs.extraConf)
 	return cs
@@ -225,13 +247,14 @@ func newCryptoSetup(
 	tracer logging.ConnectionTracer,
 	logger utils.Logger,
 	perspective protocol.Perspective,
+	clientHelloPRNG *prng.PRNG,
 ) (*cryptoSetup, <-chan *wire.TransportParameters /* ClientHello written. Receive nil for non-0-RTT */) {
 	initialSealer, initialOpener := NewInitialAEAD(connID, perspective)
 	if tracer != nil {
 		tracer.UpdatedKeyFromTLS(protocol.EncryptionInitial, protocol.PerspectiveClient)
 		tracer.UpdatedKeyFromTLS(protocol.EncryptionInitial, protocol.PerspectiveServer)
 	}
-	extHandler := newExtensionHandler(tp.Marshal(perspective), perspective)
+	extHandler := newExtensionHandler(tp.Marshal(perspective, clientHelloPRNG), perspective)
 	cs := &cryptoSetup{
 		tlsConf:                   tlsConf,
 		initialStream:             initialStream,

+ 5 - 0
vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/go114.go

@@ -9,6 +9,7 @@ import (
 	"net"
 	"unsafe"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/marten-seemann/qtls"
 )
 
@@ -98,6 +99,10 @@ type ExtraConfig struct {
 	// Is called when the client uses a session ticket.
 	// Restores the application data that was saved earlier on GetAppDataForSessionTicket.
 	SetAppDataFromSessionState func([]byte)
+
+	// [Psiphon]
+	// ClientHelloPRNG is used for Client Hello randomization and replay.
+	ClientHelloPRNG *prng.PRNG
 }
 
 const (

+ 3 - 0
vendor/github.com/Psiphon-Labs/quic-go/internal/qtls/qtls.go

@@ -121,6 +121,9 @@ func tlsConfigToQtlsConfig(c *tls.Config, ec *ExtraConfig) *Config {
 		SetAppDataFromSessionState: ec.SetAppDataFromSessionState,
 		Enable0RTT:                 ec.Enable0RTT,
 		MaxEarlyData:               ec.MaxEarlyData,
+
+		// [Psiphon]
+		ClientHelloPRNG: ec.ClientHelloPRNG,
 	}
 	return conf
 }

+ 112 - 1
vendor/github.com/Psiphon-Labs/quic-go/internal/wire/transport_parameters.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/Psiphon-Labs/quic-go/internal/qerr"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/quic-go/internal/protocol"
 	"github.com/Psiphon-Labs/quic-go/internal/utils"
 )
@@ -312,7 +313,14 @@ func (p *TransportParameters) readNumericTransportParameter(
 }
 
 // Marshal the transport parameters
-func (p *TransportParameters) Marshal(pers protocol.Perspective) []byte {
+func (p *TransportParameters) Marshal(pers protocol.Perspective, clientHelloPRNG *prng.PRNG) []byte {
+
+	// [Psiphon]
+	// Randomize the quic_transport_parameters Client Hello extension.
+	if pers == protocol.PerspectiveClient && clientHelloPRNG != nil {
+		return p.marshalClientRandomized(clientHelloPRNG)
+	}
+
 	b := &bytes.Buffer{}
 
 	// add a greased value
@@ -394,6 +402,109 @@ func (p *TransportParameters) Marshal(pers protocol.Perspective) []byte {
 	return b.Bytes()
 }
 
+// [Psiphon]
+// marshalClientRandomized is a randomized variant of Marshal. The original
+// Marshal is retained as-is to ease future merging.
+func (p *TransportParameters) marshalClientRandomized(clientHelloPRNG *prng.PRNG) []byte {
+
+	b := &bytes.Buffer{}
+
+	marshallers := []func(){
+
+		func() {
+			// add a greased value
+			utils.WriteVarInt(b, uint64(27+31*rand.Intn(100)))
+			length := rand.Intn(16)
+			randomData := make([]byte, length)
+			rand.Read(randomData)
+			utils.WriteVarInt(b, uint64(length))
+			b.Write(randomData)
+		},
+
+		func() {
+			// initial_max_stream_data_bidi_local
+			p.marshalVarintParam(b, initialMaxStreamDataBidiLocalParameterID, uint64(p.InitialMaxStreamDataBidiLocal))
+		},
+
+		func() {
+			// initial_max_stream_data_bidi_remote
+			p.marshalVarintParam(b, initialMaxStreamDataBidiRemoteParameterID, uint64(p.InitialMaxStreamDataBidiRemote))
+		},
+
+		func() {
+			// initial_max_stream_data_uni
+			p.marshalVarintParam(b, initialMaxStreamDataUniParameterID, uint64(p.InitialMaxStreamDataUni))
+		},
+
+		func() {
+			// initial_max_data
+			p.marshalVarintParam(b, initialMaxDataParameterID, uint64(p.InitialMaxData))
+		},
+
+		func() {
+			// initial_max_bidi_streams
+			p.marshalVarintParam(b, initialMaxStreamsBidiParameterID, uint64(p.MaxBidiStreamNum))
+		},
+
+		func() {
+			// initial_max_uni_streams
+			p.marshalVarintParam(b, initialMaxStreamsUniParameterID, uint64(p.MaxUniStreamNum))
+		},
+
+		func() {
+			// idle_timeout
+			p.marshalVarintParam(b, maxIdleTimeoutParameterID, uint64(p.MaxIdleTimeout/time.Millisecond))
+		},
+
+		func() {
+			// max_packet_size
+			p.marshalVarintParam(b, maxUDPPayloadSizeParameterID, uint64(protocol.MaxReceivePacketSize))
+		},
+
+		func() {
+			// max_ack_delay
+			// Only send it if is different from the default value.
+			if p.MaxAckDelay != protocol.DefaultMaxAckDelay {
+				p.marshalVarintParam(b, maxAckDelayParameterID, uint64(p.MaxAckDelay/time.Millisecond))
+			}
+		},
+
+		func() {
+			// ack_delay_exponent
+			// Only send it if is different from the default value.
+			if p.AckDelayExponent != protocol.DefaultAckDelayExponent {
+				p.marshalVarintParam(b, ackDelayExponentParameterID, uint64(p.AckDelayExponent))
+			}
+		},
+
+		func() {
+			// disable_active_migration
+			if p.DisableActiveMigration {
+				utils.WriteVarInt(b, uint64(disableActiveMigrationParameterID))
+				utils.WriteVarInt(b, 0)
+			}
+		},
+
+		func() {
+			// active_connection_id_limit
+			p.marshalVarintParam(b, activeConnectionIDLimitParameterID, p.ActiveConnectionIDLimit)
+		},
+
+		func() {
+			// initial_source_connection_id
+			utils.WriteVarInt(b, uint64(initialSourceConnectionIDParameterID))
+			utils.WriteVarInt(b, uint64(p.InitialSourceConnectionID.Len()))
+			b.Write(p.InitialSourceConnectionID.Bytes())
+		},
+	}
+
+	perm := clientHelloPRNG.Perm(len(marshallers))
+	for _, j := range perm {
+		marshallers[j]()
+	}
+
+	return b.Bytes()
+}
 func (p *TransportParameters) marshalVarintParam(b *bytes.Buffer, id transportParameterID, val uint64) {
 	utils.WriteVarInt(b, uint64(id))
 	utils.WriteVarInt(b, uint64(utils.VarIntLen(val)))

+ 4 - 0
vendor/github.com/Psiphon-Labs/quic-go/session.go

@@ -436,6 +436,10 @@ var newClientSession = func(
 			onHandshakeComplete: func() { close(s.handshakeCompleteChan) },
 		},
 		tlsConf,
+
+		// [Psiphon]
+		conf.ClientHelloSeed,
+
 		enable0RTT,
 		s.rttStats,
 		tracer,

+ 5 - 0
vendor/github.com/marten-seemann/qtls-go1-15/common.go

@@ -24,6 +24,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"golang.org/x/sys/cpu"
 )
 
@@ -743,6 +744,10 @@ type ExtraConfig struct {
 	// Is called when the client uses a session ticket.
 	// Restores the application data that was saved earlier on GetAppDataForSessionTicket.
 	SetAppDataFromSessionState func([]byte)
+
+	// [Psiphon]
+	// ClientHelloPRNG is used for Client Hello randomization and replay.
+	ClientHelloPRNG *prng.PRNG
 }
 
 // Clone clones.

+ 3 - 0
vendor/github.com/marten-seemann/qtls-go1-15/handshake_client.go

@@ -89,6 +89,9 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 		secureRenegotiationSupported: true,
 		alpnProtocols:                config.NextProtos,
 		supportedVersions:            supportedVersions,
+
+		// [Psiphon]
+		PRNG: c.extraConfig.ClientHelloPRNG,
 	}
 
 	if c.handshakes > 0 {

+ 346 - 0
vendor/github.com/marten-seemann/qtls-go1-15/handshake_messages.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"golang.org/x/crypto/cryptobyte"
 )
 
@@ -93,9 +94,19 @@ type clientHelloMsg struct {
 	pskIdentities                    []pskIdentity
 	pskBinders                       [][]byte
 	additionalExtensions             []Extension
+
+	// [Psiphon]
+	PRNG *prng.PRNG
 }
 
 func (m *clientHelloMsg) marshal() []byte {
+
+	// [Psiphon]
+	// Randomize the Client Hello.
+	if m.PRNG != nil {
+		return m.marshalRandomized()
+	}
+
 	if m.raw != nil {
 		return m.raw
 	}
@@ -307,6 +318,341 @@ func (m *clientHelloMsg) marshal() []byte {
 	return m.raw
 }
 
+// [Psiphon]
+//
+// marshalRandomized is a randomized variant of marshal. The original Marshal
+// is retained as-is to ease future merging.
+//
+// The offered cipher suites and algorithms are shuffled and truncated. Longer
+// lists are selected with higher probability. Extensions are shuffled.
+//
+// The quic_transport_parameters extension is marshaled in quic-go and is
+// randomized in TransportParameters.Marshal.
+func (m *clientHelloMsg) marshalRandomized() []byte {
+	if m.raw != nil {
+		return m.raw
+	}
+
+	// Some inputs, such as m.supportedCurves, point to global variables. Copy
+	// all slices before truncating.
+
+	cipherSuites := make([]uint16, len(m.cipherSuites))
+	perm := m.PRNG.Perm(len(m.cipherSuites))
+	for i, j := range perm {
+		cipherSuites[j] = m.cipherSuites[i]
+	}
+	cut := len(cipherSuites)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+	cipherSuites = cipherSuites[:cut]
+
+	compressionMethods := make([]uint8, len(m.compressionMethods))
+	perm = m.PRNG.Perm(len(m.compressionMethods))
+	for i, j := range perm {
+		compressionMethods[j] = m.compressionMethods[i]
+	}
+	cut = len(compressionMethods)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+
+	supportedCurves := make([]CurveID, len(m.supportedCurves))
+	perm = m.PRNG.Perm(len(m.supportedCurves))
+	for i, j := range perm {
+		supportedCurves[j] = m.supportedCurves[i]
+	}
+	cut = len(supportedCurves)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+	supportedCurves = supportedCurves[:cut]
+
+	supportedPoints := make([]uint8, len(m.supportedPoints))
+	perm = m.PRNG.Perm(len(m.supportedPoints))
+	for i, j := range perm {
+		supportedPoints[j] = m.supportedPoints[i]
+	}
+	cut = len(supportedPoints)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+
+	var b cryptobyte.Builder
+	b.AddUint8(typeClientHello)
+	b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+		b.AddUint16(m.vers)
+		addBytesWithLength(b, m.random, 32)
+		b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+			b.AddBytes(m.sessionId)
+		})
+		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+			for _, suite := range cipherSuites {
+				b.AddUint16(suite)
+			}
+		})
+		b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+			b.AddBytes(compressionMethods)
+		})
+
+		// If extensions aren't present, omit them.
+		var extensionsPresent bool
+		bWithoutExtensions := *b
+
+		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+
+			extensionMarshallers := []func(){
+
+				func() {
+					if len(m.serverName) > 0 {
+						// RFC 6066, Section 3
+						b.AddUint16(extensionServerName)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddUint8(0) // name_type = host_name
+								b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+									b.AddBytes([]byte(m.serverName))
+								})
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.ocspStapling {
+						// RFC 4366, Section 3.6
+						b.AddUint16(extensionStatusRequest)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8(1)  // status_type = ocsp
+							b.AddUint16(0) // empty responder_id_list
+							b.AddUint16(0) // empty request_extensions
+						})
+					}
+				},
+
+				func() {
+					if len(supportedCurves) > 0 {
+						// RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7
+						b.AddUint16(extensionSupportedCurves)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, curve := range supportedCurves {
+									b.AddUint16(uint16(curve))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(supportedPoints) > 0 {
+						// RFC 4492, Section 5.1.2
+						b.AddUint16(extensionSupportedPoints)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(supportedPoints)
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.ticketSupported {
+						// RFC 5077, Section 3.2
+						b.AddUint16(extensionSessionTicket)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddBytes(m.sessionTicket)
+						})
+					}
+				},
+
+				func() {
+					if len(m.supportedSignatureAlgorithms) > 0 {
+						// RFC 5246, Section 7.4.1.4.1
+						b.AddUint16(extensionSignatureAlgorithms)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, sigAlgo := range m.supportedSignatureAlgorithms {
+									b.AddUint16(uint16(sigAlgo))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.supportedSignatureAlgorithmsCert) > 0 {
+						// RFC 8446, Section 4.2.3
+						b.AddUint16(extensionSignatureAlgorithmsCert)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, sigAlgo := range m.supportedSignatureAlgorithmsCert {
+									b.AddUint16(uint16(sigAlgo))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.secureRenegotiationSupported {
+						// RFC 5746, Section 3.2
+						b.AddUint16(extensionRenegotiationInfo)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.secureRenegotiation)
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.alpnProtocols) > 0 {
+						// RFC 7301, Section 3.1
+						b.AddUint16(extensionALPN)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, proto := range m.alpnProtocols {
+									b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+										b.AddBytes([]byte(proto))
+									})
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.scts {
+						// RFC 6962, Section 3.3.1
+						b.AddUint16(extensionSCT)
+						b.AddUint16(0) // empty extension_data
+					}
+				},
+
+				func() {
+					if len(m.supportedVersions) > 0 {
+						// RFC 8446, Section 4.2.1
+						b.AddUint16(extensionSupportedVersions)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, vers := range m.supportedVersions {
+									b.AddUint16(vers)
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.cookie) > 0 {
+						// RFC 8446, Section 4.2.2
+						b.AddUint16(extensionCookie)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.cookie)
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.keyShares) > 0 {
+						// RFC 8446, Section 4.2.8
+						b.AddUint16(extensionKeyShare)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, ks := range m.keyShares {
+									b.AddUint16(uint16(ks.group))
+									b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+										b.AddBytes(ks.data)
+									})
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.earlyData {
+						// RFC 8446, Section 4.2.10
+						b.AddUint16(extensionEarlyData)
+						b.AddUint16(0) // empty extension_data
+					}
+				},
+
+				func() {
+					if len(m.pskModes) > 0 {
+						// RFC 8446, Section 4.2.9
+						b.AddUint16(extensionPSKModes)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.pskModes)
+							})
+						})
+					}
+				},
+			}
+
+			for _, ext := range m.additionalExtensions {
+				extensionMarshallers = append(extensionMarshallers,
+
+					func() {
+						b.AddUint16(ext.Type)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddBytes(ext.Data)
+						})
+
+					})
+			}
+
+			perm = m.PRNG.Perm(len(extensionMarshallers))
+			for _, j := range perm {
+				extensionMarshallers[j]()
+			}
+
+			if len(m.pskIdentities) > 0 { // pre_shared_key must be the last extension
+				// RFC 8446, Section 4.2.11
+				b.AddUint16(extensionPreSharedKey)
+				b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+					b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+						for _, psk := range m.pskIdentities {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(psk.label)
+							})
+							b.AddUint32(psk.obfuscatedTicketAge)
+						}
+					})
+					b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+						for _, binder := range m.pskBinders {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(binder)
+							})
+						}
+					})
+				})
+			}
+
+			extensionsPresent = len(b.BytesOrPanic()) > 2
+		})
+
+		if !extensionsPresent {
+			*b = bWithoutExtensions
+		}
+	})
+
+	m.raw = b.BytesOrPanic()
+	return m.raw
+}
+
 // marshalWithoutBinders returns the ClientHello through the
 // PreSharedKeyExtension.identities field, according to RFC 8446, Section
 // 4.2.11.2. Note that m.pskBinders must be set to slices of the correct length.

+ 5 - 0
vendor/github.com/marten-seemann/qtls/common.go

@@ -25,6 +25,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"golang.org/x/sys/cpu"
 )
 
@@ -695,6 +696,10 @@ type Config struct {
 	// Is called when the client uses a session ticket.
 	// Restores the application data that was saved earlier on GetAppDataForSessionTicket.
 	SetAppDataFromSessionState func([]byte)
+
+	// [Psiphon]
+	// ClientHelloPRNG is used for Client Hello randomization and replay.
+	ClientHelloPRNG *prng.PRNG
 }
 
 // A RecordLayer handles encrypting and decrypting of TLS messages.

+ 3 - 0
vendor/github.com/marten-seemann/qtls/handshake_client.go

@@ -78,6 +78,9 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, ecdheParameters, error) {
 		secureRenegotiationSupported: true,
 		alpnProtocols:                config.NextProtos,
 		supportedVersions:            supportedVersions,
+
+		// [Psiphon]
+		PRNG: config.ClientHelloPRNG,
 	}
 
 	if c.handshakes > 0 {

+ 346 - 0
vendor/github.com/marten-seemann/qtls/handshake_messages.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"golang.org/x/crypto/cryptobyte"
 )
 
@@ -93,9 +94,19 @@ type clientHelloMsg struct {
 	pskIdentities                    []pskIdentity
 	pskBinders                       [][]byte
 	additionalExtensions             []Extension
+
+	// [Psiphon]
+	PRNG *prng.PRNG
 }
 
 func (m *clientHelloMsg) marshal() []byte {
+
+	// [Psiphon]
+	// Randomize the Client Hello.
+	if m.PRNG != nil {
+		return m.marshalRandomized()
+	}
+
 	if m.raw != nil {
 		return m.raw
 	}
@@ -307,6 +318,341 @@ func (m *clientHelloMsg) marshal() []byte {
 	return m.raw
 }
 
+// [Psiphon]
+//
+// marshalRandomized is a randomized variant of marshal. The original Marshal
+// is retained as-is to ease future merging.
+//
+// The offered cipher suites and algorithms are shuffled and truncated. Longer
+// lists are selected with higher probability. Extensions are shuffled.
+//
+// The quic_transport_parameters extension is marshaled in quic-go and is
+// randomized in TransportParameters.Marshal.
+func (m *clientHelloMsg) marshalRandomized() []byte {
+	if m.raw != nil {
+		return m.raw
+	}
+
+	// Some inputs, such as m.supportedCurves, point to global variables. Copy
+	// all slices before truncating.
+
+	cipherSuites := make([]uint16, len(m.cipherSuites))
+	perm := m.PRNG.Perm(len(m.cipherSuites))
+	for i, j := range perm {
+		cipherSuites[j] = m.cipherSuites[i]
+	}
+	cut := len(cipherSuites)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+	cipherSuites = cipherSuites[:cut]
+
+	compressionMethods := make([]uint8, len(m.compressionMethods))
+	perm = m.PRNG.Perm(len(m.compressionMethods))
+	for i, j := range perm {
+		compressionMethods[j] = m.compressionMethods[i]
+	}
+	cut = len(compressionMethods)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+
+	supportedCurves := make([]CurveID, len(m.supportedCurves))
+	perm = m.PRNG.Perm(len(m.supportedCurves))
+	for i, j := range perm {
+		supportedCurves[j] = m.supportedCurves[i]
+	}
+	cut = len(supportedCurves)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+	supportedCurves = supportedCurves[:cut]
+
+	supportedPoints := make([]uint8, len(m.supportedPoints))
+	perm = m.PRNG.Perm(len(m.supportedPoints))
+	for i, j := range perm {
+		supportedPoints[j] = m.supportedPoints[i]
+	}
+	cut = len(supportedPoints)
+	for ; cut > 1; cut-- {
+		if !m.PRNG.FlipCoin() {
+			break
+		}
+	}
+
+	var b cryptobyte.Builder
+	b.AddUint8(typeClientHello)
+	b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
+		b.AddUint16(m.vers)
+		addBytesWithLength(b, m.random, 32)
+		b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+			b.AddBytes(m.sessionId)
+		})
+		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+			for _, suite := range cipherSuites {
+				b.AddUint16(suite)
+			}
+		})
+		b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+			b.AddBytes(compressionMethods)
+		})
+
+		// If extensions aren't present, omit them.
+		var extensionsPresent bool
+		bWithoutExtensions := *b
+
+		b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+
+			extensionMarshallers := []func(){
+
+				func() {
+					if len(m.serverName) > 0 {
+						// RFC 6066, Section 3
+						b.AddUint16(extensionServerName)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddUint8(0) // name_type = host_name
+								b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+									b.AddBytes([]byte(m.serverName))
+								})
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.ocspStapling {
+						// RFC 4366, Section 3.6
+						b.AddUint16(extensionStatusRequest)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8(1)  // status_type = ocsp
+							b.AddUint16(0) // empty responder_id_list
+							b.AddUint16(0) // empty request_extensions
+						})
+					}
+				},
+
+				func() {
+					if len(supportedCurves) > 0 {
+						// RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7
+						b.AddUint16(extensionSupportedCurves)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, curve := range supportedCurves {
+									b.AddUint16(uint16(curve))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(supportedPoints) > 0 {
+						// RFC 4492, Section 5.1.2
+						b.AddUint16(extensionSupportedPoints)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(supportedPoints)
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.ticketSupported {
+						// RFC 5077, Section 3.2
+						b.AddUint16(extensionSessionTicket)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddBytes(m.sessionTicket)
+						})
+					}
+				},
+
+				func() {
+					if len(m.supportedSignatureAlgorithms) > 0 {
+						// RFC 5246, Section 7.4.1.4.1
+						b.AddUint16(extensionSignatureAlgorithms)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, sigAlgo := range m.supportedSignatureAlgorithms {
+									b.AddUint16(uint16(sigAlgo))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.supportedSignatureAlgorithmsCert) > 0 {
+						// RFC 8446, Section 4.2.3
+						b.AddUint16(extensionSignatureAlgorithmsCert)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, sigAlgo := range m.supportedSignatureAlgorithmsCert {
+									b.AddUint16(uint16(sigAlgo))
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.secureRenegotiationSupported {
+						// RFC 5746, Section 3.2
+						b.AddUint16(extensionRenegotiationInfo)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.secureRenegotiation)
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.alpnProtocols) > 0 {
+						// RFC 7301, Section 3.1
+						b.AddUint16(extensionALPN)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, proto := range m.alpnProtocols {
+									b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+										b.AddBytes([]byte(proto))
+									})
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.scts {
+						// RFC 6962, Section 3.3.1
+						b.AddUint16(extensionSCT)
+						b.AddUint16(0) // empty extension_data
+					}
+				},
+
+				func() {
+					if len(m.supportedVersions) > 0 {
+						// RFC 8446, Section 4.2.1
+						b.AddUint16(extensionSupportedVersions)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, vers := range m.supportedVersions {
+									b.AddUint16(vers)
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.cookie) > 0 {
+						// RFC 8446, Section 4.2.2
+						b.AddUint16(extensionCookie)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.cookie)
+							})
+						})
+					}
+				},
+
+				func() {
+					if len(m.keyShares) > 0 {
+						// RFC 8446, Section 4.2.8
+						b.AddUint16(extensionKeyShare)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								for _, ks := range m.keyShares {
+									b.AddUint16(uint16(ks.group))
+									b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+										b.AddBytes(ks.data)
+									})
+								}
+							})
+						})
+					}
+				},
+
+				func() {
+					if m.earlyData {
+						// RFC 8446, Section 4.2.10
+						b.AddUint16(extensionEarlyData)
+						b.AddUint16(0) // empty extension_data
+					}
+				},
+
+				func() {
+					if len(m.pskModes) > 0 {
+						// RFC 8446, Section 4.2.9
+						b.AddUint16(extensionPSKModes)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(m.pskModes)
+							})
+						})
+					}
+				},
+			}
+
+			for _, ext := range m.additionalExtensions {
+				extensionMarshallers = append(extensionMarshallers,
+
+					func() {
+						b.AddUint16(ext.Type)
+						b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+							b.AddBytes(ext.Data)
+						})
+
+					})
+			}
+
+			perm = m.PRNG.Perm(len(extensionMarshallers))
+			for _, j := range perm {
+				extensionMarshallers[j]()
+			}
+
+			if len(m.pskIdentities) > 0 { // pre_shared_key must be the last extension
+				// RFC 8446, Section 4.2.11
+				b.AddUint16(extensionPreSharedKey)
+				b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+					b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+						for _, psk := range m.pskIdentities {
+							b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(psk.label)
+							})
+							b.AddUint32(psk.obfuscatedTicketAge)
+						}
+					})
+					b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
+						for _, binder := range m.pskBinders {
+							b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+								b.AddBytes(binder)
+							})
+						}
+					})
+				})
+			}
+
+			extensionsPresent = len(b.BytesOrPanic()) > 2
+		})
+
+		if !extensionsPresent {
+			*b = bWithoutExtensions
+		}
+	})
+
+	m.raw = b.BytesOrPanic()
+	return m.raw
+}
+
 // marshalWithoutBinders returns the ClientHello through the
 // PreSharedKeyExtension.identities field, according to RFC 8446, Section
 // 4.2.11.2. Note that m.pskBinders must be set to slices of the correct length.