【问题标题】:How to use HttpClient with ANY ssl cert, no matter how "bad" it is如何将 HttpClient 与任何 ssl 证书一起使用,无论它有多“糟糕”
【发布时间】:2018-11-15 14:06:57
【问题描述】:

我在仅用于抓取公共数据的网络爬虫中使用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 设置为空,以确保这不是问题,但我仍然会遇到上述故障。

【问题讨论】:

  • 连接时无法与无法进行 DH 密钥交换或重置连接的服务器进行通信。你不能改变那个客户端。
  • 您是否为 Apache 的 HttpClient 尝试过这个示例? stackoverflow.com/a/50274496/3523579

标签: java apache-httpclient-4.x


【解决方案1】:

对您的问题的简短回答,即特别信任所有证书,是使用 TrustAllStrategy 并执行以下操作:

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

但是...无效的证书可能不是您的主要问题。发生握手失败的原因有很多,但根据我的经验,这通常是由于 SSL/TLS 版本不匹配或密码套件协商失败。这并不意味着 ssl 证书是“坏的”,它只是服务器和客户端之间的不匹配。您可以使用 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 客户端支持的内容。您可以从上面的代码 sn-p 提供 SSLConnectionSocketFactory 的重写实现,以获取启用/支持的协议和密码套件列表,如下所示 (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

【讨论】:

  • 尽管我选择了这个作为答案,并且我明白最终服务器可以控制它发送给客户端的内容,但我觉得没有 100% 的方式可以说“忽略所有安全问题服务器,请通过我们损坏的 https 合同向我发送此响应,这并不比 http 更好,但是,无论如何!”。我想这不是常态 - 像接受 http 一样接受 https,但我敢肯定还有其他人出于类似的原因也想做同样的事情!
  • TrustAllStrategy 确实告诉服务器忽略所有真实性问题,但连接仍然需要成功的握手,因为那是 SSL 协议。没有握手就不再是 HTTPS ......基本上你想要做的是将连接降级为使用 http 而不是 https 是的,这取决于服务器是否支持单独的端口(即80 而不是 443)。如果服务器通过 http 而不是 https 提供网页,则该网站不再被视为安全网站,因此许多人甚至不再支持此网站。
【解决方案2】:

我认为您所指的帖子非常接近需要完成的工作。您是否尝试过类似的方法:

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

我没有尝试过这段代码,但希望它可以工作。

干杯

【讨论】:

    【解决方案3】:

    如果您可以使用 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>
    

    【讨论】:

    • 不需要任何额外的库,因为 org.apache.http.conn.ssl.TrustAllStrategy 可以使用并且是 httpclient 的一部分。
    【解决方案4】:

    我认为@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();
    

    【讨论】:

    • 正如@Friwi 在 cmets 中已经指出的那样,您将无法使用此处理 TLS 的密钥交换阶段的问题。为了能够处理这个问题,您需要一个自定义 (SSL)Socket 实现。
    • @dpr 如果想要控制JSSE 提供的API 之外的控制,是的,可能需要自定义JSSE 提供程序。
    【解决方案5】:

    你也可以用core jdk来做,但是iirc,httpclient也允许你设置SSL Socket Factory。

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

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

    我曾经将许多客户端堆栈转换为“接受自签名”,这在大多数堆栈中都非常容易。更糟糕的情况是 3rd 方库不允许选择 ssl 套接字工厂实例,而只能选择其类名。在这种情况下,我使用 ThreadLocalSSLSocketFactory,它不拥有任何实际工厂,而只是查找 threadlocal 以找到上层堆栈帧(您可以控制)准备好的一个。这仅在第 3 方库当然没有在不同线程上进行工作时才有效。我知道可以告诉 http 客户端使用特定的 ssl 套接字工厂,所以这很容易。

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

    【讨论】:

      猜你喜欢
      • 2011-02-16
      • 1970-01-01
      • 2017-06-12
      • 1970-01-01
      • 1970-01-01
      • 2013-08-03
      • 2012-09-19
      • 2021-10-23
      • 1970-01-01
      相关资源
      最近更新 更多