I'm using Boost's asio to connect to a site via HTTPS. I want this to only succeed if the certificate is valid, not expired, not self-signed, etc. Unfortunately it seems to always work regardless. Here is my code:
try
{
asio::io_service ioService;
asio::ssl::context sslContext(asio::ssl::context::sslv3_client);
sslContext.load_verify_file("cacert.pem");
asio::ip::tcp::resolver resolver(ioService);
asio::ip::tcp::resolver::query query("self-signed.badssl.com", "443");
asio::ip::tcp::resolver::iterator endpointIterator = resolver.resolve(query);
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(ioService, sslContext);
ioService.run();
// Enable SSL peer verification.
socket.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert);
asio::connect(socket.lowest_layer(), endpointIterator);
socket.handshake(asio::ssl::stream_base::client);
boost::asio::streambuf request;
std::ostream requestStream(&request);
requestStream << "GET / HTTP/1.0\r\n";
requestStream << "Host: self-signed.badssl.com\r\n";
requestStream << "Accept: */*\r\n";
requestStream << "Connection: close\r\n\r\n";
asio::write(socket, request);
And so on. If I use set_verify_callback()
and return false
from the callback, the connection does fail, but preverified
always seems to be true, even for https://self-signed.badssl.com/. Surely that isn't right?
The issue here is Server Name Indication (SNI):
Server Name Indication (SNI) is an extension to the TLS computer
networking protocol by which a client indicates which hostname it is
attempting to connect to at the start of the handshaking process. This
allows a server to present multiple certificates on the same IP
address and TCP port number and hence allows multiple secure (HTTPS)
websites (or any other Service over TLS) to be served off the same IP
address without requiring all those sites to use the same certificate.
The badssl.com server is sending a certificate with a proper chain when you connect with no SNI. If you connect with SNI then the self-signed certificate will be sent. You can verify this with OpenSSL on the command line by observing the difference between the two commands:
openssl s_client -connect self-signed.badssl.com:443 -showcerts
openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com -showcerts
boost::asio
has no API to add SNI, but I think you can do it by using the underlying OpenSSL API and the native_handle() method on your stream. It should be something like this:
SSL_set_tlsext_host_name(socket.native_handle(), "self-signed.badssl.com");
I do note that you are configuring your context with sslv3_client
. As SNI is a TLS extension (i.e. not SSLv3), this may not work without configuring a TLS context.
Well this is weird.
Obviously that server should be serving a self-signed certificate.
When checking with a browser or this online tool: https://www.digicert.com/help/ this is even confirmed. It shows the certificate is:
SHA1 Thumbprint = 079B3259D07C4DE2A1CE0EF4A5B5599D3B2D62EA
However, when I tried from my shell using e.g.
openssl s_client -connect self-signed.badssl.com:443 -debug |&
openssl x509 -text -noout
We get a clearly different cert with a "real" issuer: Issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA
.
We clearly get a different cert:
SHA1 Fingerprint=C8:67:8E:DB:FD:BB:30:B5:3F:2D:7B:F9:66:B8:14:C6:2E:95:92:CE
When I added a callback to your code snippet:
auto cb = [](bool preverified, boost::asio::ssl::verify_context& ctx) {
char subject_name[256];
X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
std::cout << "SSL Verify: " << subject_name << "\n";
return preverified;
};
socket.set_verify_callback(cb);
asio::connect(socket.lowest_layer(), endpointIterator);
std::cout << "Connected: " << socket.lowest_layer().remote_endpoint() << "\n";
system::error_code ec;
socket.handshake(asio::ssl::stream_base::client, ec);
std::cout << "Shook hands: " << ec.message() << "\n";
I saw this was indeed the certificate ASIO was handling for me:
Connected: 104.154.89.105:443
SSL Verify: /C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root
SSL Verify: /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Certification Authority
SSL Verify: /C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA
SSL Verify: /OU=Domain Control Validated/OU=PositiveSSL Wildcard/CN=*.badssl.com
Shook hands: Success
I honestly have no idea how the two can be disagree - even when the IP address appears to resolve to the same. But this certainly seems to be relevant to the symptoms.