From 0b736267bbdf9b70a0883ca17076fc6aa657b89d Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 12 Apr 2019 15:15:49 +0100 Subject: [PATCH] Add ESNI client and server support Implements https://tools.ietf.org/html/draft-ietf-tls-esni-01 Extends the tls.Config API with a ClientESNIKeys structure which must contain a valid key. If this key is not valid, the handshake will fail. A GetServerESNIKeys API is also added which allows the server to dynamically query for an appropriate ESNI key. Add a new 'esnitool' utility to generate ESNIKeys for testing purposes, this uses a short lifetime, a single curve and cipher suite. The test client and server can now be used with these keys. Additionally the test client can securely query the ESNI key from DNS (hardcoded to use 1.1.1.1:853 using DoT for now). --- 13.go | 6 + _dev/.gitignore | 1 + _dev/Makefile | 8 + _dev/esnitool/esnitool.go | 137 +++++++++ _dev/interop_test_runner.py | 47 ++- _dev/tris-localserver/runner.sh | 7 +- _dev/tris-localserver/server.go | 32 +++ _dev/tris-testclient/client.go | 40 ++- _dev/tris-testclient/esni_query.go | 105 +++++++ common.go | 14 + esni.go | 448 +++++++++++++++++++++++++++++ esni_test.go | 61 ++++ handshake_client.go | 17 ++ handshake_messages.go | 37 ++- handshake_server.go | 35 ++- tls_test.go | 2 +- 16 files changed, 982 insertions(+), 15 deletions(-) create mode 100644 _dev/esnitool/esnitool.go create mode 100644 _dev/tris-testclient/esni_query.go create mode 100644 esni.go create mode 100644 esni_test.go diff --git a/13.go b/13.go index e12caa674d..e6c1655cf8 100644 --- a/13.go +++ b/13.go @@ -900,6 +900,12 @@ func (hs *clientHandshakeState) processEncryptedExtensions(ee *encryptedExtensio c.clientProtocol = ee.alpnProtocol c.clientProtocolFallback = false } + if len(hs.esniNonce) != 0 { + // ESNI was requested, it must be present with a valid nonce. + if subtle.ConstantTimeCompare(ee.esniNonce, hs.esniNonce) != 1 { + return c.sendAlert(alertIllegalParameter) + } + } return nil } diff --git a/_dev/.gitignore b/_dev/.gitignore index 23c0b93cd2..65bcef3b52 100644 --- a/_dev/.gitignore +++ b/_dev/.gitignore @@ -4,3 +4,4 @@ /tris-testclient/tris-testclient /caddy/caddy /caddy/echo +/esnitool/esnitool diff --git a/_dev/Makefile b/_dev/Makefile index e36824bb1e..2702c1a58a 100644 --- a/_dev/Makefile +++ b/_dev/Makefile @@ -118,11 +118,13 @@ build-all: \ build-test-tris-client \ build-test-tris-server \ build-test-bogo \ + build-esnitool \ $(addprefix build-test-,$(TARGET_TEST_COMPAT)) # Builds TRIS client build-test-tris-client: $(BUILD_DIR)/$(OS_ARCH)/.ok_$(VER_OS_ARCH) cd $(DEV_DIR)/tris-testclient && \ + GOROOT="$(GOROOT_LOCAL)" $(GO) get -d -v && \ GOROOT="$(GOROOT_LOCAL)" GOOS=linux CGO_ENABLED=0 $(GO) build -v -i . $(DOCKER) build -t tris-testclient $(DEV_DIR)/tris-testclient @@ -132,6 +134,12 @@ build-test-tris-server: $(BUILD_DIR)/$(OS_ARCH)/.ok_$(VER_OS_ARCH) GOROOT="$(GOROOT_LOCAL)" GOOS=linux CGO_ENABLED=0 $(GO) build -v -i . $(DOCKER) build -t tris-localserver $(DEV_DIR)/tris-localserver +# Builds esnitool for ESNI tests +build-esnitool: $(BUILD_DIR)/$(OS_ARCH)/.ok_$(VER_OS_ARCH) + cd $(DEV_DIR)/esnitool && \ + GOROOT="$(GOROOT_LOCAL)" $(GO) get -d -v && \ + GOROOT="$(GOROOT_LOCAL)" $(GO) build -v -i . + # BoringSSL specific stuff build-test-boring: BUILDARG=--build-arg REVISION=$(BORINGSSL_REVISION) diff --git a/_dev/esnitool/esnitool.go b/_dev/esnitool/esnitool.go new file mode 100644 index 0000000000..2fc64ef81d --- /dev/null +++ b/_dev/esnitool/esnitool.go @@ -0,0 +1,137 @@ +// Standalone utility to generate ESNI keys. +// Can be run independently of tris. +package main + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "log" + "time" + + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/curve25519" +) + +// Internal definitions, copied from common.go and esni.go + +type keyShare struct { + group tls.CurveID + data []byte +} + +const esniKeysVersionDraft01 uint16 = 0xff01 + +func addUint64(b *cryptobyte.Builder, v uint64) { + b.AddUint32(uint32(v >> 32)) + b.AddUint32(uint32(v)) +} + +// ESNIKeys structure that is exposed through DNS. +type ESNIKeys struct { + version uint16 + checksum [4]uint8 + // (Draft -03 introduces "public_name" here) + keys []keyShare // 16-bit vector length + cipherSuites []uint16 // 16-bit vector length + paddedLength uint16 + notBefore uint64 + notAfter uint64 + extensions []byte // 16-bit vector length. No extensions are defined in draft -01 +} + +func (k *ESNIKeys) serialize() []byte { + var b cryptobyte.Builder + b.AddUint16(k.version) + b.AddBytes(k.checksum[:]) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, ks := range k.keys { + b.AddUint16(uint16(ks.group)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(ks.data) + }) + } + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cs := range k.cipherSuites { + b.AddUint16(cs) + } + }) + b.AddUint16(k.paddedLength) + addUint64(&b, k.notBefore) + addUint64(&b, k.notAfter) + // No extensions are defined in the initial draft. + b.AddUint16(0) + // Should always succeed as we use simple types only. + return b.BytesOrPanic() +} + +func generateX25519() ([]byte, keyShare) { + var scalar, public [32]byte + if _, err := rand.Read(scalar[:]); err != nil { + panic(err) + } + curve25519.ScalarBaseMult(&public, &scalar) + ks := keyShare{ + group: tls.X25519, + data: public[:], + } + return scalar[:], ks +} + +// Creates a new ESNIKeys structure with a new semi-static key share. +// Returns the private key and a new ESNIKeys structure. +func NewESNIKeys(validity time.Duration) ([]byte, *ESNIKeys) { + serverPrivate, serverKS := generateX25519() + notBefore := time.Now() + notAfter := notBefore.Add(validity) + k := &ESNIKeys{ + version: esniKeysVersionDraft01, + keys: []keyShare{serverKS}, + cipherSuites: []uint16{tls.TLS_AES_128_GCM_SHA256}, + // draft-ietf-tls-esni-01: "If the server supports wildcard names, it SHOULD set this value to 260." + paddedLength: 260, + notBefore: uint64(notBefore.Unix()), + notAfter: uint64(notAfter.Unix()), + } + data := k.serialize() + hash := sha256.New() + hash.Write(data[:2]) // version + hash.Write([]byte{0, 0, 0, 0}) + hash.Write(data[6:]) // fields after checksum + copy(k.checksum[:], hash.Sum(nil)[:4]) + return serverPrivate, k +} + +func main() { + var esniKeysFile, esniPrivateFile string + var validity time.Duration + flag.StringVar(&esniKeysFile, "esni-keys-file", "", "Write base64-encoded ESNI keys to file instead of stdout") + flag.StringVar(&esniPrivateFile, "esni-private-file", "", "Write ESNI private key to file instead of stdout") + flag.DurationVar(&validity, "validity", 24*time.Hour, "Validity period of the keys") + flag.Parse() + + serverPrivate, k := NewESNIKeys(validity) + esniBase64 := base64.StdEncoding.EncodeToString(k.serialize()) + if esniKeysFile == "" { + // draft -01 uses a TXT record instead of a dedicated RR. + fmt.Printf("_esni TXT record: %s\n", esniBase64) + } else { + err := ioutil.WriteFile(esniKeysFile, []byte(esniBase64+"\n"), 0644) + if err != nil { + log.Fatalf("Failed to write %s: %s", esniKeysFile, err) + } + } + if esniPrivateFile == "" { + fmt.Printf("ESNI private key: %x\n", serverPrivate) + } else { + err := ioutil.WriteFile(esniPrivateFile, serverPrivate, 0600) + if err != nil { + log.Fatalf("Failed to write %s: %s", esniPrivateFile, err) + } + } +} diff --git a/_dev/interop_test_runner.py b/_dev/interop_test_runner.py index 56c0f4a70d..bad6630846 100755 --- a/_dev/interop_test_runner.py +++ b/_dev/interop_test_runner.py @@ -1,9 +1,13 @@ #!/usr/bin/env python3 import docker -import unittest +import os import re +import subprocess +import sys +import tempfile import time +import unittest # Regex patterns used for testing @@ -34,17 +38,17 @@ def get_ip(self, server): tris_localserver_container = self.d.containers.get(server) return tris_localserver_container.attrs['NetworkSettings']['IPAddress'] - def run_client(self, image_name, cmd): + def run_client(self, image_name, cmd, volumes=None): ''' Runs client and returns tuple (status_code, logs) ''' - c = self.d.containers.run(image=image_name, detach=True, command=cmd) + c = self.d.containers.run(image=image_name, detach=True, command=cmd, volumes=volumes) res = c.wait() ret = c.logs().decode('utf8') c.remove() return (res['StatusCode'], ret) - def run_server(self, image_name, cmd=None, ports=None, entrypoint=None): + def run_server(self, image_name, cmd=None, ports=None, entrypoint=None, volumes=None): ''' Starts server and returns docker container ''' - c = self.d.containers.run(image=image_name, auto_remove=True, detach=True, command=cmd, ports=ports, entrypoint=entrypoint) + c = self.d.containers.run(image=image_name, auto_remove=True, detach=True, command=cmd, ports=ports, entrypoint=entrypoint, volumes=volumes) # TODO: maybe can be done better? time.sleep(3) return c @@ -97,8 +101,12 @@ class InteropServer(object): def setUpClass(self): self.d = Docker() try: - self.server = self.d.run_server(self.TRIS_SERVER_NAME) + self.tmpdir = self._create_tmpdir() + self._create_esni_keys() + self.server = self.d.run_server(self.TRIS_SERVER_NAME, volumes=self.data_volumes()) except: + if hasattr(self, 'tmpdir'): + self.tmpdir.cleanup() self.d.close() raise @@ -106,6 +114,28 @@ def setUpClass(self): def tearDownClass(self): self.server.kill() self.d.close() + self.tmpdir.cleanup() + + @classmethod + def _create_tmpdir(cls): + basedir = None + if sys.platform == 'darwin': + # Workaround for TMPDIR=/var/folders/.../T/ which is not permitted + # by the default Docker configuration. + basedir = '/tmp' + return tempfile.TemporaryDirectory(dir=basedir) + + @classmethod + def _create_esni_keys(cls): + # Create fresh ESNIKeys that has not expired yet. + esni_priv = os.path.join(cls.tmpdir.name, 'esni.key') + esni_pub = os.path.join(cls.tmpdir.name, 'esni.pub') + esnitool = os.path.join(os.path.dirname(__file__), 'esnitool', 'esnitool') + subprocess.check_call([esnitool, '-esni-keys-file', esni_pub, '-esni-private-file', esni_priv]) + + @classmethod + def data_volumes(cls): + return {cls.tmpdir.name: {'bind': '/testdata', 'mode': 'ro'}} @property def server_ip(self): @@ -313,5 +343,10 @@ def test_server_doesnt_support_SIDH(self): res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=true '+self.server_ip+":7443") self.assertEqual(res[0], 0) + def test_esni(self): + res = self.d.run_client(self.CLIENT_NAME, '-rsa=false -ecdsa=false -esni-keys=/testdata/esni.pub '+self.server_ip+':1443', + volumes=self.data_volumes()) + self.assertEqual(res[0], 0) + if __name__ == '__main__': unittest.main() diff --git a/_dev/tris-localserver/runner.sh b/_dev/tris-localserver/runner.sh index 9b43013da5..6f879a77de 100755 --- a/_dev/tris-localserver/runner.sh +++ b/_dev/tris-localserver/runner.sh @@ -1,6 +1,11 @@ #!/bin/sh -./tris-localserver -b 0.0.0.0:1443 -cert=rsa -rtt0=n 2>&1 & # first port: ECDSA (and no 0-RTT) +esni_args= +if [ -e /testdata/esni.key ]; then + esni_args='-esni-keys=/testdata/esni.pub -esni-private=/testdata/esni.key' +fi + +./tris-localserver -b 0.0.0.0:1443 -cert=rsa -rtt0=n $esni_args 2>&1 & # first port: ECDSA (and no 0-RTT) ./tris-localserver -b 0.0.0.0:2443 -cert=ecdsa -rtt0=a 2>&1 & # second port: RSA (and accept 0-RTT but not offer it) ./tris-localserver -b 0.0.0.0:3443 -cert=ecdsa -rtt0=o 2>&1 & # third port: offer and reject 0-RTT ./tris-localserver -b 0.0.0.0:4443 -cert=ecdsa -rtt0=oa 2>&1 & # fourth port: offer and accept 0-RTT diff --git a/_dev/tris-localserver/server.go b/_dev/tris-localserver/server.go index 4c7733f1e6..8b52249d36 100644 --- a/_dev/tris-localserver/server.go +++ b/_dev/tris-localserver/server.go @@ -3,6 +3,7 @@ package main import ( "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/hex" "errors" "flag" @@ -154,6 +155,8 @@ func main() { arg_confirm := flag.Bool("rtt0ack", false, "0-RTT confirm") arg_clientauth := flag.Bool("cliauth", false, "Performs client authentication (RequireAndVerifyClientCert used)") arg_pq := flag.String("pq", "", "Enable quantum-resistant algorithms [c: Support classical and Quantum-Resistant, q: Enable Quantum-Resistant only]") + arg_esniKeys := flag.String("esni-keys", "", "File with base64-encoded ESNIKeys") + arg_esniPrivate := flag.String("esni-private", "", "Private key file for ESNI") flag.Parse() s.Address = *arg_addr @@ -178,6 +181,35 @@ func main() { enablePQ(s, false) } + var err error + var esniKeys *tls.ESNIKeys + var esniPrivateKey []byte + if *arg_esniPrivate == "" && *arg_esniKeys != "" || + *arg_esniPrivate != "" && *arg_esniKeys == "" { + log.Fatal("Both -esni-keys and -esni-private must be provided.") + } + if *arg_esniPrivate != "" { + esniPrivateKey, err = ioutil.ReadFile(*arg_esniPrivate) + if err != nil { + log.Fatalf("Failed to read ESNI private key: %s", err) + } + } + if *arg_esniKeys != "" { + contents, err := ioutil.ReadFile(*arg_esniKeys) + if err != nil { + log.Fatalf("Failed to read ESNIKeys: %s", err) + } + esniKeysBytes, err := base64.StdEncoding.DecodeString(string(contents)) + if err != nil { + log.Fatal("Bad -esni-keys: %s", err) + } + esniKeys, err = tls.ParseESNIKeys(esniKeysBytes) + if esniKeys == nil { + log.Fatalf("Cannot parse ESNIKeys: %s", err) + } + s.TLS.GetServerESNIKeys = func([]byte) (*tls.ESNIKeys, []byte, error) { return esniKeys, esniPrivateKey, nil } + } + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { tlsConn := r.Context().Value(http.TLSConnContextKey).(*tls.Conn) diff --git a/_dev/tris-testclient/client.go b/_dev/tris-testclient/client.go index 71609b3edd..29277bacb8 100644 --- a/_dev/tris-testclient/client.go +++ b/_dev/tris-testclient/client.go @@ -3,11 +3,14 @@ package main import ( "crypto/tls" "crypto/x509" + "encoding/base64" "errors" "flag" "fmt" "io" + "io/ioutil" "log" + "net" "os" "strings" ) @@ -55,6 +58,8 @@ type Client struct { func NewClient() *Client { var c Client c.TLS.InsecureSkipVerify = true + // ESNI support requires a server name to be set. + c.TLS.ServerName = "localhost" return &c } @@ -112,7 +117,8 @@ func result() { // Usage client args host:port func main() { var keylog_file, tls_version, named_groups, named_ciphers string - var enable_rsa, enable_ecdsa, client_auth bool + var enable_rsa, enable_ecdsa, client_auth, enable_esni bool + var esniKeys string flag.StringVar(&keylog_file, "keylogfile", "", "Secrets will be logged here") flag.BoolVar(&enable_rsa, "rsa", true, "Whether to enable RSA cipher suites") @@ -121,6 +127,8 @@ func main() { flag.StringVar(&tls_version, "tls_version", "1.3", "TLS version to use") flag.StringVar(&named_groups, "groups", "X25519:P-256:P-384:P-521", "NamedGroups IDs to use") flag.StringVar(&named_ciphers, "ciphers", "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", "Named cipher IDs to use") + flag.BoolVar(&enable_esni, "esni", false, "Whether to enable ESNI (using DNS if -esni-keys is not provided)") + flag.StringVar(&esniKeys, "esni-keys", "", "Enable ESNI, using the base64-encoded ESNIKeys from this file instead of DNS") flag.Parse() if flag.NArg() != 1 { flag.Usage() @@ -132,6 +140,10 @@ func main() { if !strings.Contains(client.addr, ":") { client.addr += ":443" } + host, _, err := net.SplitHostPort(client.addr) + if err != nil { + log.Fatalf("Cannot parse address: %s", err) + } if keylog_file == "" { keylog_file = os.Getenv("SSLKEYLOGFILE") @@ -159,6 +171,32 @@ func main() { } } + var esniKeysBytes []byte + if len(esniKeys) != 0 { + contents, err := ioutil.ReadFile(esniKeys) + if err != nil { + log.Fatalf("Failed to read ESNIKeys: %s", err) + } + esniKeysBytes, err = base64.StdEncoding.DecodeString(string(contents)) + if err != nil { + log.Fatalf("Failed to parse -esni-keys: %s", err) + } + enable_esni = true + } else if enable_esni { + esniKeysBytes, err = QueryESNIKeysForHost(host) + // Note: on error the spec suggests to continue with cleartext + // ESNI, but for testing purposes we will treat it as fatal. + if err != nil { + log.Fatalf("Failed to retrieve ESNI for host: %s", err) + } + } + if enable_esni { + client.TLS.ClientESNIKeys, err = tls.ParseESNIKeys(esniKeysBytes) + if client.TLS.ClientESNIKeys == nil { + log.Fatalf("Failed to process ESNI response for host: %s", err) + } + } + if enable_rsa { // Sanity check: TLS 1.2 with the mandatory cipher suite from RFC 5246 c := client.clone() diff --git a/_dev/tris-testclient/esni_query.go b/_dev/tris-testclient/esni_query.go new file mode 100644 index 0000000000..aa794ac1cd --- /dev/null +++ b/_dev/tris-testclient/esni_query.go @@ -0,0 +1,105 @@ +package main + +import ( + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "strings" + + "golang.org/x/net/dns/dnsmessage" +) + +func makeDoTQuery(dnsName string) ([]byte, error) { + query := dnsmessage.Message{ + Header: dnsmessage.Header{ + RecursionDesired: true, + }, + Questions: []dnsmessage.Question{ + { + Name: dnsmessage.MustNewName(dnsName), + Type: dnsmessage.TypeTXT, + Class: dnsmessage.ClassINET, + }, + }, + } + req, err := query.Pack() + if err != nil { + return nil, err + } + l := len(req) + req = append([]byte{ + uint8(l >> 8), + uint8(l), + }, req...) + return req, nil +} + +func parseTXTResponse(buf []byte, wantName string) (string, error) { + var p dnsmessage.Parser + hdr, err := p.Start(buf) + if err != nil { + return "", err + } + if hdr.RCode != dnsmessage.RCodeSuccess { + return "", fmt.Errorf("DNS query failed, rcode=%s", hdr.RCode) + } + if err := p.SkipAllQuestions(); err != nil { + return "", err + } + for { + h, err := p.AnswerHeader() + if err == dnsmessage.ErrSectionDone { + break + } + if err != nil { + return "", err + } + if h.Type != dnsmessage.TypeTXT || h.Class != dnsmessage.ClassINET { + continue + } + if !strings.EqualFold(h.Name.String(), wantName) { + if err := p.SkipAnswer(); err != nil { + return "", err + } + } + r, err := p.TXTResource() + if err != nil { + return "", err + } + return r.TXT[0], nil + } + return "", errors.New("No TXT record found") +} + +func QueryESNIKeysForHost(hostname string) ([]byte, error) { + esniDnsName := "_esni." + hostname + "." + query, err := makeDoTQuery(esniDnsName) + if err != nil { + return nil, fmt.Errorf("Building DNS query failed: %s", err) + } + + c, err := tls.Dial("tcp", "1.1.1.1:853", &tls.Config{}) + if err != nil { + return nil, err + } + defer c.Close() + + // Send DNS query + n, err := c.Write(query) + if err != nil || n != len(query) { + return nil, fmt.Errorf("Failed to write query: %s", err) + } + + // Read DNS response + buf := make([]byte, 4096) + n, err = c.Read(buf) + if n < 2 && err != nil { + return nil, fmt.Errorf("Cannot read response: %s", err) + } + txt, err := parseTXTResponse(buf[2:n], esniDnsName) + if err != nil { + return nil, fmt.Errorf("Cannot process TXT record: %s", err) + } + return base64.StdEncoding.DecodeString(txt) +} diff --git a/common.go b/common.go index 46741ad10a..c72a373917 100644 --- a/common.go +++ b/common.go @@ -641,6 +641,18 @@ type Config struct { // This value has no meaning for the client. GetDelegatedCredential func(*ClientHelloInfo, uint16) ([]byte, crypto.PrivateKey, error) + // ClientESNIKeys enables SNI encryption by the client using the + // provided server keys, it has no effect on a server. If the value is + // nil, then SNI encryption is not enabled. + // + // See https://tools.ietf.org/html/draft-ietf-tls-esni-01 + ClientESNIKeys *ESNIKeys + + // GetServerESNIKeys should return the ESNIKeys structure identified by + // the given recordDigest and the corresponding private key share or + // return an error if unknown. + GetServerESNIKeys func(recordDigest []byte) (*ESNIKeys, []byte, error) + serverInitOnce sync.Once // guards calling (*Config).serverInit // mutex protects sessionTicketKeys. @@ -723,6 +735,8 @@ func (c *Config) Clone() *Config { SessionTicketSealer: c.SessionTicketSealer, AcceptDelegatedCredential: c.AcceptDelegatedCredential, GetDelegatedCredential: c.GetDelegatedCredential, + ClientESNIKeys: c.ClientESNIKeys, + GetServerESNIKeys: c.GetServerESNIKeys, sessionTicketKeys: sessionTicketKeys, UseExtendedMasterSecret: c.UseExtendedMasterSecret, } diff --git a/esni.go b/esni.go new file mode 100644 index 0000000000..851a135629 --- /dev/null +++ b/esni.go @@ -0,0 +1,448 @@ +package tls + +import ( + "bytes" + "crypto" + "crypto/cipher" + "crypto/sha256" + "crypto/subtle" + "errors" + "fmt" + "io" + "strings" + "time" + + "golang.org/x/crypto/cryptobyte" +) + +// https://tools.ietf.org/html/draft-ietf-tls-esni-01 +const esniKeysVersionDraft01 uint16 = 0xff01 + +const extensionEncryptedServerName uint16 = 0xffce + +const esniNonceLength = 16 + +// ESNIKeys structure that is exposed through DNS. +type ESNIKeys struct { + version uint16 + checksum [4]uint8 + // (Draft -03 introduces "public_name" here) + keys []keyShare // 16-bit vector length + cipherSuites []uint16 // 16-bit vector length + paddedLength uint16 + notBefore uint64 + notAfter uint64 + extensions []byte // 16-bit vector length. No extensions are defined in draft -01 +} + +// Like cryptobyte.ReadUint16LengthPrefixed, but accepts a []byte output. +func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool { + return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out)) +} + +func readUint64(s *cryptobyte.String, out *uint64) bool { + var high, low uint32 + if !s.ReadUint32(&high) || !s.ReadUint32(&low) { + return false + } + *out = uint64(high)<<32 | uint64(low) + return true +} + +func addUint64(b *cryptobyte.Builder, v uint64) { + b.AddUint32(uint32(v >> 32)) + b.AddUint32(uint32(v)) +} + +// Parses the raw ESNIKeys structure (not base64-encoded). If obtained from DNS, +// then it must have used a secure transport (DoH, DoT). +// Returns a ESNIKeys structure if parsing was successful and nil otherwise +// (unknown version, bad encoding, invalid checksum, etc.) +func ParseESNIKeys(data []byte) (*ESNIKeys, error) { + k := &ESNIKeys{} + input := cryptobyte.String(data) + var keyShares, cipherSuites, extensions cryptobyte.String + if !input.ReadUint16(&k.version) || + k.version != esniKeysVersionDraft01 { + return nil, errors.New("Invalid version") + } + if !input.CopyBytes(k.checksum[:]) { + return nil, errors.New("Invalid format") + } + + // Verify checksum: SHA256(ESNIKeys)[:4] with checksum = 0 + hash := sha256.New() + hash.Write(data[:2]) // version + hash.Write([]byte{0, 0, 0, 0}) + hash.Write(data[6:]) // fields after checksum + actualChecksum := hash.Sum(nil)[:4] + if subtle.ConstantTimeCompare(k.checksum[:], actualChecksum) != 1 { + return nil, errors.New("Bad checksum") + } + + if !input.ReadUint16LengthPrefixed(&keyShares) || + len(keyShares) == 0 || + !input.ReadUint16LengthPrefixed(&cipherSuites) || + len(cipherSuites) == 0 || + !input.ReadUint16(&k.paddedLength) || + !readUint64(&input, &k.notBefore) || + !readUint64(&input, &k.notAfter) || + !input.ReadUint16LengthPrefixed(&extensions) || + !input.Empty() { + return nil, errors.New("Invalid format") + } + + for !keyShares.Empty() { + var ks keyShare + if !keyShares.ReadUint16((*uint16)(&ks.group)) || + !readUint16LengthPrefixed(&keyShares, &ks.data) || + len(ks.data) == 0 { + return nil, errors.New("Invalid format") + } + k.keys = append(k.keys, ks) + } + for !cipherSuites.Empty() { + var cipherSuite uint16 + if !cipherSuites.ReadUint16(&cipherSuite) { + return nil, errors.New("Invalid format") + } + k.cipherSuites = append(k.cipherSuites, cipherSuite) + } + // Draft -01 does not have any extensions, fail if there are any. + if !extensions.Empty() { + return nil, errors.New("Extensions are not supported") + } + return k, nil +} + +func (k *ESNIKeys) serialize() []byte { + var b cryptobyte.Builder + b.AddUint16(k.version) + b.AddBytes(k.checksum[:]) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, ks := range k.keys { + b.AddUint16(uint16(ks.group)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(ks.data) + }) + } + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cs := range k.cipherSuites { + b.AddUint16(cs) + } + }) + b.AddUint16(k.paddedLength) + addUint64(&b, k.notBefore) + addUint64(&b, k.notAfter) + // No extensions are defined in the initial draft. + b.AddUint16(0) + // Should always succeed as we use simple types only. + return b.BytesOrPanic() +} + +// Returns true if the client can still use this key. +func (k *ESNIKeys) isValid(now time.Time) bool { + nowUnix := uint64(now.Unix()) + return k.notBefore <= nowUnix && nowUnix <= k.notAfter +} + +// Computes a record digest for the given hash algorithm. +func (k *ESNIKeys) recordDigest(hash crypto.Hash) []byte { + h := hash.New() + h.Write(k.serialize()) + return h.Sum(nil) +} + +func (k *ESNIKeys) createPaddedServerNameList(serverName string) ([]byte, error) { + if len(serverName) == 0 { + return nil, errors.New("ServerName must be set") + } + // https://tools.ietf.org/html/rfc6066#section-3 + // + // struct { + // NameType name_type; + // select (name_type) { + // case host_name: HostName; + // } name; + // } ServerName; + // + // enum { + // host_name(0), (255) + // } NameType; + // + // opaque HostName<1..2^16-1>; + // + // struct { + // ServerName server_name_list<1..2^16-1> + // } ServerNameList; + // TODO use NewFixedBuilder when CL 148882 is imported. + b := cryptobyte.NewBuilder(make([]byte, 0, k.paddedLength)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddUint8(0) // NameType: host_name + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte(serverName)) + }) + }) + serverNameList, err := b.Bytes() + if err != nil || len(serverNameList) > int(k.paddedLength) { + // The client MUST NOT use ESNI when the name length is too long + return nil, errors.New("ServerName is too long") + } + // Append zeroes as padding (if any). + serverNameList = serverNameList[:k.paddedLength] + return serverNameList, nil +} + +type clientEncryptedSNI struct { + suite uint16 // CipherSuite + keyShare keyShare // KeyShareEntry + recordDigest []byte + encryptedSni []byte +} + +// pickCipherSuite selects a supported cipher suite or returns nil if no mutual +// cipher suite is supported. +func (k *ESNIKeys) pickCipherSuite() *cipherSuite { + for _, availCipher := range k.cipherSuites { + for _, supportedCipher := range cipherSuites { + if supportedCipher.flags&suiteTLS13 == 0 { + continue + } + if supportedCipher.id == availCipher { + return supportedCipher + } + } + } + return nil +} + +func (k *ESNIKeys) pickKeyShare() keyShare { + for _, availKeyShare := range k.keys { + switch availKeyShare.group { + case CurveP256, CurveP384, CurveP521, X25519: + return availKeyShare + } + } + return keyShare{} +} + +func pickEsniKex(curveId CurveID) kex { + switch curveId { + case CurveP256: + return &kexNIST{} + case CurveP384: + return &kexNIST{} + case CurveP521: + return &kexNIST{} + case X25519: + return &kexX25519{} + default: + return nil + } +} + +func serializeKeyShares(keyShares []keyShare) []byte { + var b cryptobyte.Builder + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, ks := range keyShares { + b.AddUint16(uint16(ks.group)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(ks.data) + }) + } + }) + return b.BytesOrPanic() +} + +func (k *ESNIKeys) makeClientHelloExtension(rand io.Reader, serverName string, clientHelloRandom []byte, clientHelloKeyShares []keyShare) ([]byte, *clientEncryptedSNI, error) { + suite := k.pickCipherSuite() + if suite == nil { + return nil, nil, errors.New("Unsupported cipher suite") + } + serverKS := k.pickKeyShare() + if serverKS.group == 0 { + return nil, nil, errors.New("Unsupported key shares") + } + + // Prepare plaintext ESNI contents. + paddedSNI, err := k.createPaddedServerNameList(serverName) + if paddedSNI == nil { + // Name is empty or too long. + return nil, nil, err + } + innerESNI := make([]byte, esniNonceLength+len(paddedSNI)) + esniNonce := innerESNI[:esniNonceLength] + if _, err := io.ReadFull(rand, esniNonce); err != nil { + return nil, nil, err + } + copy(innerESNI[esniNonceLength:], paddedSNI) + + // Derive key using a new ephemeral key and the semi-static key provided + // by the server. + kex := pickEsniKex(serverKS.group) + if kex == nil { + return nil, nil, errors.New("Unsupported curve") + } + dhSharedSecret, clientKS, err := kex.keyAgreementServer(rand, serverKS) + if err != nil { + return nil, nil, err + } + recordDigest := k.recordDigest(hashForSuite(suite)) + aead := k.aeadForESNI(suite, recordDigest, clientHelloRandom, clientKS, dhSharedSecret) + aad := serializeKeyShares(clientHelloKeyShares) + // A fixed nonce was provided before, do not provide XOR mask since it + // is used only once. + esni := aead.Seal(nil, nil, innerESNI, aad) + + clientESNI := &clientEncryptedSNI{ + suite: suite.id, + keyShare: clientKS, + recordDigest: recordDigest, + encryptedSni: esni, + } + return esniNonce, clientESNI, nil +} + +func (clientESNI *clientEncryptedSNI) marshal() []byte { + var b cryptobyte.Builder + b.AddUint16(clientESNI.suite) + b.AddUint16(uint16(clientESNI.keyShare.group)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(clientESNI.keyShare.data) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(clientESNI.recordDigest) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(clientESNI.encryptedSni) + }) + return b.BytesOrPanic() +} + +func (clientESNI *clientEncryptedSNI) unmarshal(data []byte) bool { + input := cryptobyte.String(data) + if !input.ReadUint16((*uint16)(&clientESNI.suite)) || + !input.ReadUint16((*uint16)(&clientESNI.keyShare.group)) || + !readUint16LengthPrefixed(&input, &clientESNI.keyShare.data) || + !readUint16LengthPrefixed(&input, &clientESNI.recordDigest) || + !readUint16LengthPrefixed(&input, &clientESNI.encryptedSni) || + !input.Empty() { + return false + } + return true +} + +// processClientESNIForServer processes the ESNI extension sent by the client. +// On success, it returns the nonce, decrypted server name, alertSuccess and no +// error. On failure, it will return the alert code and error description. +func (clientESNI *clientEncryptedSNI) processClientESNIForServer(config *Config, clientHelloRandom []byte, clientHelloKeyShares []keyShare) ([]byte, string, alert, error) { + if config.GetServerESNIKeys == nil { + return nil, "", alertIllegalParameter, fmt.Errorf("ESNI support is not enabled") + } + // TODO check validity of esniKeys or document that GetServerESNIKeys should check this + esniKeys, serverPrivateKey, err := config.GetServerESNIKeys(clientESNI.recordDigest) + if err != nil { + return nil, "", alertIllegalParameter, fmt.Errorf("tls: unable to find ESNIKeys for server: %s", err) + } + if esniKeys == nil || len(serverPrivateKey) == 0 { + return nil, "", alertInternalError, errors.New("tls: missing ESNIKeys and private key") + } + suite := mutualCipherSuite(esniKeys.cipherSuites, clientESNI.suite) + if suite == nil { + return nil, "", alertIllegalParameter, fmt.Errorf("tls: forbidden cipher suite for ESNI: %#x", clientESNI.suite) + } + clientKS := clientESNI.keyShare + kex := pickEsniKex(clientKS.group) + if kex == nil { + return nil, "", alertIllegalParameter, fmt.Errorf("tls: forbidden key share for ESNI: %#x", clientESNI.keyShare.group) + } + // Sanity check the public ESNIKeys value from GetServerESNIKeys against + // those included in the Client Hello. + recordDigest := esniKeys.recordDigest(hashForSuite(suite)) + if !bytes.Equal(recordDigest, clientESNI.recordDigest) { + return nil, "", alertInternalError, fmt.Errorf("tls: GetServerESNIKeys keys do not match the expected record_digest") + } + + // Compute secrets and decrypt ESNI. keyAgreementClient is used since + // we (the server) were the first to provide a semi-static ESNI key to + // the peer. + dhSharedSecret, err := kex.keyAgreementClient(clientKS, serverPrivateKey) + if err != nil { + return nil, "", alertInternalError, err + } + aead := esniKeys.aeadForESNI(suite, recordDigest, clientHelloRandom, clientKS, dhSharedSecret) + aad := serializeKeyShares(clientHelloKeyShares) + // A fixed nonce was provided before, do not provide XOR mask since it + // is used only once. + innerESNI, err := aead.Open(nil, nil, clientESNI.encryptedSni, aad) + if err != nil { + return nil, "", alertDecryptError, fmt.Errorf("tls: decryption error in ESNI: %s", err) + } + if len(innerESNI) != esniNonceLength+int(esniKeys.paddedLength) { + return nil, "", alertIllegalParameter, errors.New("tls: bad ESNI padded length") + } + + esniNonce := innerESNI[:esniNonceLength] + serverName, ok := parseRealSNI(innerESNI[esniNonceLength:]) + if !ok { + return nil, "", alertIllegalParameter, errors.New("tls: bad ESNI name") + } + return esniNonce, serverName, alertSuccess, nil +} + +func parseRealSNI(data []byte) (string, bool) { + input := cryptobyte.String(data) + var serverNameList cryptobyte.String + if !input.ReadUint16LengthPrefixed(&serverNameList) { + return "", false + } + // TODO padding check + + var serverName string + for !serverNameList.Empty() { + var nameType uint8 + var name []byte + if !serverNameList.ReadUint8(&nameType) || + !readUint16LengthPrefixed(&serverNameList, &name) { + return "", false + } + if nameType == 0 { + serverName = string(name) + } + } + if serverName == "" { + return "", false + } + // An SNI value may not include a trailing dot. See + // https://tools.ietf.org/html/rfc6066#section-3 + if strings.HasSuffix(serverName, ".") { + return "", false + } + return serverName, true +} + +// Returns an AEAD for one encryption/decryption operation. +func (k *ESNIKeys) aeadForESNI(suite *cipherSuite, recordDigest, clientHelloRandom []byte, clientKS keyShare, dhSharedSecret []byte) cipher.AEAD { + hash := hashForSuite(suite) + hashOfESNIContents := hash.New() + hashOfESNIContents.Write([]byte{ + byte(len(recordDigest) >> 8), + byte(len(recordDigest)), + }) + hashOfESNIContents.Write(recordDigest) + hashOfESNIContents.Write([]byte{ + byte(clientKS.group >> 8), + byte(clientKS.group), + byte(len(clientKS.data) >> 8), + byte(len(clientKS.data)), + }) + hashOfESNIContents.Write(clientKS.data) + hashOfESNIContents.Write(clientHelloRandom[:]) + contextHash := hashOfESNIContents.Sum(nil) + + zx := hkdfExtract(hash, dhSharedSecret, nil) + key := hkdfExpandLabel(hash, zx, contextHash, "esni key", suite.keyLen) + iv := hkdfExpandLabel(hash, zx, contextHash, "esni iv", suite.ivLen) + return suite.aead(key, iv) +} diff --git a/esni_test.go b/esni_test.go new file mode 100644 index 0000000000..51f2bb1966 --- /dev/null +++ b/esni_test.go @@ -0,0 +1,61 @@ +package tls + +import ( + "encoding/base64" + "fmt" + "testing" +) + +// dig _esni.cloudflare.com TXT +short +const esniTxtRecord = "/wE7+NS+ACQAHQAgxTKiPqR+6KrU1WMBf21UbzibTZ3kOW2wV8aSohULfzQAAhMBAQQAAAAAXKm8EAAAAABcsaUQAAA=" + +var esniKeysData []byte + +func init() { + d, err := base64.StdEncoding.DecodeString(esniTxtRecord) + if err != nil { + panic(fmt.Sprintf("Bad base64-encoded ESNI record: %s", err)) + } + esniKeysData = d +} + +func TestParseESNIKeysCorruptChecksum(t *testing.T) { + d := make([]byte, len(esniKeysData)) + copy(d, esniKeysData) + d[2] ^= 0xff // corrupt checksum + k, err := ParseESNIKeys(d) + if k != nil { + t.Error("Bad checksum, expected failure!") + } + if err.Error() != "Bad checksum" { + t.Errorf("Expected checksum error, got: %s", err) + } +} + +func TestParseESNIKeys(t *testing.T) { + k, err := ParseESNIKeys(esniKeysData) + if k == nil || err != nil { + t.Errorf("Unable to parse ESNI record: %s", err) + } + if k.version != 0xff01 { + t.Errorf("Unexpected version: %#04x", k.version) + } + if len(k.keys) != 1 || k.keys[0].group != X25519 || len(k.keys[0].data) != 32 { + t.Errorf("Unexpected keyShare: %v", k.keys) + } + if len(k.cipherSuites) != 1 || k.cipherSuites[0] != TLS_AES_128_GCM_SHA256 { + t.Errorf("Unexpected cipher suites: %v", k.cipherSuites) + } + if k.paddedLength != 260 { + t.Errorf("Unexpected paddedLength: %d", k.paddedLength) + } + if k.notBefore != 1554627600 { + t.Errorf("Unexpected notBefore: %d", k.notBefore) + } + if k.notAfter != 1555146000 { + t.Errorf("Unexpected notAfter: %d", k.notAfter) + } + if len(k.extensions) != 0 { + t.Errorf("Unexpected extensions: %v", k.extensions) + } +} diff --git a/handshake_client.go b/handshake_client.go index e8f377dc27..2732863307 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -34,6 +34,7 @@ type clientHandshakeState struct { // TLS 1.3 fields keySchedule *keySchedule13 privateKey []byte + esniNonce []byte } func makeClientHello(config *Config) (*clientHelloMsg, error) { @@ -205,6 +206,22 @@ func (c *Conn) clientHandshake() error { if _, err := io.ReadFull(c.config.rand(), hello.sessionId); err != nil { return errors.New("tls: short read from Rand: " + err.Error()) } + + if c.config.ClientESNIKeys != nil { + esniKeys := c.config.ClientESNIKeys + if !esniKeys.isValid(c.config.time()) { + return errors.New("tls: ClientESNIKeys has expired") + } + esniNonce, esniExt, err := esniKeys.makeClientHelloExtension(c.config.rand(), hello.serverName, hello.random, hello.keyShares) + if esniExt == nil { + // Bad serverName, etc. + return fmt.Errorf("tls: invalid ESNI configuration: %s", err) + } + // Replace SNI with ESNI, omitting "server_name". + hello.encryptedServerName = esniExt.marshal() + hello.serverName = "" + hs.esniNonce = esniNonce + } } if err = hs.handshake(); err != nil { diff --git a/handshake_messages.go b/handshake_messages.go index 91f0ca8cda..1582e8192f 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -33,6 +33,7 @@ type clientHelloMsg struct { compressionMethods []uint8 nextProtoNeg bool serverName string + encryptedServerName []byte ocspStapling bool scts bool supportedCurves []CurveID @@ -116,6 +117,7 @@ func (m *clientHelloMsg) equal(i interface{}) bool { bytes.Equal(m.compressionMethods, m1.compressionMethods) && m.nextProtoNeg == m1.nextProtoNeg && m.serverName == m1.serverName && + bytes.Equal(m.encryptedServerName, m1.encryptedServerName) && m.ocspStapling == m1.ocspStapling && m.scts == m1.scts && eqCurveIDs(m.supportedCurves, m1.supportedCurves) && @@ -154,6 +156,10 @@ func (m *clientHelloMsg) marshal() []byte { extensionsLength += 5 + len(m.serverName) numExtensions++ } + if len(m.encryptedServerName) > 0 { + extensionsLength += len(m.encryptedServerName) + numExtensions++ + } if len(m.supportedCurves) > 0 { extensionsLength += 2 + 2*len(m.supportedCurves) numExtensions++ @@ -284,6 +290,13 @@ func (m *clientHelloMsg) marshal() []byte { copy(z[5:], []byte(m.serverName)) z = z[l:] } + if len(m.encryptedServerName) > 0 { + l := len(m.encryptedServerName) + binary.BigEndian.PutUint16(z, extensionEncryptedServerName) + binary.BigEndian.PutUint16(z[2:], uint16(l)) + copy(z[4:], m.encryptedServerName) + z = z[4+l:] + } if m.ocspStapling { // RFC 4366, section 3.6 z[0] = byte(extensionStatusRequest >> 8) @@ -490,6 +503,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) alert { m.nextProtoNeg = false m.serverName = "" + m.encryptedServerName = nil m.ocspStapling = false m.ticketSupported = false m.sessionTicket = nil @@ -776,6 +790,9 @@ func (m *clientHelloMsg) unmarshal(data []byte) alert { if length != 0 { return alertDecodeError } + case extensionEncryptedServerName: + // https://tools.ietf.org/html/draft-ietf-tls-esni-01 + m.encryptedServerName = data[:length] } data = data[length:] bindersOffset += length @@ -1234,6 +1251,7 @@ type encryptedExtensionsMsg struct { raw []byte alpnProtocol string earlyData bool + esniNonce []byte } func (m *encryptedExtensionsMsg) equal(i interface{}) bool { @@ -1244,7 +1262,8 @@ func (m *encryptedExtensionsMsg) equal(i interface{}) bool { return bytes.Equal(m.raw, m1.raw) && m.alpnProtocol == m1.alpnProtocol && - m.earlyData == m1.earlyData + m.earlyData == m1.earlyData && + bytes.Equal(m.esniNonce, m1.esniNonce) } func (m *encryptedExtensionsMsg) marshal() []byte { @@ -1264,6 +1283,9 @@ func (m *encryptedExtensionsMsg) marshal() []byte { } length += 2 + 2 + 2 + 1 + alpnLen } + if len(m.esniNonce) > 0 { + length += 2 + 2 + 16 + } x := make([]byte, 4+length) x[0] = typeEncryptedExtensions @@ -1296,6 +1318,13 @@ func (m *encryptedExtensionsMsg) marshal() []byte { z = z[4:] } + if len(m.esniNonce) > 0 { + binary.BigEndian.PutUint16(z, extensionEncryptedServerName) + binary.BigEndian.PutUint16(z[2:], 16) + copy(z[4:], m.esniNonce) + z = z[4+len(m.esniNonce):] + } + m.raw = x return x } @@ -1308,6 +1337,7 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) alert { m.alpnProtocol = "" m.earlyData = false + m.esniNonce = nil extensionsLength := int(data[4])<<8 | int(data[5]) data = data[6:] @@ -1350,6 +1380,11 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) alert { case extensionEarlyData: // https://tools.ietf.org/html/draft-ietf-tls-tls13-18#section-4.2.8 m.earlyData = true + case extensionEncryptedServerName: + if length != 16 { + return alertDecodeError + } + m.esniNonce = data[:16] } data = data[length:] diff --git a/handshake_server.go b/handshake_server.go index 4d4bff7e17..da335b0bff 100644 --- a/handshake_server.go +++ b/handshake_server.go @@ -246,6 +246,28 @@ Curves: return false, errors.New("tls: initial handshake had non-empty renegotiation extension") } + c.serverName = hs.clientHello.serverName + + var esniNonce []byte + if len(hs.clientHello.encryptedServerName) != 0 { + if c.vers < VersionTLS13 { + c.sendAlert(alertHandshakeFailure) + return false, errors.New("tls: ESNI requires TLS 1.3") + } + var clientESNI clientEncryptedSNI + if !clientESNI.unmarshal(hs.clientHello.encryptedServerName) { + c.sendAlert(alertDecodeError) + return false, errors.New("tls: malformed clientESNI extension") + } + var alert alert + // Override and ignore the old server_name value. + esniNonce, c.serverName, alert, err = clientESNI.processClientESNIForServer(c.config, hs.clientHello.random, hs.clientHello.keyShares) + if alert != alertSuccess { + c.sendAlert(alert) + return false, err + } + } + if c.vers < VersionTLS13 { hs.hello = new(serverHelloMsg) hs.hello.vers = c.vers @@ -268,10 +290,7 @@ Curves: c.sendAlert(alertInternalError) return false, err } - } - - if len(hs.clientHello.serverName) > 0 { - c.serverName = hs.clientHello.serverName + hs.hello13Enc.esniNonce = esniNonce } if len(hs.clientHello.alpnProtocols) > 0 { @@ -914,9 +933,15 @@ func (hs *serverHandshakeState) clientHelloInfo() *ClientHelloInfo { pskBinder = hs.clientHello.psks[0].binder } + // Use ESNI if available. + serverName := hs.c.serverName + if len(serverName) == 0 { + serverName = hs.clientHello.serverName + } + hs.cachedClientHelloInfo = &ClientHelloInfo{ CipherSuites: hs.clientHello.cipherSuites, - ServerName: hs.clientHello.serverName, + ServerName: serverName, SupportedCurves: hs.clientHello.supportedCurves, SupportedPoints: hs.clientHello.supportedPoints, SignatureSchemes: hs.clientHello.supportedSignatureAlgorithms, diff --git a/tls_test.go b/tls_test.go index 1b6a445810..4352d779c5 100644 --- a/tls_test.go +++ b/tls_test.go @@ -674,7 +674,7 @@ func TestCloneNonFuncFields(t *testing.T) { switch fn := typ.Field(i).Name; fn { case "Rand": f.Set(reflect.ValueOf(io.Reader(os.Stdin))) - case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "GetClientCertificate", "GetDelegatedCredential": + case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "GetClientCertificate", "GetDelegatedCredential", "ClientESNIKeys", "GetServerESNIKeys": // DeepEqual can't compare functions. If you add a // function field to this list, you must also change // TestCloneFuncFields to ensure that the func field is