如果 Jetty 的密钥存储中有多个证书,它如何选择?

2022-09-02 10:40:31

我们的系统中有一些代码可以自动将自签名证书生成到密钥存储中,然后由Jetty使用。如果给定主机的密钥已经存在,则不会发生任何事情,但如果它不存在,我们会生成一个新密钥,如下所示:

public void generateKey(String commonName) {
    X500Name x500Name = new X500Name("CN=" + commonName);
    CertAndKeyGen keyPair = new CertAndKeyGen("DSA", "SHA1withDSA");
    keyPair.generate(1024);
    PrivateKey privateKey = keyPair.getPrivateKey();
    X509Certificate certificate = keyPair.getSelfCertificate(x500Name, 20*365*24*60*60);
    Certificate[] chain = { certificate };
    keyStore.setEntry(commonName, privateKey, "secret".toCharArray(), chain);
}

只要密钥存储中只有一个密钥和证书,这一切都可以正常工作。一旦你有多个键,当你尝试连接时,奇怪的事情就会发生:

java.io.IOException: HTTPS hostname wrong:  should be <127.0.0.1>

这是一个非常神秘的错误,但我最终设法通过编写一个单元测试来跟踪它,该单元测试连接到服务器并断言证书上的CN与主机名匹配。我发现的非常有趣 - Jetty似乎任意选择向客户出示哪个证书,但以一致的方式。

例如:

  • 如果“CN=localhost”和“CN=cheese.mydomain”在密钥商店中,它总是选择“CN=cheese.mydomain”。
  • 如果密钥库中的“CN=127.0.0.1”和“CN=cheese.mydomain”,它总是选择“CN=cheese.mydomain”。
  • 如果密钥存储区中“CN=192.168.222.100”(cheese.mydomain)和“CN=cheese.mydomain”,则始终选择“CN=192.168.222.100”。

我写了一些代码,循环访问存储中的证书以将其打印出来,发现它没有始终如一地选择第一个证书或类似的东西。

那么它到底用什么标准呢?最初,我认为localhost很特别,但后来第三个例子让我完全困惑。

我认为这是由KeyManagerFactory以某种方式决定的,在我的情况下是SunX509。


答案 1

这确实最终是由(通常从a获得)决定的。KeyManagerKeyManagerFactory

一个密钥库可以有多个证书存储在不同的别名下。如果在 Jetty 配置中没有通过 certAlias 显式配置别名,则实现将选取它找到的第一个别名,这些别名具有私钥和所选密码套件的正确类型的密钥(通常是 RSA,但此处可能是 DSA)。如果您查看 Sun 提供程序的实现,那么选择逻辑还有更多内容,但您不应该真正依赖一般的顺序,而只依赖于别名。SunX509

您当然可以使用自己的X509KeyManager为Jetty提供自己的别名。您必须实现:SSLContext

 chooseServerAlias(String keyType, Principal[] issuers, Socket socket)

不幸的是,除了 和 ,你唯一能做的就是它本身。充其量,您在那里获得的有用信息是本地IP地址和远程IP地址。keyTypeissuerssocket

除非您的服务器侦听同一端口上的多个 IP 地址,否则您将始终获得相同的本地 IP 地址。(在这里,显然,你至少有两个:和,但我怀疑除了你自己的测试之外,你对localhost并不感兴趣。您需要在服务器端支持服务器名称指示 (SNI),以便能够根据请求的主机名(由支持它的客户端)做出决定。不幸的是,SNI只在Java 7中引入,而只是在客户端引入。127.0.0.1192.168.222.100

您将在这里面临的另一个问题是Java客户端会抱怨主题DN的CN中的IP地址。一些浏览器会容忍这种情况,但这不符合HTTPS规范(RFC 2818)。IP 地址必须是 IP 地址类型的使用者备用名称条目。


答案 2