为什么SSL握手会出现“Could not generate DH keypair”异常?

150

当我与某些IRC服务器建立SSL连接时(但不是所有的 - 可能是因为服务器的首选加密方法),我会得到以下异常:

Caused by: java.lang.RuntimeException: Could not generate DH keypair
    at com.sun.net.ssl.internal.ssl.DHCrypt.<init>(DHCrypt.java:106)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverKeyExchange(ClientHandshaker.java:556)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:183)
    at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:593)
    at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:529)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:893)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1138)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1165)
    ... 3 more

最终原因:

Caused by: java.security.InvalidAlgorithmParameterException: Prime size must be multiple of 64, and can only range from 512 to 1024 (inclusive)
    at com.sun.crypto.provider.DHKeyPairGenerator.initialize(DashoA13*..)
    at java.security.KeyPairGenerator$Delegate.initialize(KeyPairGenerator.java:627)
    at com.sun.net.ssl.internal.ssl.DHCrypt.<init>(DHCrypt.java:100)
    ... 10 more

一个展示此问题的服务器实例是aperture.esper.net:6697(这是IRC服务器)。一个没有展示此问题的服务器实例是kornbluth.freenode.net:6697。[毫不意外,每个网络上的所有服务器分享相同的行为。]

我的代码(如注所述,在连接到某些SSL服务器时可以正常工作)是:

    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustAllCerts, new SecureRandom());
    s = (SSLSocket)sslContext.getSocketFactory().createSocket();
    s.connect(new InetSocketAddress(host, port), timeout);
    s.setSoTimeout(0);
    ((SSLSocket)s).startHandshake();

最后一个startHandshake引发了异常。是的,'trustAllCerts' 有些神奇;该代码强制SSL系统不验证证书。(所以……这不是证书的问题。)

显然,esper服务器配置错误是一种可能性,但我搜索了一下,没有找到其他参考信息表明有人在使用esper的SSL端口时遇到问题,并且从命令行使用 'openssl' 连接成功(见下文)。所以我想知道这是否是Java默认SSL支持的限制或者其它原因。有什么建议吗?

以下是使用 'openssl' 连接 aperture.esper.net 6697 时发生的情况:

~ $ openssl s_client -connect aperture.esper.net:6697
CONNECTED(00000003)
depth=0 /C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
verify error:num=18:self signed certificate
verify return:1
depth=0 /C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
verify return:1
---
Certificate chain
 0 s:/C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
   i:/C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
---
Server certificate
-----BEGIN CERTIFICATE-----
[There was a certificate here, but I deleted it to save space]
-----END CERTIFICATE-----
subject=/C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
issuer=/C=GB/ST=England/L=London/O=EsperNet/OU=aperture.esper.net/CN=*.esper.net/emailAddress=support@esper.net
---
No client certificate CA names sent
---
SSL handshake has read 2178 bytes and written 468 bytes
---
New, TLSv1/SSLv3, Cipher is DHE-RSA-AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : DHE-RSA-AES256-SHA
    Session-ID: 51F1D40A1B044700365D3BD1C61ABC745FB0C347A334E1410946DCB5EFE37AFD
    Session-ID-ctx: 
    Master-Key: DF8194F6A60B073E049C87284856B5561476315145B55E35811028C4D97F77696F676DB019BB6E271E9965F289A99083
    Key-Arg   : None
    Start Time: 1311801833
    Timeout   : 300 (sec)
    Verify return code: 18 (self signed certificate)
---

正如注释所述,经过这一番操作后,它确实成功连接了,这是我的Java应用程序做不到的。

如果有必要提及,我正在使用OS X 10.6.8,Java版本为1.6.0_26。


3
原因似乎是这样的:质数大小必须是64的倍数,且只能在512到1024之间(含)范围内。 不清楚服务器发送了什么大小,以及规范对此的规定。 - Paŭlo Ebermann
@PaŭloEbermann:实际上,您可以在问题的openssl输出中查看服务器使用的大小:“Cipher is DHE-RSA-AES256-SHA,Server public key is 2048 bit”。而2048>1024 :-)。 - sleske
@sleske:并不完全是这样。服务器公钥(大小)是证书中的密钥,一直都是。2011年的s_client根本没有显示临时密钥;而2015年及以上版本的1.0.2则将其显示为几行更高的服务器临时密钥。尽管一个好的服务器通常应该使DHE大小与RSA认证大小相同。 - dave_thompson_085
22个回答

123
问题在于质数的大小。Java可接受的最大大小为1024位。这是一个已知问题(参见JDK-6521495)。
我链接的错误报告提到了使用BouncyCastle的JCE实现的workaround。希望这对您有用。 更新 这被报告为错误JDK-7044060并最近得到修复。
请注意,然而,限制仅提高到2048位。对于大于2048位的大小,有JDK-8072452 - 删除DH密钥的最大质数大小;该修复似乎适用于9。

17
请所有拥有Sun开发者网络账户的人为这个bug投票。 - Paŭlo Ebermann
1
谢谢。鉴于存在请求更大尺寸的服务器,这似乎是一个相当严重的问题!:( 我尝试了BouncyCastle;如果将其设置为首选提供程序,则会崩溃并出现不同的异常(叹息),我无法看到明显的方法仅用于DH。然而,我找到了一种替代解决方案,我将其添加为新答案。(它不太美观。) - sam
3
很好。这个问题已经在新版本的Java中得到了修复。但我的问题是关于使用旧版本的Java。当我使用旧版本时,有时它可以工作,有时它会出现上述异常。为什么会出现如此随机的行为?如果这是Java中的一个错误,那么我想它应该永远不会工作? - Popeye
2
2048修复程序已经被移植到IcedTea 2.5.3中。 较新版本的IcedTea将其增加到4096。 - fuzzyTew
1
问题在于DH素数的大小。Java接受的最大可接受大小为2048位。在等待Oracle扩展限制的同时,您可以使用具有扩展限制的Excelsior Jet进行编译。 - user1332994
显示剩余6条评论

70

“Java密码扩展(JCE)无限制权限策略文件”方案对我无效,但使用BouncyCastle的JCE提供程序建议有效。

以下是我在Mac OSC 10.7.5上使用Java 1.6.0_65-b14-462所采取的步骤:

1)下载这些jar文件:

2)将这些jar文件移动到$JAVA_HOME/lib/ext目录下

3)编辑$JAVA_HOME/lib/security/java.security,添加以下内容: security.provider.1=org.bouncycastle.jce.provider.BouncyCastleProvider

重新启动使用JRE的应用程序并进行尝试。


13
security.provider.2=org.bouncycastle.jce.provider.BouncyCastleProvider表现更佳,但将其放在1号位置会导致默认软件出现错误。 - TinusSky
1
这对我也有效,但我已经动态添加了一个提供程序。有关详细信息,请参阅我的答案 - v.ladynev
非常感谢,这对我很有帮助,并且是成功构建Tomcat 7.0.78源代码所必需的。 - phillipuniverse
@mjj1409,在Java 1.5中,我们没有$JAVA_HOME/lib/security路径。 - Mostafa

16

这是我的解决方案(Java 1.6),也想知道为什么我要这样做:

我从javax.security.debug=ssl中注意到,有时使用的加密套件是TLS_DHE_...,有时是TLS_ECDHE_.... 如果选择了TLS_ECDHE_,大部分时间都能正常工作,但并不总是如此,因此即使添加BouncyCastle提供程序也不可靠(每隔一段时间或更长时间就会失败)。我猜在Sun SSL实现中的某个地方,它有时会选择DHE,有时会选择ECDHE。

因此,此处发布的解决方案依赖于完全删除TLS_DHE_密码。 注意:解决方案不需要BouncyCastle。

因此,通过以下方式创建服务器认证文件:

echo |openssl s_client -connect example.org:443 2>&1 |sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'

保存这个,因为稍后会用到它,以下是一个 SSL HTTP GET 的解决方案,不包括 TLS_DHE_ 密码套件。

package org.example.security;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.apache.log4j.Logger;

public class SSLExcludeCipherConnectionHelper {

    private Logger logger = Logger.getLogger(SSLExcludeCipherConnectionHelper.class);

    private String[] exludedCipherSuites = {"_DHE_","_DH_"};

    private String trustCert = null;

    private TrustManagerFactory tmf;

    public void setExludedCipherSuites(String[] exludedCipherSuites) {
        this.exludedCipherSuites = exludedCipherSuites;
    }

    public SSLExcludeCipherConnectionHelper(String trustCert) {
        super();
        this.trustCert = trustCert;
        //Security.addProvider(new BouncyCastleProvider());
        try {
            this.initTrustManager();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private void initTrustManager() throws Exception {
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        InputStream caInput = new BufferedInputStream(new FileInputStream(trustCert));
        Certificate ca = null;
        try {
            ca = cf.generateCertificate(caInput);
            logger.debug("ca=" + ((X509Certificate) ca).getSubjectDN());
        } finally {
            caInput.close();
        }

        // Create a KeyStore containing our trusted CAs
        KeyStore keyStore = KeyStore.getInstance("jks");
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        // Create a TrustManager that trusts the CAs in our KeyStore
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);
    }

    public String get(URL url) throws Exception {
        // Create an SSLContext that uses our TrustManager
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, tmf.getTrustManagers(), null);
        SSLParameters params = context.getSupportedSSLParameters();
        List<String> enabledCiphers = new ArrayList<String>();
        for (String cipher : params.getCipherSuites()) {
            boolean exclude = false;
            if (exludedCipherSuites != null) {
                for (int i=0; i<exludedCipherSuites.length && !exclude; i++) {
                    exclude = cipher.indexOf(exludedCipherSuites[i]) >= 0;
                }
            }
            if (!exclude) {
                enabledCiphers.add(cipher);
            }
        }
        String[] cArray = new String[enabledCiphers.size()];
        enabledCiphers.toArray(cArray);

        // Tell the URLConnection to use a SocketFactory from our SSLContext
        HttpsURLConnection urlConnection =
            (HttpsURLConnection)url.openConnection();
        SSLSocketFactory sf = context.getSocketFactory();
        sf = new DOSSLSocketFactory(sf, cArray);
        urlConnection.setSSLSocketFactory(sf);
        BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        String inputLine;
        StringBuffer buffer = new StringBuffer();
        while ((inputLine = in.readLine()) != null) 
            buffer.append(inputLine);
        in.close();

        return buffer.toString();
    }

    private class DOSSLSocketFactory extends javax.net.ssl.SSLSocketFactory {

        private SSLSocketFactory sf = null;
        private String[] enabledCiphers = null;

        private DOSSLSocketFactory(SSLSocketFactory sf, String[] enabledCiphers) {
            super();
            this.sf = sf;
            this.enabledCiphers = enabledCiphers;
        }

        private Socket getSocketWithEnabledCiphers(Socket socket) {
            if (enabledCiphers != null && socket != null && socket instanceof SSLSocket)
                ((SSLSocket)socket).setEnabledCipherSuites(enabledCiphers);

            return socket;
        }

        @Override
        public Socket createSocket(Socket s, String host, int port,
                boolean autoClose) throws IOException {
            return getSocketWithEnabledCiphers(sf.createSocket(s, host, port, autoClose));
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return sf.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            if (enabledCiphers == null)
                return sf.getSupportedCipherSuites();
            else
                return enabledCiphers;
        }

        @Override
        public Socket createSocket(String host, int port) throws IOException,
                UnknownHostException {
            return getSocketWithEnabledCiphers(sf.createSocket(host, port));
        }

        @Override
        public Socket createSocket(InetAddress address, int port)
                throws IOException {
            return getSocketWithEnabledCiphers(sf.createSocket(address, port));
        }

        @Override
        public Socket createSocket(String host, int port, InetAddress localAddress,
                int localPort) throws IOException, UnknownHostException {
            return getSocketWithEnabledCiphers(sf.createSocket(host, port, localAddress, localPort));
        }

        @Override
        public Socket createSocket(InetAddress address, int port,
                InetAddress localaddress, int localport) throws IOException {
            return getSocketWithEnabledCiphers(sf.createSocket(address, port, localaddress, localport));
        }

    }
}

最后,以下是如何使用它(如果certFilePath是从openssl保存的证书路径):

try {
            URL url = new URL("https://www.example.org?q=somedata");            
            SSLExcludeCipherConnectionHelper sslExclHelper = new SSLExcludeCipherConnectionHelper(certFilePath);
            logger.debug(
                    sslExclHelper.get(url)
            );
        } catch (Exception ex) {
            ex.printStackTrace();
        }

11
刚刚测试了你的解决方案,它按照预期工作。谢谢。其实只需在“JDK_HOME/jre/lib/security/java.security”文件中添加“jdk.tls.disabledAlgorithms=DHE, ECDHE”,也可以避免所有这些代码。 - Ludovic Guillaume
4
只需禁用DHE即可解决我的问题:jdk.tls.disabledAlgorithms=DHE。使用的是1.7.0_85-b15版本。 - stephan f
1
在JDK_HOME/jre/lib/security/java.security中添加了jdk.tls.disabledAlgorithms=DHE, ECDHE,结果一切正常!谢谢!使用的是1.7.0_79版本。 - Eric
2
不要禁用ECDHE。它与DHE不同,甚至不使用质数。 - kubanczyk
1
有人可以告诉我如何获取'certFilePath'吗?我卡了一个星期了。@Zsozso - user1172490

14

上面的答案是正确的,但就解决方法而言,当我将BouncyCastle实现设置为首选提供程序时,我遇到了问题:

java.lang.ArrayIndexOutOfBoundsException: 64
    at com.sun.crypto.provider.TlsPrfGenerator.expand(DashoA13*..)

我在一个论坛帖子中找到了相关讨论,但没有提到解决方案。 http://www.javakb.com/Uwe/Forum.aspx/java-programmer/47512/TLS-problems

我找到了一种适用于我的情况的替代解决方案,尽管我对它并不满意。解决方案是将Diffie-Hellman算法设置为不可用状态。然后,假设服务器支持另一种算法,则在正常协商过程中将选择该算法。显然,缺点是如果有人以某种方式找到只支持1024位或更低位数的Diffie-Hellman算法的服务器,则意味着它将无法在之前的位置工作。

这里是给定SSLSocket的代码(在连接之前):

List<String> limited = new LinkedList<String>();
for(String suite : ((SSLSocket)s).getEnabledCipherSuites())
{
    if(!suite.contains("_DHE_"))
    {
        limited.add(suite);
    }
}
((SSLSocket)s).setEnabledCipherSuites(limited.toArray(
    new String[limited.size()]));

很糟糕。


2
很遗憾这是唯一的方法 :(. 那个问题自从07年就一直存在了。奇怪的是在4年内什么都没有做。 - Vivin Paliath
我对Java安全性还不熟悉,请问我应该在哪里编写这段代码? - Shashank
1
我尝试了这个线程中的每一个解决方案,而这是唯一一个能够在我的IBM JVM(ibm-java-s390x-60)上正常工作的。 - Gianluca Greco
1
@Sam,((SSLSocket)s) 中的 s 变量是什么? - Mostafa

13
您可以在jdk中完全禁用DHE,编辑jre/lib/security/java.security并确保DHE已禁用,例如:

jdk.tls.disabledAlgorithms=SSLv3, DHE


4
只有在 JDK 1.7 及更高版本中才可用。 - hoangthienan
问题已经为我解决,使用的是JDK 1.8.0_144-b01版本。 - MrSmith42

12
您可以动态地安装提供程序:
1)下载这些JAR文件:
  • bcprov-jdk15on-152.jar
  • bcprov-ext-jdk15on-152.jar
2)将JAR文件复制到WEB-INF/lib(或您的类路径)。
3)动态添加提供程序:

import org.bouncycastle.jce.provider.BouncyCastleProvider;

...

Security.addProvider(new BouncyCastleProvider());


这个解决方案很好用,而且非常适合我的情况,因为我只想在项目级别上进行更改,而不是在服务器/环境级别上进行更改。谢谢。 - ishanbakshi
谢谢!这对我有用。在我的情况下,是itext导入bcp 1.4导致向谷歌发送电子邮件失败。 - Japheth Ongeri - inkalimeva
我在使用Java版本1.6.0_27时遇到了javax.net.ssl.SSLHandshakeException: Unsupported curveId: 29的异常。 - Another coder

8

7

你可能存在错误的Maven依赖关系。 必须在Maven依赖层次结构中找到这些库:

bcprov-jdk14, bcpkix-jdk14, bcmail-jdk14

如果您遇到这种依赖性相关的错误,应该执行以下步骤:
添加相关依赖项:
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcmail-jdk15on</artifactId>
    <version>1.59</version>
</dependency>

从包含错误依赖项的构件中排除这些依赖项,我的情况是:

<dependency>
    <groupId>com.lowagie</groupId>
    <artifactId>itext</artifactId>
    <version>2.1.7</version>
    <exclusions>
        <exclusion>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bctsp-jdk14</artifactId>                
        </exclusion>
        <exclusion>
            <groupId>bouncycastle</groupId>
            <artifactId>bcprov-jdk14</artifactId>               
        </exclusion>
        <exclusion>
            <groupId>bouncycastle</groupId>
            <artifactId>bcmail-jdk14</artifactId>               
        </exclusion>
    </exclusions>       
</dependency>

6
如果您正在使用jdk1.7.0_04,请升级到jdk1.7.0_21。该更新已经解决了这个问题。

2
我刚刚下载了Java SE Development Kit 7u25,并根据我编写的小程序来确定最大支持的DH大小(https://gist.github.com/ppelleti/6275452),结果仍然是1024。 - user2666524
2
问题在JDK 1.7.0_25版本仍然存在。 - Sébastien Vanmechelen
更新JRE对我有用。从6.22升级到7.59。问题解决了。 - Elazaron
1
@Shashank - 我升级到JDK 1.8.0_73后问题解决了。 - dgoverde
1
DHKeyPairs比1024位更长的位数从JDK 1.7.0_91开始引入,但不对公众开放。 - Another coder

5
尝试从Java下载网站下载“Java加密扩展(JCE)无限制权限策略文件”,并将文件替换为您的JRE中的文件。
这对我有用,而且我甚至不需要使用BouncyCastle-标准Sun JCE能够连接到服务器。
PS. 在更改策略文件之前,当我尝试使用BouncyCastle时,我遇到了相同的错误(ArrayIndexOutOfBoundsException: 64),因此我们的情况似乎非常相似。

你做了其他的事情吗?还是只是把这两个文件复制到文件夹里了? - Joergi
已经有一段时间了,但据我所记,那就是我需要做的全部。除了之后重新启动任何正在运行的Java进程。 - mjomble

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