Java11服务端基于SNI选择对应证书链异常问题及解决方案
bash-4.4$ java -version openjdk version "11.0.12" 2021-07-20 OpenJDK Runtime Environment (build 11.0.12+7-suse-3.62.1-x8664) OpenJDK 64-Bit Server VM (build 11.0.12+7-suse-3.62.1-x8664, mixed mode)
- TLS客户端使用SNI扩展,将Server Name字段设置为根CA的SubjectDN的CN字段。
- 服务端需要在TLS握手阶段,根据SNI选择对应的中间CA证书,与服务端证书共同组成证书链返回给客户端。
- 服务端证书由两个子CA交叉签名,证书信任链结构如下:
RootCA1 RootCA2 | | +--+ | | | +-->Sub2* +-> Sub2 | | +--------->+-> ServerCertificate
注意事项:
- Sub2和Sub2*的SubjectDN完全相同,但IssuerDN不同。
- Sub2的IssuerDN为RootCA2
- Sub2*的IssuerDN为RootCA1
服务端KeyStore中有两个条目:
- [KeyStore-Entry-0] {alias: "Primary", key, [ServerCertificate, Sub2]}
- [KeyStore-Entry-1] {alias: "Secondary", key, [ServerCertificate, Sub2*]}
Java服务端的SSL参数中添加的SNIMatcher配置为匹配RootCA*格式的请求。
测试使用的openssl命令如下:
openssl s_client -servername 'RootCA1' -CAfile ./RootCA1.pem -connect localhost:4321 openssl s_client -servername 'RootCA2' -CAfile ./RootCA2.pem -connect localhost:4321
原本预期Java KeyManager会根据可信Sub2证书的IssuerDN字段匹配SNI请求,当Client Hello的SNI为RootCA1时,服务端返回ServerCertificate + Sub2*证书链;SNI为RootCA2时返回ServerCertificate + Sub2证书链。
运行时服务端始终返回ServerCertificate + Sub2证书链,后续握手失败,报错如下:
openssl s_client -servername 'RootCA1' -CAfile ./RootCA1.pem -connect localhost:4321 ... Verification error: unable to get local issuer certificate ...
调整KeyStore中的别名顺序、密钥顺序均无法解决问题。
以下是最终适配方案:
- 第一步:在SSL参数中使用正确的正则表达式配置SNI Matcher,
javax.net.ssl.SNIMatcher的作用仅为拦截非法客户端请求,返回TLS告警Unrecognized Name。 - 第二步:将KeyStore中所有条目的别名统一设置为小写。
- 第三步:自定义类继承
X509ExtendedKeyManager,重写chooseServerAlias方法(根据实现不同可能还需要重写chooseEngineServerAlias方法),在该方法中解析客户端发送的SNI值,返回对应KeyStore条目的别名,后续流程会使用该别名获取对应的证书链。
代码实现说明:SniKeyManager包装了KeyManagerFactory生成的ExtendedKeyManager,之后传入SSLContext.init方法,保留KeyManagerFactory生成的原生X509ExtendedKeyManager实例,由其持有KeyStore的访问权。
SniKeyManager注入SSLContext的代码如下:
for (KeyManager keyManager : keyManagerFactory.getKeyManagers()) { if (keyManager instanceof X509ExtendedKeyManager) { x509KeyManager = (X509ExtendedKeyManager) keyManager; break; } } SniKeyManager sniKeyManager = new SniKeyManager(x509KeyManager); sslContext.init(new KeyManager[] {sniKeyManager}, null, null);
SniKeyManager类示例代码如下:
public final class SniKeyManager extends X509ExtendedKeyManager { private final X509ExtendedKeyManager keyManager; public SniKeyManager(X509ExtendedKeyManager keyManager) { this.keyManager = keyManager; } @Override public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { String result = this.keyManager.chooseServerAlias(keyType, issuers, socket); // chooseServerAlias在keyType非法时可能返回null if (result != null && socket instanceof SSLSocket) { SSLSocket s = (SSLSocket) socket; ExtendedSSLSession session = (ExtendedSSLSession) s.getHandshakeSession(); List<SNIServerName> snis = session.getRequestedServerNames(); for (SNIServerName sniValue : snis) { result = getKeyStoreAliasBySni(sniValue); if (null != result) break; } } // 注意返回的别名必须保持小写 return result; } // 其余方法省略 @Override public X509Certificate[] getCertificateChain(String alias) { return keyManager.getCertificateChain(alias); } @Override public PrivateKey getPrivateKey(String alias) { return keyManager.getPrivateKey(alias); } }
SNI值到KeyStore别名的绑定逻辑在SniKeyManager.getKeyStoreAliasBySni方法中实现即可。Java内置的ServerHandshaker类会使用X509ExtendedKeyManager处理TLS握手,chooseServerAlias返回的别名会被用于调用getPrivateKey和getCertificateChain方法获取对应密钥和证书链。
内容的提问来源于stack exchange,提问作者elefanxp

