返回

解决SSL握手失败:getCipherSuite 返回 SSL_NULL_WITH_NULL_NULL

java

SSL 连接问题:getCipherSuite() 返回 SSL_NULL_WITH_NULL_NULL

在使用 Java 创建 SSL 服务器时,有时会遇到 SSLSocketgetSession().getCipherSuite() 方法返回 SSL_NULL_WITH_NULL_NULL 的情况。 这个值表示在 TLS 握手过程中没有成功协商出一个合适的加密套件。 如果没有协商出有效的加密套件,客户端与服务器就无法建立安全的加密连接,可能导致客户端在尝试连接时报握手失败的错误。下面就来深入探讨该问题以及对应的解决方案。

问题原因分析

SSL_NULL_WITH_NULL_NULL 错误通常表明 TLS/SSL 握手阶段没有成功完成,客户端和服务器之间无法就具体的加密算法和密钥交换达成一致。这个结果通常归咎于以下几个原因:

  1. 服务端证书配置错误 : 最常见的问题是服务端配置证书时出现错误,例如使用的是自签名证书、证书链不完整或者证书与所使用的私钥不匹配。此外, 也有可能密钥存储中的证书类型不是Private Key, 而变成了trusted Certificate,这也无法正常运行TLS 握手。
  2. 客户端不支持的加密套件 : 服务端启用的加密套件客户端并不支持。由于加密套件是由客户端和服务端协商选择的,当二者都找不到可以匹配的套件时,连接失败在所难免。
  3. SSL/TLS 协议版本不匹配 : 客户端和服务端支持的 SSL/TLS 协议版本不一致也会造成问题。客户端可能只支持旧版本的 SSL 协议,但服务端禁用了这些协议,或者是反过来,也会导致握手失败。
  4. 密钥算法或者签名算法的限制: 如果密钥算法或者签名算法服务端无法使用,TLS握手自然失败,比如,使用过期的 MD5WithRSA 算法签名。
  5. 中间防火墙拦截 : 有时候中间设备或防火墙可能会干扰 TLS 握手,阻断某些握手报文。

解决办法

根据上面分析的问题原因,可以采取以下步骤逐个排除问题,解决 getCipherSuite() 返回 SSL_NULL_WITH_NULL_NULL 的问题:

1. 检查并配置正确的服务端证书

首先确保服务端使用的证书有效且配置正确。如果使用的是自签名证书,需要保证证书中公钥、私钥匹配。还要确保KeyStore中存储的是 PrivateKey, 而不是trusted certificate。以下提供一份基于 BouncyCastle API 的 Java 自签名证书生成示例代码。

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Date;

public class CertUtil {

 public static X509Certificate generateSelfSignedCert(String domainName, KeyPair KPair)
            throws GeneralSecurityException, IOException {
         Security.addProvider(new BouncyCastleProvider());


        X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
                 new X500Name("CN=" + domainName), // issuer
                 BigInteger.valueOf(new SecureRandom().nextInt()),
                 new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30), // notBefore
                 new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10)), // notAfter
                 new X500Name("CN=" + domainName), // subject
                 new SubjectPublicKeyInfo(KPair.getPublic().getEncoded())); // subject Public key



        ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(KPair.getPrivate());
         X509CertificateHolder certHolder = certBuilder.build(signer);


         return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()).getCertificate(certHolder);
 }

    public static void main(String[] args) {
        try{
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            KeyPair KPair = keyPairGenerator.generateKeyPair();

           X509Certificate certificate = generateSelfSignedCert("localhost", KPair);
             // Verify and check the cert if need
           System.out.println(certificate);
            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(null);
            ks.setKeyEntry("localhost", KPair.getPrivate(), "password".toCharArray(), new java.security.cert.Certificate[] { certificate });

           javax.net.ssl.KeyManagerFactory kmf =
                   javax.net.ssl.KeyManagerFactory.getInstance("X509");
            kmf.init(ks, "password".toCharArray());
           javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance("TLSv1.3");

          sslContext.init(kmf.getKeyManagers(), null, null);

           javax.net.ssl.SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();

            javax.net.ssl.SSLServerSocket s = (javax.net.ssl.SSLServerSocket) ssf.createServerSocket(8888);

             s.setEnabledCipherSuites(s.getSupportedCipherSuites());
        //   SSLServerSocket s = (SSLServerSocket) ssf.createServerSocket(8888);
             System.out.println("Server started.");

           javax.net.ssl.SSLSocket c = (javax.net.ssl.SSLSocket)s.accept();
         System.out.println("Connection accpet");
        }
        catch (Exception ex){
            ex.printStackTrace();
        }

    }

}

此代码通过 SHA256withRSA 签名算法生成一个X.509证书,将证书的 私钥和公钥保存到 KeyStore, 并通过 KeyManagerFactory 管理证书信息。 KeyStore.setKeyEntry 保存PrivateKey 时, 会建立起 私钥和公钥证书的对应关系,使之能够在 TLS握手过程中成功传递信息。 值得强调的是这里使用的KeyStore 的类型与是否为TLS 握手失败,没有必然关系。 代码使用了 SHA256WithRSA,更加符合安全实践。 运行以上服务端代码,客户端使用以下 OpenSSL 命令,验证是否能够成功 TLS 握手:

openssl s_client -connect 127.0.0.1:8888
成功连接会看到如下提示,  cipher  Suite 为  `TLS_AES_256_GCM_SHA384`,  而不是  `SSL_NULL_WITH_NULL_NULL`
    CONNECTED(00000003)
    TLSv1.3  .....
    Cipher    : TLS_AES_256_GCM_SHA384

2. 配置服务端可用的加密套件

服务端应该配置支持的加密套件列表,这有助于客户端选择合适的加密套件。可以优先使用符合安全的 TLS 加密套件,以下给出一种服务端选择套件的方法:

s.setEnabledCipherSuites(new String[] {
   "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
   "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
   "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
   "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
   "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
  });

此段代码列出了一些常见的安全的加密套件。 使用 setEnabledCipherSuites 指定的加密套件集合之后,服务器只使用这些套件进行协商。客户端也需要使用 openssl 进行类似的指定加密套件集合:

openssl s_client -connect 127.0.0.1:8888 -cipher "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"

3. 启用TLS 1.3 协议 (或更高的版本)

如果可能,在服务端和客户端都尝试启用 TLS 1.3 (或更高的版本), 新的协议支持更现代化的加密算法,同时也具有更好的安全性:

SSLContext sc = SSLContext.getInstance("TLSv1.3");

4. 排查中间设备干扰

某些中间设备或者防火墙可能会对 SSL/TLS 握手造成干扰。可以通过关闭防火墙、临时移除代理或者其他可能干扰 TLS 握手的网络设备的方式,进一步排查问题。

5. 更新 Java 和 OpenSSL 版本

如果上面的操作都没能解决问题,那么更新客户端和服务端的Java和 OpenSSL 版本是一个可行的方向。使用新版本的TLS 协议和加密套件,往往可以解决很多老版本存在的问题。

安全建议

  1. 始终使用高强度的加密算法,例如 AES-256-GCM 或 ChaCha20-Poly1305,而不是老旧的加密算法。
  2. 定期更新你的证书,以确保您的服务始终符合行业安全最佳实践。
  3. 避免使用自签名证书。对于生产环境,应考虑从可信的 CA 机构购买证书。
  4. 不要在服务端启用客户端并不需要的过多的加密套件,确保加密套件的安全性。
  5. 保持客户端和服务端的操作系统和应用软件在最新版本,以获得最新的安全补丁和性能优化。

通过遵循上述方法和建议,能更好地解决在构建安全通信服务过程中可能出现的 SSL_NULL_WITH_NULL_NULL 问题,从而构建更安全稳定的系统。