解决SSL握手失败:getCipherSuite 返回 SSL_NULL_WITH_NULL_NULL
2025-01-15 02:06:35
SSL 连接问题:getCipherSuite()
返回 SSL_NULL_WITH_NULL_NULL
在使用 Java 创建 SSL 服务器时,有时会遇到 SSLSocket
的 getSession().getCipherSuite()
方法返回 SSL_NULL_WITH_NULL_NULL
的情况。 这个值表示在 TLS 握手过程中没有成功协商出一个合适的加密套件。 如果没有协商出有效的加密套件,客户端与服务器就无法建立安全的加密连接,可能导致客户端在尝试连接时报握手失败的错误。下面就来深入探讨该问题以及对应的解决方案。
问题原因分析
SSL_NULL_WITH_NULL_NULL
错误通常表明 TLS/SSL 握手阶段没有成功完成,客户端和服务器之间无法就具体的加密算法和密钥交换达成一致。这个结果通常归咎于以下几个原因:
- 服务端证书配置错误 : 最常见的问题是服务端配置证书时出现错误,例如使用的是自签名证书、证书链不完整或者证书与所使用的私钥不匹配。此外, 也有可能密钥存储中的证书类型不是Private Key, 而变成了trusted Certificate,这也无法正常运行TLS 握手。
- 客户端不支持的加密套件 : 服务端启用的加密套件客户端并不支持。由于加密套件是由客户端和服务端协商选择的,当二者都找不到可以匹配的套件时,连接失败在所难免。
- SSL/TLS 协议版本不匹配 : 客户端和服务端支持的 SSL/TLS 协议版本不一致也会造成问题。客户端可能只支持旧版本的 SSL 协议,但服务端禁用了这些协议,或者是反过来,也会导致握手失败。
- 密钥算法或者签名算法的限制: 如果密钥算法或者签名算法服务端无法使用,TLS握手自然失败,比如,使用过期的 MD5WithRSA 算法签名。
- 中间防火墙拦截 : 有时候中间设备或防火墙可能会干扰 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 协议和加密套件,往往可以解决很多老版本存在的问题。
安全建议
- 始终使用高强度的加密算法,例如 AES-256-GCM 或 ChaCha20-Poly1305,而不是老旧的加密算法。
- 定期更新你的证书,以确保您的服务始终符合行业安全最佳实践。
- 避免使用自签名证书。对于生产环境,应考虑从可信的 CA 机构购买证书。
- 不要在服务端启用客户端并不需要的过多的加密套件,确保加密套件的安全性。
- 保持客户端和服务端的操作系统和应用软件在最新版本,以获得最新的安全补丁和性能优化。
通过遵循上述方法和建议,能更好地解决在构建安全通信服务过程中可能出现的 SSL_NULL_WITH_NULL_NULL
问题,从而构建更安全稳定的系统。