如何将 HttpClient 与任何 ssl 证书一起使用,无论它如何 "bad"

How to use HttpClient with ANY ssl cert, no matter how "bad" it is

我在仅用于抓取 public 数据的网络抓取工具中使用 Apache HttpClient

我希望它能够抓取证书无效的网站,无论证书多么无效。

我的爬虫不会传递任何用户名、密码等,也不会发送或接收任何敏感数据。

对于这个用例,我会抓取网站的 http 版本(如果存在),但有时它当然不存在。

如何使用 Apache 的 HttpClient 完成此操作?

我尝试了一些建议,例如 this one,但它们仍然因某些无效证书而失败,例如:

failed for url:https://dh480.badssl.com/, reason:java.lang.RuntimeException: Could not generate DH keypair
failed for url:https://null.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://rc4-md5.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://rc4.badssl.com/, reason:Received fatal alert: handshake_failure
failed for url:https://superfish.badssl.com/, reason:Connection reset

请注意,我已经尝试将 $JAVA_HOME/jre/lib/security/java.security 文件的 jdk.tls.disabledAlgorithms 设置为空,以确保这不是问题,但我仍然遇到与上述类似的失败。

我认为您所指的 post 非常接近需要完成的工作。您是否尝试过类似的东西:

HttpClientBuilder clientBuilder = HttpClientBuilder.create();
SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
sslContextBuilder.setSecureRandom(new java.security.SecureRandom());
try {
    sslContextBuilder.loadTrustMaterial(new TrustStrategy() {
        @Override
        public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
            return true;
        }
    });
    clientBuilder.setSSLContext(sslContextBuilder.build());
} catch (Throwable t) {
    Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Can't set ssl context", t);
}
CloseableHttpClient apacheHttpClient = clientBuilder.build();

我还没有尝试过这段代码,但希望它能奏效。

干杯

如果您可以使用 netty 等其他开源库,那么下面值得一试:

SslProvider provider = SslProvider.JDK;  // If you are not concerned about http2 / http1.1 then JDK provider will be enough
SSLContext sslCtx = SslContextBuilder.forClient()
                .sslProvider(provider)
                .trustManager(InsecureTrustManagerFactory.INSTANCE) // This will trust all certs
                ...  // Any other required parameters used for ssl context.e.g. protocols , ciphers etc.
                .build();

我使用以下版本的 netty 来信任任何具有上述代码的证书:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.29.Final</version>
</dependency>

我认为@nmorenor 的回答非常接近标准。另外,我会做的是显式启用 SSLv3(HttpClient 出于安全考虑,默认情况下会自动禁用它)并禁用主机名验证。

SSLContext sslContext = SSLContexts.custom()
        .loadTrustMaterial((chain, authType) -> true)
        .build();

CloseableHttpClient client = HttpClients.custom()
        .setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext,
                new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"},
                null,
                NoopHostnameVerifier.INSTANCE))
        .build();

你也可以用核心 jdk 做到这一点,但是 iirc,httpclient 也允许你设置 SSL 套接字工厂。

工厂定义并使用您通过信任管理器信任的 ssl 上下文。该经理根本不会验证证书链,如上所示 post.

您还需要一个 hostnameverifier 实例,它也会选择忽略证书主机名与 url 的主机(或 ip)的潜在不匹配。否则,即使盲目信任证书签署者,它仍然会失败。

我曾经将许多客户端堆栈转换为 'accept self-signed',这在大多数堆栈中都非常容易。最糟糕的情况是第 3 方库不允许选择 ssl 套接字工厂实例,而只允许选择其类名。在那种情况下,我使用了一个 ThreadLocalSSLSocketFactory,它不拥有任何实际工厂,只是简单地查找 threadlocal 以找到上层堆栈框架(您可以控制)已经准备好的工厂。当然,这仅在第 3 方库未在不同线程上执行工作时才有效。我知道可以告诉 http 客户端使用特定的 ssl 套接字工厂,所以这很容易。

还要花时间阅读 JSSE 文档,完全值得花时间阅读。

您的问题的简短答案是专门信任所有证书,将使用 TrustAllStrategy 并执行如下操作:

SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
sslContextBuilder.loadTrustMaterial(null, new TrustAllStrategy());
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
        sslContextBuilder.build());
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(
        socketFactory).build();

但是...无效的证书可能不是您的主要问题。 handshake_failure 可能由于多种原因而发生,但根据我的经验,这通常是由于 SSL/TLS 版本不匹配或密码套件协商失败。这并不意味着 ssl 证书是 "bad",它只是服务器和客户端之间的不匹配。您可以使用 Wireshark (more on that)

等工具准确查看握手失败的位置

虽然 Wireshark 可以很好地查看它的失败之处,但它不会帮助您提出解决方案。每当我过去调试 handshake_failures 时,我发现这个工具特别有用:https://testssl.sh/

您可以将该脚本指向任何失败的网站,以详细了解该目标上可用的协议以及您的客户端需要支持什么才能建立成功的握手。它还将打印有关证书的信息。

例如(仅显示 testssl.sh 输出的两部分):

./testssl.sh www.google.com
....
 Testing protocols (via sockets except TLS 1.2, SPDY+HTTP2) 

 SSLv2               not offered (OK)
 SSLv3               not offered (OK)
 TLS 1               offered
 TLS 1.1             offered
 TLS 1.2             offered (OK)
 ....
Server Certificate #1
   Signature Algorithm          SHA256 with RSA
   Server key size              RSA 2048 bits
   Common Name (CN)             "www.google.com"
   subjectAltName (SAN)         "www.google.com" 
   Issuer                       "Google Internet Authority G3" ("Google Trust Services" from "US")
   Trust (hostname)             Ok via SAN and CN (works w/o SNI)
   Chain of trust               "/etc/*.pem" cannot be found / not readable
   Certificate Expiration       expires < 60 days (58) (2018-10-30 06:14 --> 2019-01-22 06:14 -0700)
 ....
 Testing all 102 locally available ciphers against the server, ordered by encryption strength 
(Your /usr/bin/openssl cannot show DH/ECDH bits)

Hexcode  Cipher Suite Name (OpenSSL)       KeyExch.  Encryption Bits
------------------------------------------------------------------------
xc030   ECDHE-RSA-AES256-GCM-SHA384       ECDH       AESGCM    256       
xc02c   ECDHE-ECDSA-AES256-GCM-SHA384     ECDH       AESGCM    256       
xc014   ECDHE-RSA-AES256-SHA              ECDH       AES       256       
xc00a   ECDHE-ECDSA-AES256-SHA            ECDH       AES       256       
x9d     AES256-GCM-SHA384                 RSA        AESGCM    256       
x35     AES256-SHA                        RSA        AES       256       
xc02f   ECDHE-RSA-AES128-GCM-SHA256       ECDH       AESGCM    128       
xc02b   ECDHE-ECDSA-AES128-GCM-SHA256     ECDH       AESGCM    128       
xc013   ECDHE-RSA-AES128-SHA              ECDH       AES       128       
xc009   ECDHE-ECDSA-AES128-SHA            ECDH       AES       128       
x9c     AES128-GCM-SHA256                 RSA        AESGCM    128       
x2f     AES128-SHA                        RSA        AES       128       
x0a     DES-CBC3-SHA                      RSA        3DES      168 

因此使用此输出我们可以看到,如果您的客户端仅支持 SSLv3,则握手将失败,因为服务器不支持该协议。提供的协议不太可能是问题,但您可以通过获取已启用协议的列表来仔细检查 java 客户端支持的内容。您可以从上面的代码片段中提供 SSLConnectionSocketFactory 的覆盖实现,以获取 enabled/supported 协议和密码套件的列表,如下所示 (SSLSocket):

class MySSLConnectionSocketFactory extends SSLConnectionSocketFactory {
    @Override
    protected void prepareSocket(SSLSocket socket) throws IOException {
        System.out.println("Supported Ciphers" + Arrays.toString(socket.getSupportedCipherSuites()));
        System.out.println("Supported Protocols" + Arrays.toString(socket.getSupportedProtocols()));
        System.out.println("Enabled Ciphers" + Arrays.toString(socket.getEnabledCipherSuites()));
        System.out.println("Enabled Protocols" + Arrays.toString(socket.getEnabledProtocols()));
    }
}

经常遇到handshake_failure出现密码组协商失败的情况。为避免此错误,您的客户端支持的密码套件列表必须至少包含一个与服务器支持的密码套件列表中的密码套件匹配的项。

如果服务器需要基于 AES256 的密码套件,您可能需要 java 加密扩展 (JCE)。这些图书馆受国家限制,因此美国以外的人可能无法使用。

有关密码学限制的更多信息,如果您有兴趣:https://crypto.stackexchange.com/questions/20524/why-there-are-limitations-on-using-encryption-with-keys-beyond-certain-length