如何使用具有 480 位密钥长度的 Diffie-Hellman 获取 SSL 页面?

How can I fetch an SSL page using Diffie-Hellman with a 480-bit key length?

我正在尝试获取使用 480 位密钥长度的 DH 的页面,但我无法使用 Apache HTTP 客户端执行此操作。如果我将异步 HTTP 客户端与 JDK 提供程序一起使用,我可以成功获取页面。

我知道 JDK 对最大大小有限制,但这个问题似乎是相反的。密钥大小太小,不是 64 的倍数。

我写了一个 SSCCE 来说明这种行为,经过两天的寻找,我已经无计可施了。

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

import javax.net.ssl.SSLContext;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.junit.Assert;
import org.junit.Test;

import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.providers.jdk.JDKAsyncHttpProvider;

public class DH480Test extends Assert {

    private final String URL = "https://dh480.badssl.com/";

    @Test
    public void testNingHTTPClientWithJDKProvider() {
        AsyncHttpClientConfig config = new AsyncHttpClientConfig.Builder().build();
        AsyncHttpClient client = new AsyncHttpClient(new JDKAsyncHttpProvider(config), config);

        try {
            client.prepareGet(URL).execute().get();
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }

    @Test
    public void testApacheHTTClient() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, null, null);

        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("https", new SSLConnectionSocketFactory(sslContext))
                .register("http", new PlainConnectionSocketFactory())
                .build();
        HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager(registry);
        HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setConnectionManager(ccm);

        HttpClient client = builder.build();

        try {
            HttpGet get = new HttpGet(URL);
            HttpResponse r = client.execute(get);
            EntityUtils.consume(r.getEntity());
        } catch (Exception e) {
            e.printStackTrace();
            fail();
        }
    }
}

这是失败测试的堆栈跟踪。

javax.net.ssl.SSLException: java.lang.RuntimeException: Could not generate DH keypair
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1906)
    at sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1889)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1410)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:290)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:259)
    at org.apache.http.impl.conn.HttpClientConnectionOperator.connect(HttpClientConnectionOperator.java:125)
    at org.apache.http.impl.conn.BasicHttpClientConnectionManager.connect(BasicHttpClientConnectionManager.java:318)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:363)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:219)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:195)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:86)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:108)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:106)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:57)
    at DH480Test.testApacheHTTClient(DH480Test.java:59)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=11=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.RuntimeException: Could not generate DH keypair
    at sun.security.ssl.DHCrypt.<init>(DHCrypt.java:142)
    at sun.security.ssl.DHCrypt.<init>(DHCrypt.java:114)
    at sun.security.ssl.ClientHandshaker.serverKeyExchange(ClientHandshaker.java:708)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:268)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    ... 41 more
Caused by: java.security.InvalidAlgorithmParameterException: Prime size must be multiple of 64, and can only range from 512 to 2048 (inclusive)
    at com.sun.crypto.provider.DHKeyPairGenerator.initialize(DHKeyPairGenerator.java:120)
    at java.security.KeyPairGenerator$Delegate.initialize(KeyPairGenerator.java:674)
    at sun.security.ssl.DHCrypt.<init>(DHCrypt.java:128)
    ... 49 more

java.lang.AssertionError
    ...

这是另一个独立测试,展示了我能够使用默认 JDK 网络库获取页面。这是异步 http 客户端使用的底层网络实现。这是我的 JVM 详细信息

➜  ~  java -version
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)

@Test
public void testHTTPURLConnection() throws IOException {
    try {
        java.net.URL url = new URL(URL);
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setHostnameVerifier((s, sslSession) -> true);
        connection.setRequestMethod("GET");
        connection.setReadTimeout(15 * 1000);
        connection.connect();
        System.out.println(IOUtils.toString(connection.getInputStream()));
    } catch (Exception e) {
        fail();
    }
}

我不是专家,也从未在 java 中使用过 DH,但是在查看了您的日志并对 DHKeyPairGenerator Grepcode 中的代码进行了一些探索之后,initialize 函数,如果 Prime size < 512 它总是抛出异常,因为它是用代码编写的,

83         if ((keysize < 512) || (keysize > 1024) || (keysize % 64 != 0)) {
84             throw new InvalidParameterException("Keysize must be multiple "
85                                                 + "of 64, and can only range "
86                                                 + "from 512 to 1024 "
87                                                 + "(inclusive)");
88         }

118    if ((pSize < 512) || (pSize > 1024) ||
119            (pSize % 64 != 0)) {
120            throw new InvalidAlgorithmParameterException
121                ("Prime size must be multiple of 64, and can only range "
122                 + "from 512 to 1024 (inclusive)");
123        }

因此可以肯定的是,您不能将这些标准库用于 480 位密钥长度。

它可能在您需要探索的其他一些库中可用,我们使用 bouncycastle 库来做一些可能具有这些功能的加密内容。

我几乎没有研究并在 bouncycastle 包中发现了相同的 DHKeyPairGenerator class,没有约束和检查密钥大小。

免责声明:我是 AsyncHttpClient 的维护者(请不要称它为 Ning)。

有趣的事实:当 运行 JDK8 时,这确实适用于 AHC 的 JDK 提供程序,但不适用于 JDK7!

现在:AHC JDK 提供程序(它只是一个玩具,在 AHC2 中即将消失,您不应该使用它)归结为 Http(s)UrlConnection,因此它有望获得相同的结果结果。

然后,基于 NIO 的客户端(例如其他 AHC 提供程序)和 Apache HttpComponents 无法使用 HttpsUrlConnection 使用的底层 HTTPS 组件,因为它们不是 public!

所以他们不得不直接使用javax.net.ssl.SSLEngine。但是,此 API 适用于通用 TLS,不适用于构建在其之上的协议,例如 HTTPS。

例如,在 JDK7 之前,这会对所有基于 Java NIO 的 HTTP 客户端造成巨大的安全问题,因为它们无法正确执行主机名验证。主机名验证特定于 HTTPS,因此您可以将 HostnameVerifier 传递给 HttpsUrlConnection(特定于 HTTPS),但不能传递给 SSLEngine(通用 TLS)。

现在,自 JDK7 以来,我们终于可以在 SSLEngine 上配置协议(HTTPS 或 LDAP),以便它处理 HTTPS 的特定行为。但是,与使用 HttpsUrlConnection 相比,您的控制权仍然较少。

在这里,您似乎发现了一个 JDK 错误,并且启用了 HTTPS 协议的 HttpsUrlConnection 和 SSLEngine 的行为方式不同。

例如,无论我做什么(无论我启用什么密码套件),我都无法获得 BadSSL.com 服务器选择 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,这是 HttpsUrlConnection 选择的一个。它总是选择 TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 代替(没有椭圆曲线)。启用 TLS 调试时请参阅 ServerHello (-Djavax.net.debug=all)。