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

3

我正在使用Boost's 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()并从回调中返回false,则连接会失败,但preverified似乎总是为真,即使对于https://self-signed.badssl.com/也是如此。这肯定不对吧?


怪事。我会留意这篇文章以防别人发现真正的原因。太神奇了。每天都能学到新东西。 - sehe
2个回答

9
这里的问题在于服务器名称指示(SNI):
引入了TLS计算机网络协议的扩展,客户端在握手过程开始时指示其尝试连接的主机名。这允许服务器在同一IP地址和TCP端口号上呈现多个证书,并因此允许在同一IP地址上提供多个安全(HTTPS)网站(或任何其他通过TLS的服务),而无需所有这些站点使用相同的证书。
当您没有使用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 没有添加 SNI 的 API,但我认为您可以通过使用底层 OpenSSL API 和流的 native_handle() 方法来实现它。大概是这样的:

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

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

我很高兴学到了这个。我原本以为它必须在协议协商/握手阶段中完成。但这对我来说是新的。非常有用的信息。 - sehe
@sehe,我知道你不知道的事情真是少见。虽然我确实知道解决方案,但说实话,如果没有你的答案,我可能无法理解问题。 - rhashimoto
啊,我懂了!而且,我注意到sslv3_client不正确。 - Timmmm

2

这很奇怪。

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

当使用浏览器或此在线工具https://www.digicert.com/help/进行检查时,甚至确认了这一点。它显示证书为:

SHA1 Thumbprint = 079B3259D07C4DE2A1CE0EF4A5B5599D3B2D62EA

然而,当我尝试使用我的Shell时,例如

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

我们得到了一个明显不同的证书,其“真实”颁发者为:颁发者:C=GB,ST=大曼彻斯特,L=索尔福德,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地址似乎解析到相同的地方。但这显然与症状有关。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接