1

I find myself needing to set up a WebSocket connection in a hostile environment in which a firewall sniffs SNI information from TLS which I'd rather it didn't. In my particular case, the WebSocket server does not use SNI for request handling, so as such, the SNI part of the handshake could be safely removed.

My question then becomes: In the golang.org WebSocket package, golang.org/x/net/websocket, what is the simplest way to strip SNI information while retaining validation of the provided chain?

The best I have been able to come up with is to simply replace the hostname of the URL to be dialled with its corresponding IP. This causes crypto/tls to never add the problematic SNI information, but, in the solution I was able to come up with, a custom validator ends up having to be provided to validate the chain:

func dial(url string, origin string) (*websocket.Conn, error) {
    // Use system resolver to get IP of host
    hostRegExp := regexp.MustCompile("//([^/]+)/")
    host := hostRegExp.FindStringSubmatch(url)[1]
    addrs, err := net.LookupHost(host)
    if err != nil {
        return fmt.Errorf("Could not resolve address of %s: %v", host, err)
    }
    ip := addrs[0]

    // Replace the hostname in the given URL with its IP instead
    newURL := strings.Replace(url, host, ip, 1)
    config, _ := websocket.NewConfig(newURL, origin)

    // As we have removed the hostname, the Go TLS package will not know what to
    // validate the certificate DNS names against, so we have to provide a custom
    // verifier based on the hostname we threw away.
    config.TlsConfig = &tls.Config{
        InsecureSkipVerify:    true,
        VerifyPeerCertificate: verifier(host),
    }
    return websocket.DialConfig(config)
}

func verifier(host string) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
    return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        // For simplicity, let us only consider the case in which the first certificate is the one
        // to validate, and in which it is signed directly by a CA, with no parsing of
        // intermediate certificates required.
        opts := x509.VerifyOptions{
            DNSName: host,
        }
        rawCert := rawCerts[0]
        cert, err := x509.ParseCertificate(rawCert)
        if err != nil {
            return err
        }
        _, err = cert.Verify(opts)
        return err
    }
}

This totally works but seems rather clunky. Is there a simpler approach? (Ideally one that is not specific to WebSocket applications but works for TLS in general; the exact same idea as above could be applied to HTTPS.)

fuglede
  • 17,388
  • 2
  • 54
  • 99
  • If you don't need the SNI extension then the hostname in URL must be the server you are connecting to. So there's no additional information in the SNI extension and thus no security reason to suppress it. – President James K. Polk Jul 09 '18 at 17:07
  • @JamesKPolk: The reason for replacing the hostname with its IP is exactly to cheat `crypto/tls` into not adding SNI; ideally, `tls.Config` would have something amounting to a `UseSNI` option. – fuglede Jul 09 '18 at 17:22
  • But I don't see why you are avoiding SNI since it reveals no additional information. – President James K. Polk Jul 09 '18 at 20:54
  • It reveals the hostname, which is what is explicitly removed here, and which is what is used for SNI based firewall filters. – fuglede Jul 09 '18 at 21:13
  • Ok, that makes sense, sorry for my confusion. I don't think there is any simpler approach than providing a custom verifier, other than totally ignoring the hostname and leaving yourself open to man-in-the-middle attacks. – President James K. Polk Jul 09 '18 at 21:35
  • No worries, I'm sure it could have been worded more clearly. And I suspect you're right; too bad. – fuglede Jul 09 '18 at 22:20

0 Answers0