mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-30 10:12:45 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			743 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			743 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package httpserver
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"crypto/tls"
 | |
| 	"io"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| )
 | |
| 
 | |
| // tlsHandler is a http.Handler that will inject a value
 | |
| // into the request context indicating if the TLS
 | |
| // connection is likely being intercepted.
 | |
| type tlsHandler struct {
 | |
| 	next        http.Handler
 | |
| 	listener    *tlsHelloListener
 | |
| 	closeOnMITM bool // whether to close connection on MITM; TODO: expose through new directive
 | |
| }
 | |
| 
 | |
| // ServeHTTP checks the User-Agent. For the four main browsers (Chrome,
 | |
| // Edge, Firefox, and Safari) indicated by the User-Agent, the properties
 | |
| // of the TLS Client Hello will be compared. The context value "mitm" will
 | |
| // be set to a value indicating if it is likely that the underlying TLS
 | |
| // connection is being intercepted.
 | |
| //
 | |
| // Note that due to Microsoft's decision to intentionally make IE/Edge
 | |
| // user agents obscure (and look like other browsers), this may offer
 | |
| // less accuracy for IE/Edge clients.
 | |
| //
 | |
| // This MITM detection capability is based on research done by Durumeric,
 | |
| // Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17):
 | |
| // https://jhalderm.com/pub/papers/interception-ndss17.pdf
 | |
| func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | |
| 	if h.listener == nil {
 | |
| 		h.next.ServeHTTP(w, r)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	h.listener.helloInfosMu.RLock()
 | |
| 	info := h.listener.helloInfos[r.RemoteAddr]
 | |
| 	h.listener.helloInfosMu.RUnlock()
 | |
| 
 | |
| 	ua := r.Header.Get("User-Agent")
 | |
| 
 | |
| 	var checked, mitm bool
 | |
| 	if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values)
 | |
| 		r.Header.Get("X-FCCKV2") != "" || // Fortinet
 | |
| 		info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat
 | |
| 		checked = true
 | |
| 		mitm = true
 | |
| 	} else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") ||
 | |
| 		strings.Contains(ua, "Trident") {
 | |
| 		checked = true
 | |
| 		mitm = !info.looksLikeEdge()
 | |
| 	} else if strings.Contains(ua, "Chrome") {
 | |
| 		checked = true
 | |
| 		mitm = !info.looksLikeChrome()
 | |
| 	} else if strings.Contains(ua, "CriOS") {
 | |
| 		// Chrome on iOS sometimes uses iOS-provided TLS stack (which looks exactly like Safari)
 | |
| 		// but for connections that don't render a web page (favicon, etc.) it uses its own...
 | |
| 		checked = true
 | |
| 		mitm = !info.looksLikeChrome() && !info.looksLikeSafari()
 | |
| 	} else if strings.Contains(ua, "Firefox") {
 | |
| 		checked = true
 | |
| 		if strings.Contains(ua, "Windows") {
 | |
| 			ver := getVersion(ua, "Firefox")
 | |
| 			if ver == 45.0 || ver == 52.0 {
 | |
| 				mitm = !info.looksLikeTor()
 | |
| 			} else {
 | |
| 				mitm = !info.looksLikeFirefox()
 | |
| 			}
 | |
| 		} else {
 | |
| 			mitm = !info.looksLikeFirefox()
 | |
| 		}
 | |
| 	} else if strings.Contains(ua, "Safari") {
 | |
| 		checked = true
 | |
| 		mitm = !info.looksLikeSafari()
 | |
| 	}
 | |
| 
 | |
| 	if checked {
 | |
| 		r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm))
 | |
| 	}
 | |
| 
 | |
| 	if mitm && h.closeOnMITM {
 | |
| 		// TODO: This termination might need to happen later in the middleware
 | |
| 		// chain in order to be picked up by the log directive, in case the site
 | |
| 		// owner still wants to log this event. It'll probably require a new
 | |
| 		// directive. If this feature is useful, we can finish implementing this.
 | |
| 		r.Close = true
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	h.next.ServeHTTP(w, r)
 | |
| }
 | |
| 
 | |
| // getVersion returns a (possibly simplified) representation of the version string
 | |
| // from a UserAgent string. It returns a float, so it can represent major and minor
 | |
| // versions; the rest of the version is just tacked on behind the decimal point.
 | |
| // The purpose of this is to stay simple while allowing for basic, fast comparisons.
 | |
| // If the version for softwareName is not found in ua, -1 is returned.
 | |
| func getVersion(ua, softwareName string) float64 {
 | |
| 	search := softwareName + "/"
 | |
| 	start := strings.Index(ua, search)
 | |
| 	if start < 0 {
 | |
| 		return -1
 | |
| 	}
 | |
| 	start += len(search)
 | |
| 	end := strings.Index(ua[start:], " ")
 | |
| 	if end < 0 {
 | |
| 		end = len(ua)
 | |
| 	}
 | |
| 	strVer := strings.Replace(ua[start:end], "-", "", -1)
 | |
| 	firstDot := strings.Index(strVer, ".")
 | |
| 	if firstDot >= 0 {
 | |
| 		strVer = strVer[:firstDot+1] + strings.Replace(strVer[firstDot+1:], ".", "", -1)
 | |
| 	}
 | |
| 	ver, err := strconv.ParseFloat(strVer, 64)
 | |
| 	if err != nil {
 | |
| 		return -1
 | |
| 	}
 | |
| 	return ver
 | |
| }
 | |
| 
 | |
| // clientHelloConn reads the ClientHello
 | |
| // and stores it in the attached listener.
 | |
| type clientHelloConn struct {
 | |
| 	net.Conn
 | |
| 	listener  *tlsHelloListener
 | |
| 	readHello bool // whether ClientHello has been read
 | |
| 	buf       *bytes.Buffer
 | |
| }
 | |
| 
 | |
| // Read reads from c.Conn (by letting the standard library
 | |
| // do the reading off the wire), with the exception of
 | |
| // getting a copy of the ClientHello so it can parse it.
 | |
| func (c *clientHelloConn) Read(b []byte) (n int, err error) {
 | |
| 	// if we've already read the ClientHello, pass thru
 | |
| 	if c.readHello {
 | |
| 		return c.Conn.Read(b)
 | |
| 	}
 | |
| 
 | |
| 	// we let the standard lib read off the wire for us, and
 | |
| 	// tee that into our buffer so we can read the ClientHello
 | |
| 	tee := io.TeeReader(c.Conn, c.buf)
 | |
| 	n, err = tee.Read(b)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if c.buf.Len() < 5 {
 | |
| 		return // need to read more bytes for header
 | |
| 	}
 | |
| 
 | |
| 	// read the header bytes
 | |
| 	hdr := make([]byte, 5)
 | |
| 	_, err = io.ReadFull(c.buf, hdr)
 | |
| 	if err != nil {
 | |
| 		return // this would be highly unusual and sad
 | |
| 	}
 | |
| 
 | |
| 	// get length of the ClientHello message and read it
 | |
| 	length := int(uint16(hdr[3])<<8 | uint16(hdr[4]))
 | |
| 	if c.buf.Len() < length {
 | |
| 		return // need to read more bytes
 | |
| 	}
 | |
| 	hello := make([]byte, length)
 | |
| 	_, err = io.ReadFull(c.buf, hello)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	bufpool.Put(c.buf) // buffer no longer needed
 | |
| 
 | |
| 	// parse the ClientHello and store it in the map
 | |
| 	rawParsed := parseRawClientHello(hello)
 | |
| 	c.listener.helloInfosMu.Lock()
 | |
| 	c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
 | |
| 	c.listener.helloInfosMu.Unlock()
 | |
| 
 | |
| 	c.readHello = true
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // parseRawClientHello parses data which contains the raw
 | |
| // TLS Client Hello message. It extracts relevant information
 | |
| // into info. Any error reading the Client Hello (such as
 | |
| // insufficient length or invalid length values) results in
 | |
| // a silent error and an incomplete info struct, since there
 | |
| // is no good way to handle an error like this during Accept().
 | |
| // The data is expected to contain the whole ClientHello and
 | |
| // ONLY the ClientHello.
 | |
| //
 | |
| // The majority of this code is borrowed from the Go standard
 | |
| // library, which is (c) The Go Authors. It has been modified
 | |
| // to fit this use case.
 | |
| func parseRawClientHello(data []byte) (info rawHelloInfo) {
 | |
| 	if len(data) < 42 {
 | |
| 		return
 | |
| 	}
 | |
| 	sessionIDLen := int(data[38])
 | |
| 	if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
 | |
| 		return
 | |
| 	}
 | |
| 	data = data[39+sessionIDLen:]
 | |
| 	if len(data) < 2 {
 | |
| 		return
 | |
| 	}
 | |
| 	// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
 | |
| 	// they are uint16s, the number must be even.
 | |
| 	cipherSuiteLen := int(data[0])<<8 | int(data[1])
 | |
| 	if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
 | |
| 		return
 | |
| 	}
 | |
| 	numCipherSuites := cipherSuiteLen / 2
 | |
| 	// read in the cipher suites
 | |
| 	info.cipherSuites = make([]uint16, numCipherSuites)
 | |
| 	for i := 0; i < numCipherSuites; i++ {
 | |
| 		info.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
 | |
| 	}
 | |
| 	data = data[2+cipherSuiteLen:]
 | |
| 	if len(data) < 1 {
 | |
| 		return
 | |
| 	}
 | |
| 	// read in the compression methods
 | |
| 	compressionMethodsLen := int(data[0])
 | |
| 	if len(data) < 1+compressionMethodsLen {
 | |
| 		return
 | |
| 	}
 | |
| 	info.compressionMethods = data[1 : 1+compressionMethodsLen]
 | |
| 
 | |
| 	data = data[1+compressionMethodsLen:]
 | |
| 
 | |
| 	// ClientHello is optionally followed by extension data
 | |
| 	if len(data) < 2 {
 | |
| 		return
 | |
| 	}
 | |
| 	extensionsLength := int(data[0])<<8 | int(data[1])
 | |
| 	data = data[2:]
 | |
| 	if extensionsLength != len(data) {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// read in each extension, and extract any relevant information
 | |
| 	// from extensions we care about
 | |
| 	for len(data) != 0 {
 | |
| 		if len(data) < 4 {
 | |
| 			return
 | |
| 		}
 | |
| 		extension := uint16(data[0])<<8 | uint16(data[1])
 | |
| 		length := int(data[2])<<8 | int(data[3])
 | |
| 		data = data[4:]
 | |
| 		if len(data) < length {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// record that the client advertised support for this extension
 | |
| 		info.extensions = append(info.extensions, extension)
 | |
| 
 | |
| 		switch extension {
 | |
| 		case extensionSupportedCurves:
 | |
| 			// http://tools.ietf.org/html/rfc4492#section-5.5.1
 | |
| 			if length < 2 {
 | |
| 				return
 | |
| 			}
 | |
| 			l := int(data[0])<<8 | int(data[1])
 | |
| 			if l%2 == 1 || length != l+2 {
 | |
| 				return
 | |
| 			}
 | |
| 			numCurves := l / 2
 | |
| 			info.curves = make([]tls.CurveID, numCurves)
 | |
| 			d := data[2:]
 | |
| 			for i := 0; i < numCurves; i++ {
 | |
| 				info.curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1])
 | |
| 				d = d[2:]
 | |
| 			}
 | |
| 		case extensionSupportedPoints:
 | |
| 			// http://tools.ietf.org/html/rfc4492#section-5.5.2
 | |
| 			if length < 1 {
 | |
| 				return
 | |
| 			}
 | |
| 			l := int(data[0])
 | |
| 			if length != l+1 {
 | |
| 				return
 | |
| 			}
 | |
| 			info.points = make([]uint8, l)
 | |
| 			copy(info.points, data[1:])
 | |
| 		}
 | |
| 
 | |
| 		data = data[length:]
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // newTLSListener returns a new tlsHelloListener that wraps ln.
 | |
| func newTLSListener(ln net.Listener, config *tls.Config) *tlsHelloListener {
 | |
| 	return &tlsHelloListener{
 | |
| 		Listener:   ln,
 | |
| 		config:     config,
 | |
| 		helloInfos: make(map[string]rawHelloInfo),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // tlsHelloListener is a TLS listener that is specially designed
 | |
| // to read the ClientHello manually so we can extract necessary
 | |
| // information from it. Each ClientHello message is mapped by
 | |
| // the remote address of the client, which must be removed when
 | |
| // the connection is closed (use ConnState).
 | |
| type tlsHelloListener struct {
 | |
| 	net.Listener
 | |
| 	config       *tls.Config
 | |
| 	helloInfos   map[string]rawHelloInfo
 | |
| 	helloInfosMu sync.RWMutex
 | |
| }
 | |
| 
 | |
| // Accept waits for and returns the next connection to the listener.
 | |
| // After it accepts the underlying connection, it reads the
 | |
| // ClientHello message and stores the parsed data into a map on l.
 | |
| func (l *tlsHelloListener) Accept() (net.Conn, error) {
 | |
| 	conn, err := l.Listener.Accept()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	buf := bufpool.Get().(*bytes.Buffer)
 | |
| 	buf.Reset()
 | |
| 	helloConn := &clientHelloConn{Conn: conn, listener: l, buf: buf}
 | |
| 	return tls.Server(helloConn, l.config), nil
 | |
| }
 | |
| 
 | |
| // rawHelloInfo contains the "raw" data parsed from the TLS
 | |
| // Client Hello. No interpretation is done on the raw data.
 | |
| //
 | |
| // The methods on this type implement heuristics described
 | |
| // by Durumeric, Halderman, et. al. in
 | |
| // "The Security Impact of HTTPS Interception":
 | |
| // https://jhalderm.com/pub/papers/interception-ndss17.pdf
 | |
| type rawHelloInfo struct {
 | |
| 	cipherSuites       []uint16
 | |
| 	extensions         []uint16
 | |
| 	compressionMethods []byte
 | |
| 	curves             []tls.CurveID
 | |
| 	points             []uint8
 | |
| }
 | |
| 
 | |
| // advertisesHeartbeatSupport returns true if info indicates
 | |
| // that the client supports the Heartbeat extension.
 | |
| func (info rawHelloInfo) advertisesHeartbeatSupport() bool {
 | |
| 	for _, ext := range info.extensions {
 | |
| 		if ext == extensionHeartbeat {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // looksLikeFirefox returns true if info looks like a handshake
 | |
| // from a modern version of Firefox.
 | |
| func (info rawHelloInfo) looksLikeFirefox() bool {
 | |
| 	// "To determine whether a Firefox session has been
 | |
| 	// intercepted, we check for the presence and order
 | |
| 	// of extensions, cipher suites, elliptic curves,
 | |
| 	// EC point formats, and handshake compression methods."
 | |
| 
 | |
| 	// We check for the presence and order of the extensions.
 | |
| 	// Note: Sometimes 0x15 (21, padding) is present, sometimes not.
 | |
| 	// Note: Firefox 51+ does not advertise 0x3374 (13172, NPN).
 | |
| 	// Note: Firefox doesn't advertise 0x0 (0, SNI) when connecting to IP addresses.
 | |
| 	// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
 | |
| 	// Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability.
 | |
| 	requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
 | |
| 	if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// We check for both presence of curves and their ordering.
 | |
| 	requiredCurves := []tls.CurveID{29, 23, 24, 25}
 | |
| 	if len(info.curves) < len(requiredCurves) {
 | |
| 		return false
 | |
| 	}
 | |
| 	for i := range requiredCurves {
 | |
| 		if info.curves[i] != requiredCurves[i] {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	if len(info.curves) > len(requiredCurves) {
 | |
| 		// newer Firefox (55 Nightly?) may have additional curves at end of list
 | |
| 		allowedCurves := []tls.CurveID{256, 257}
 | |
| 		for i := range allowedCurves {
 | |
| 			if info.curves[len(requiredCurves)+i] != allowedCurves[i] {
 | |
| 				return false
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if hasGreaseCiphers(info.cipherSuites) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// We check for order of cipher suites but not presence, since
 | |
| 	// according to the paper, cipher suites may be not be added
 | |
| 	// or reordered by the user, but they may be disabled.
 | |
| 	expectedCipherSuiteOrder := []uint16{
 | |
| 		TLS_AES_128_GCM_SHA256,                      // 0x1301
 | |
| 		TLS_CHACHA20_POLY1305_SHA256,                // 0x1303
 | |
| 		TLS_AES_256_GCM_SHA384,                      // 0x1302
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,   // 0xc02f
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,  // 0xcca9
 | |
| 		tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,    // 0xcca8
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,   // 0xc030
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,    // 0xc00a
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,    // 0xc009
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,      // 0xc013
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,      // 0xc014
 | |
| 		TLS_DHE_RSA_WITH_AES_128_CBC_SHA,            // 0x33
 | |
| 		TLS_DHE_RSA_WITH_AES_256_CBC_SHA,            // 0x39
 | |
| 		tls.TLS_RSA_WITH_AES_128_CBC_SHA,            // 0x2f
 | |
| 		tls.TLS_RSA_WITH_AES_256_CBC_SHA,            // 0x35
 | |
| 		tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,           // 0xa
 | |
| 	}
 | |
| 	return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false)
 | |
| }
 | |
| 
 | |
| // looksLikeChrome returns true if info looks like a handshake
 | |
| // from a modern version of Chrome.
 | |
| func (info rawHelloInfo) looksLikeChrome() bool {
 | |
| 	// "We check for ciphers and extensions that Chrome is known
 | |
| 	// to not support, but do not check for the inclusion of
 | |
| 	// specific ciphers or extensions, nor do we validate their
 | |
| 	// order. When appropriate, we check the presence and order
 | |
| 	// of elliptic curves, compression methods, and EC point formats."
 | |
| 
 | |
| 	// Not in Chrome 56, but present in Safari 10 (Feb. 2017):
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
 | |
| 	// TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028)
 | |
| 	// TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
 | |
| 	// TLS_RSA_WITH_AES_256_CBC_SHA256 (0x3d)
 | |
| 	// TLS_RSA_WITH_AES_128_CBC_SHA256 (0x3c)
 | |
| 
 | |
| 	// Not in Chrome 56, but present in Firefox 51 (Feb. 2017):
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
 | |
| 	// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
 | |
| 	// TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x33)
 | |
| 	// TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x39)
 | |
| 
 | |
| 	// Selected ciphers present in Chrome mobile (Feb. 2017):
 | |
| 	// 0xc00a, 0xc014, 0xc009, 0x9c, 0x9d, 0x2f, 0x35, 0xa
 | |
| 
 | |
| 	chromeCipherExclusions := map[uint16]struct{}{
 | |
| 		TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:     {}, // 0xc024
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: {}, // 0xc023
 | |
| 		TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384:       {}, // 0xc028
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:   {}, // 0xc027
 | |
| 		TLS_RSA_WITH_AES_256_CBC_SHA256:             {}, // 0x3d
 | |
| 		tls.TLS_RSA_WITH_AES_128_CBC_SHA256:         {}, // 0x3c
 | |
| 		TLS_DHE_RSA_WITH_AES_128_CBC_SHA:            {}, // 0x33
 | |
| 		TLS_DHE_RSA_WITH_AES_256_CBC_SHA:            {}, // 0x39
 | |
| 	}
 | |
| 	for _, ext := range info.cipherSuites {
 | |
| 		if _, ok := chromeCipherExclusions[ext]; ok {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Chrome does not include curve 25 (CurveP521) (as of Chrome 56, Feb. 2017).
 | |
| 	for _, curve := range info.curves {
 | |
| 		if curve == 25 {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if !hasGreaseCiphers(info.cipherSuites) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // looksLikeEdge returns true if info looks like a handshake
 | |
| // from a modern version of MS Edge.
 | |
| func (info rawHelloInfo) looksLikeEdge() bool {
 | |
| 	// "SChannel connections can by uniquely identified because SChannel
 | |
| 	// is the only TLS library we tested that includes the OCSP status
 | |
| 	// request extension before the supported groups and EC point formats
 | |
| 	// extensions."
 | |
| 	//
 | |
| 	// More specifically, the OCSP status request extension appears
 | |
| 	// *directly* before the other two extensions, which occur in that
 | |
| 	// order. (I contacted the authors for clarification and verified it.)
 | |
| 	for i, ext := range info.extensions {
 | |
| 		if ext == extensionOCSPStatusRequest {
 | |
| 			if len(info.extensions) <= i+2 {
 | |
| 				return false
 | |
| 			}
 | |
| 			if info.extensions[i+1] != extensionSupportedCurves ||
 | |
| 				info.extensions[i+2] != extensionSupportedPoints {
 | |
| 				return false
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, cs := range info.cipherSuites {
 | |
| 		// As of Feb. 2017, Edge does not have 0xff, but Avast adds it
 | |
| 		if cs == scsvRenegotiation {
 | |
| 			return false
 | |
| 		}
 | |
| 		// Edge and modern IE do not have 0x4 or 0x5, but Blue Coat does
 | |
| 		if cs == TLS_RSA_WITH_RC4_128_MD5 || cs == tls.TLS_RSA_WITH_RC4_128_SHA {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if hasGreaseCiphers(info.cipherSuites) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // looksLikeSafari returns true if info looks like a handshake
 | |
| // from a modern version of MS Safari.
 | |
| func (info rawHelloInfo) looksLikeSafari() bool {
 | |
| 	// "One unique aspect of Secure Transport is that it includes
 | |
| 	// the TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0xff) cipher first,
 | |
| 	// whereas the other libraries we investigated include the
 | |
| 	// cipher last. Similar to Microsoft, Apple has changed
 | |
| 	// TLS behavior in minor OS updates, which are not indicated
 | |
| 	// in the HTTP User-Agent header. We allow for any of the
 | |
| 	// updates when validating handshakes, and we check for the
 | |
| 	// presence and ordering of ciphers, extensions, elliptic
 | |
| 	// curves, and compression methods."
 | |
| 
 | |
| 	// Note that any C lib (e.g. curl) compiled on macOS
 | |
| 	// will probably use Secure Transport which will also
 | |
| 	// share the TLS handshake characteristics of Safari.
 | |
| 
 | |
| 	// Let's do the easy check first... should be sufficient in many cases.
 | |
| 	if len(info.cipherSuites) < 1 {
 | |
| 		return false
 | |
| 	}
 | |
| 	if info.cipherSuites[0] != scsvRenegotiation {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// We check for the presence and order of the extensions.
 | |
| 	requiredExtensionsOrder := []uint16{10, 11, 13, 13172, 16, 5, 18, 23}
 | |
| 	if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	if hasGreaseCiphers(info.cipherSuites) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// We check for order and presence of cipher suites
 | |
| 	expectedCipherSuiteOrder := []uint16{
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
 | |
| 		TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,     // 0xc024
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // 0xc023
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,    // 0xc00a
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,    // 0xc009
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,   // 0xc030
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,   // 0xc02f
 | |
| 		TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,       // 0xc028
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,   // 0xc027
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,      // 0xc014
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,      // 0xc013
 | |
| 		tls.TLS_RSA_WITH_AES_256_GCM_SHA384,         // 0x9d
 | |
| 		tls.TLS_RSA_WITH_AES_128_GCM_SHA256,         // 0x9c
 | |
| 		TLS_RSA_WITH_AES_256_CBC_SHA256,             // 0x3d
 | |
| 		tls.TLS_RSA_WITH_AES_128_CBC_SHA256,         // 0x3c
 | |
| 		tls.TLS_RSA_WITH_AES_256_CBC_SHA,            // 0x35
 | |
| 		tls.TLS_RSA_WITH_AES_128_CBC_SHA,            // 0x2f
 | |
| 	}
 | |
| 	return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, true)
 | |
| }
 | |
| 
 | |
| // looksLikeTor returns true if the info looks like a ClientHello from Tor browser
 | |
| // (based on Firefox).
 | |
| func (info rawHelloInfo) looksLikeTor() bool {
 | |
| 	requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13}
 | |
| 	if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// check for session tickets support; Tor doesn't support them to prevent tracking
 | |
| 	for _, ext := range info.extensions {
 | |
| 		if ext == 35 {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// We check for both presence of curves and their ordering, including
 | |
| 	// an optional curve at the beginning (for Tor based on Firefox 52)
 | |
| 	infoCurves := info.curves
 | |
| 	if len(info.curves) == 4 {
 | |
| 		if info.curves[0] != 29 {
 | |
| 			return false
 | |
| 		}
 | |
| 		infoCurves = info.curves[1:]
 | |
| 	}
 | |
| 	requiredCurves := []tls.CurveID{23, 24, 25}
 | |
| 	if len(infoCurves) < len(requiredCurves) {
 | |
| 		return false
 | |
| 	}
 | |
| 	for i := range requiredCurves {
 | |
| 		if infoCurves[i] != requiredCurves[i] {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if hasGreaseCiphers(info.cipherSuites) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	// We check for order of cipher suites but not presence, since
 | |
| 	// according to the paper, cipher suites may be not be added
 | |
| 	// or reordered by the user, but they may be disabled.
 | |
| 	expectedCipherSuiteOrder := []uint16{
 | |
| 		TLS_AES_128_GCM_SHA256,                      // 0x1301
 | |
| 		TLS_CHACHA20_POLY1305_SHA256,                // 0x1303
 | |
| 		TLS_AES_256_GCM_SHA384,                      // 0x1302
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,   // 0xc02f
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,  // 0xcca9
 | |
| 		tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,    // 0xcca8
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,   // 0xc030
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,    // 0xc00a
 | |
| 		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,    // 0xc009
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,      // 0xc013
 | |
| 		tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,      // 0xc014
 | |
| 		TLS_DHE_RSA_WITH_AES_128_CBC_SHA,            // 0x33
 | |
| 		TLS_DHE_RSA_WITH_AES_256_CBC_SHA,            // 0x39
 | |
| 		tls.TLS_RSA_WITH_AES_128_CBC_SHA,            // 0x2f
 | |
| 		tls.TLS_RSA_WITH_AES_256_CBC_SHA,            // 0x35
 | |
| 		tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,           // 0xa
 | |
| 	}
 | |
| 	return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false)
 | |
| }
 | |
| 
 | |
| // assertPresenceAndOrdering will return true if candidateList contains
 | |
| // the items in requiredItems in the same order as requiredItems.
 | |
| //
 | |
| // If requiredIsSubset is true, then all items in requiredItems must be
 | |
| // present in candidateList. If requiredIsSubset is false, then requiredItems
 | |
| // may contain items that are not in candidateList.
 | |
| //
 | |
| // In all cases, the order of requiredItems is enforced.
 | |
| func assertPresenceAndOrdering(requiredItems, candidateList []uint16, requiredIsSubset bool) bool {
 | |
| 	superset := requiredItems
 | |
| 	subset := candidateList
 | |
| 	if requiredIsSubset {
 | |
| 		superset = candidateList
 | |
| 		subset = requiredItems
 | |
| 	}
 | |
| 
 | |
| 	var j int
 | |
| 	for _, item := range subset {
 | |
| 		var found bool
 | |
| 		for j < len(superset) {
 | |
| 			if superset[j] == item {
 | |
| 				found = true
 | |
| 				break
 | |
| 			}
 | |
| 			j++
 | |
| 		}
 | |
| 		if j == len(superset) && !found {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func hasGreaseCiphers(cipherSuites []uint16) bool {
 | |
| 	for _, cipher := range cipherSuites {
 | |
| 		if _, ok := greaseCiphers[cipher]; ok {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // pool buffers so we can reuse allocations over time
 | |
| var bufpool = sync.Pool{
 | |
| 	New: func() interface{} {
 | |
| 		return new(bytes.Buffer)
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var greaseCiphers = map[uint16]struct{}{
 | |
| 	0x0A0A: {},
 | |
| 	0x1A1A: {},
 | |
| 	0x2A2A: {},
 | |
| 	0x3A3A: {},
 | |
| 	0x4A4A: {},
 | |
| 	0x5A5A: {},
 | |
| 	0x6A6A: {},
 | |
| 	0x7A7A: {},
 | |
| 	0x8A8A: {},
 | |
| 	0x9A9A: {},
 | |
| 	0xAAAA: {},
 | |
| 	0xBABA: {},
 | |
| 	0xCACA: {},
 | |
| 	0xDADA: {},
 | |
| 	0xEAEA: {},
 | |
| 	0xFAFA: {},
 | |
| }
 | |
| 
 | |
| // Define variables used for TLS communication
 | |
| const (
 | |
| 	extensionOCSPStatusRequest = 5
 | |
| 	extensionSupportedCurves   = 10 // also called "SupportedGroups"
 | |
| 	extensionSupportedPoints   = 11
 | |
| 	extensionHeartbeat         = 15
 | |
| 
 | |
| 	scsvRenegotiation = 0xff
 | |
| 
 | |
| 	// cipher suites missing from the crypto/tls package,
 | |
| 	// in no particular order here
 | |
| 	TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024
 | |
| 	TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384   = 0xc028
 | |
| 	TLS_RSA_WITH_AES_256_CBC_SHA256         = 0x3d
 | |
| 	TLS_DHE_RSA_WITH_AES_128_CBC_SHA        = 0x33
 | |
| 	TLS_DHE_RSA_WITH_AES_256_CBC_SHA        = 0x39
 | |
| 	TLS_RSA_WITH_RC4_128_MD5                = 0x4
 | |
| 
 | |
| 	// new PSK ciphers introduced by TLS 1.3, not (yet) in crypto/tls
 | |
| 	// https://tlswg.github.io/tls13-spec/#rfc.appendix.A.4)
 | |
| 	TLS_AES_128_GCM_SHA256       = 0x1301
 | |
| 	TLS_AES_256_GCM_SHA384       = 0x1302
 | |
| 	TLS_CHACHA20_POLY1305_SHA256 = 0x1303
 | |
| 	TLS_AES_128_CCM_SHA256       = 0x1304
 | |
| 	TLS_AES_128_CCM_8_SHA256     = 0x1305
 | |
| )
 |