You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 18:54:04