Boost SSL 验证过期和自签名证书

Boost SSL verifies expired and self-signed certificates

我正在使用 Boost 的 asio 通过 HTTPS 连接到站点。我希望这只有在证书有效、未过期、未自签名等情况下才能成功。不幸的是,无论如何它似乎总是有效。这是我的代码:

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);

等等。如果我从回调中使用 set_verify_callback() 和 return false,连接确实会失败,但 preverified 似乎总是正确的,即使对于 https://self-signed.badssl.com/ 也是如此。肯定不对吧?

嗯,这很奇怪。

显然该服务器应该提供自签名证书。

使用浏览器或此在线工具检查时:https://www.digicert.com/help/甚至可以确认。它显示证书是:

SHA1 Thumbprint = 079B3259D07C4DE2A1CE0EF4A5B5599D3B2D62EA

然而,当我尝试从 shell 使用例如

 openssl s_client -connect self-signed.badssl.com:443 -debug |&
      openssl x509 -text -noout

我们从 "real" 颁发者获得了明显不同的证书:Issuer: C=GB, ST=Greater Manchester, L=Salford, O=COMODO CA Limited, CN=COMODO RSA Domain Validation Secure Server CA

我们显然得到了不同的证书:

SHA1 Fingerprint=C8:67:8E:DB:FD:BB:30:B5:3F:2D:7B:F9:66:B8:14:C6:2E:95:92:CE

当我向您的代码片段添加回调时:

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";

我看到这确实是 ASIO 正在为我处理的证书:

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

老实说,我不知道这两者怎么会不一致 - 即使 IP 地址似乎解析为相同。但这肯定与症状有关。

这里的问题是 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.

当您没有使用 SNI 连接时,badssl.com 服务器正在发送具有正确链的证书。如果您使用 SNI 连接,那么将发送自签名证书。您可以通过观察两个命令之间的区别,在命令行上使用 OpenSSL 验证这一点:

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 没有 API 添加 SNI,但我认为您可以通过在流中使用底层 OpenSSL API 和 native_handle() 方法来实现。它应该是这样的:

SSL_set_tlsext_host_name(socket.native_handle(), "self-signed.badssl.com");

我确实注意到您正在使用 sslv3_client 配置上下文。由于 SNI 是 TLS 扩展(即不是 SSLv3),如果不配置 TLS 上下文,这可能无法工作。