I've been experimenting with ways to omit SNI while still doing proper
certificate verification (or more generally, to verify using a name that
differs from the SNI name) in Go code. It is not that straightforward. I
wrote a prototype, and I'd appreciate any comment by people who have
worked through a similar problem. One of the APIs it uses,
VerifyPeerCertificate, was only added in Go 1.8, about one year ago.
https://bugs.torproject.org/12208#comment:14
https://trac.torproject.org/projects/tor/attachment/ticket/12208/names.go
I'll paste my design rationales from the ticket here.
----
The past couple of days I've been experimenting with ways to set or omit the SNI. There's a demo program in [
https://trac.torproject.org/projects/tor/attachment/ticket/12208/names.go names.go] and I'd appreciate any review or questions about design decisions. I took inspiration from Ox's [
https://gist.github.com/oxtoacart/5e78d25a7f9a9cda10cd domainfront.go demo] from 2014, but changes in the standard library (especially since Go 1.8) provide better ways to do some things now. If this looks good, I'm going to use it as the basis of new code in meek-client.
The basic situation is that we are managing four domain names:
* `urlName`: this is the name we resolve and actually establish a TCP connection with.
* `hostName`: this is the name that goes in the HTTP Host header.
* `sniName`: this is the name that goes in the SNI extension.
* `verifyName`: this is the name that the client verifies the server certificate against.
In normal everyday HTTPS, all four of these names are the same. Domain fronting allows `hostName` to differ, but the other three names are the same. [
https://trac.torproject.org/projects/tor/attachment/ticket/12208/names.go names.go] shows how to make all four names independent, so you can, for example, send no SNI but still verify the server certificate against a hostname, while still fronting a different domain in the Host header.
An explanation of some decisions:
1. The way to set the SNI is to modify `TLSClientConfig.ServerName`; the way to set the verification name is to modify the `TLSClientConfig.VerifyPeerCertificate` callback. Both of these are properties of [
https://golang.org/pkg/net/http/#Transport http.Transport], which is a long-lived data structure that manages multiple HTTP roundtrips. We therefore need a separate `http.Transport` for each unique (`sniName`, `verifyName`) pair. The only exception is when `urlName` = `sniName` = `verifyName`: that's the built-in behavior of the net/http package, so we can share an `http.Transport` in that case. The function `getHTTPTransportForNames` creates and caches new `http.Transport`s as needed. (And note that `hostName` does not enter this logic at all: whether or not you are domain fronting is orthogonal to certificate verification.)
2. The way to omit the SNI extension is to set `TLSClientConfig.ServerName` to an IP address. The IP address is not actually used for anything else, as long as `TLSClientConfig.InsecureSkipVerify` is set. The trick of setting `TLSClientConfig.TLSDial`, and then pulling the certificates from the `tls.Conn` using `ConnectionState`, [
https://github.com/golang/go/issues/9126 doesn't work] when a proxy is set, because the proxy bypasses the dialer (which is the reason why [
https://github.com/golang/go/issues/16363 VerifyPeerCertificate was introduced]).
3. We need to use multiple simultaneous `http.Transport`s, so we can't just modify the global [
https://golang.org/pkg/net/http/#RoundTripper http.DefaultTransport] like we do currently. But `http.DefaultTransport` has some nice default settings for things like timeouts. Go doesn't provide any method for creating a new `http.Transport` that has the default settings, and you can't just copy the struct literal because it contains mutexes. So I used a reflection trick I found on a mailing list to copy just the public struct members. Also if we naively set `TLSClientConfig`, it [
https://github.com/golang/go/issues/17051 disables HTTP/2 support]; because we plan to modify `TLSClientConfig`, we have to first call `http2.ConfigureTransport`.
Here are examples use cases.
all names equal (ordinary HTTPS)::
{{{
$ ./names -host
example.com -sni
example.com -verify
example.com https://example.com/
urlName: "
example.com"
hostName: "
example.com"
sniName: "
example.com"
verifyName: "
example.com"
HTTP/2.0 200 OK
}}}
omit SNI, but still verify::
{{{
$ ./names -host
example.com -sni "" -verify
example.com https://example.com/
urlName: "
example.com"
hostName: "
example.com"
sniName: ""
verifyName: "
example.com"
HTTP/2.0 200 OK
}}}
omit DNS request and SNI, but still verify::
{{{
$ dig +short
example.com
93.184.216.34
$ ./names -host
example.com -sni "" -verify
example.com https://93.184.216.34/
urlName: "93.184.216.34"
hostName: "
example.com"
sniName: ""
verifyName: "
example.com"
HTTP/2.0 200 OK
}}}
ask for one name in the SNI but verify against another (still valid) name::
{{{
$ ./names -host
example.com -sni
example.com -verify
www.example.net https://example.com/
urlName: "
example.com"
hostName: "
example.com"
sniName: "
example.com"
verifyName: "
www.example.net"
HTTP/2.0 200 OK
}}}
try verifying against a name that's not valid for the certificate::
{{{
$ ./names -host
example.com -sni
example.com -verify
microsoft.com https://example.com/
urlName: "
example.com"
hostName: "
example.com"
sniName: "
example.com"
verifyName: "
microsoft.com"
error: x509: certificate is valid for
www.example.org,
example.com,
example.edu,
example.net,
example.org,
www.example.com,
www.example.edu,
www.example.net, not
microsoft.com
}}}
domain fronting::
{{{
$ ./names -host
maps.google.com -sni
www.google.com -verify
www.google.com https://www.google.com/
urlName: "
www.google.com"
hostName: "
maps.google.com"
sniName: "
www.google.com"
verifyName: "
www.google.com"
HTTP/2.0 302 Found
}}}
domain fronting [
https://bugs.torproject.org/25804 thwarted]::
{{{
$ ./names -host
html5-demos.appspot.com -sni
www.google.com -verify
www.google.com https://www.google.com/
urlName: "
www.google.com"
hostName: "
html5-demos.appspot.com"
sniName: "
www.google.com"
verifyName: "
www.google.com"
HTTP/2.0 502 Bad Gateway
}}}
but works if you leave off the SNI::
{{{
$ ./names -host
html5-demos.appspot.com -sni "" -verify
www.google.com https://www.google.com/
urlName: "
www.google.com"
hostName: "
html5-demos.appspot.com"
sniName: ""
verifyName: "
www.google.com"
HTTP/2.0 200 OK
}}}