OpenSSL 1.0 + SNI + curl = interoperability #fail
I was messing around with a new install of Apache 2.4.3 with the new OpenSSL library and ran into an interesting problem. MokaFive Player would fail to connect to my Apache server, but I could connect fine from my web browser. libcurl was returning CURLE_SSL_CONNECT_ERROR (which had this mildly entertaining and uninformative message: "wrong when connecting with SSL"). The SSL certs and ciphers were all set up correctly, so what gives?
I turned on cURL verbose logging to see what was the matter, and saw this error line:
error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:reason(1112)
What's that? Decoding the "1112" reason means this is a TLS Alert with Level 1 (Warning) and Code 112 (Unrecognized name).
This gets into the SSL "SNI" (Server Name Indication) feature, which is a nifty recent extension to SSL where the client sends the name of the host it is trying to connect to at the start of the SSL handshake process. This is really useful because it allows you to run different virtual hosts on the same IP address and port. At connection time, the server can look at what host the client is connecting to and present the correct certificate. Even cooler, proxies and load balancers can look at the SNI name and just forward the raw socket traffic to the appropriate server. The proxy/load balancer doesn't need to terminate the SSL connection - the SSL connection can terminate at the real server, which is much nicer.
Before SNI, people would do this with HTTP "Host" headers. But SNI is much better because you don't need to decode HTTP headers (which requires terminating the SSL connection to read and parse the headers). And of course without SNI the server can't present the right certificate during SSL negotiation because it doesn't know which certificate you want until after you establish the connection and read the Host header.
Anyway, why does this fail with cURL and not with any browser? It turns out to be a strange interaction between different OpenSSL versions, cURL, and Apache. Basically, when you are using cURL compiled with OpenSSL >=0.9.8g but <1.0 and are connecting to an server with OpenSSL >=1.0, *and* the hostname sent via SNI does not match what the server expects, the server will return an advisory warning "unrecognized name", but the server will continue to process the request anyway using the default virtual host definition. Every client on the planet except cURL will ignore the advisory warning and keep going. But cURL will fail the connection.
There are a few workarounds, however. If you tell cURL explicitly what SSL version to use when connecting, it won't send an SNI header so the connection will work fine. Also, if you change your Apache server configuration by adding a ServerName/ServerAlias that matches what the client sends, the server will no longer send the 1112 warning and you won't trigger the bug. This second workaround is the one I implemented.
So whose fault is it? One could argue this is an OpenSSL bug because they changed the protocol in an incompatible way between different OpenSSL versions, so old clients will return a new warning code when connecting with newer servers. But to me, the fault clearly lies with cURL. Every other client ignores the SNI warning. cURL is the only one that fails. The cURL author knows about the problem but does not seem interested in fixing it, insisting this is a bug in OpenSSL or Apache.
Who loses in this finger-pointing? People who use cURL and want to connect to web servers running newer versions of SSL. So it really makes no sense.
SSL is one of those fundamental things you assume "just works", and you shouldn't have to worry about version mismatch or interoperability. It is designed to be backward compatible and negotiate ciphers, protocol versions, certificates, etc. Unfortunately that is not the case. So a big ol' #fail to OpenSSL and cURL for screwing up SSL interoperability!