在 Java (JSSE) 中使用默认 KeyStore 时如何提供特定的 TrustStore

Posted

技术标签:

【中文标题】在 Java (JSSE) 中使用默认 KeyStore 时如何提供特定的 TrustStore【英文标题】:How do I provide a specific TrustStore while using the default KeyStore in Java (JSSE) 【发布时间】:2013-02-22 13:07:25 【问题描述】:

概述

JSSE 允许用户通过指定 javax.net.ssl.* 参数来提供默认的信任库和密钥库。我想为我的应用程序提供一个非默认的 TrustManager,同时允许用户像往常一样指定 KeyManager,但似乎没有任何方法可以实现这一点。

详情

http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#CustomizingStores

假设在 unix 机器上我希望允许用户使用 pkcs12 密钥库进行身份验证,而在 OS X 上我希望允许用户使用系统钥匙串。在 OS X 上,应用程序可能会按如下方式启动:

java -Djavax.net.ssl.keyStore=NONE -Djavax.net.ssl.keyStoreType=KeychainStore \
     -Djavax.net.ssl.keyStorePassword=- -jar MyApplication.jar

这可以正常工作:当应用程序访问需要相互身份验证(客户端证书身份验证)的 https 服务器时,将提示用户允许访问他们的钥匙串。

问题

现在假设我想将自签名证书颁发机构与我的应用程序捆绑在一起。我可以通过构造一个 TrustManagerFactory 并传入一个包含我的证书 (javadoc) 的 KeyStore 来覆盖默认信任管理器。但是,要使用这个非默认信任管理器,我需要创建和初始化 SSLContext。问题就在这里。

通过调用init(..) 并同时传递 KeyManager 和 TrustManager 来初始化 SSLContext。但是,使用 javax.net.ssl.* 参数创建 KeyManager 的逻辑嵌入在默认 SSLContexts 的实现中——我找不到使用默认行为获取 KeyManager 或 KeyManagerFactory 的方法,同时还指定了非默认 TrustManager 或 TrustManagerFactory。因此,似乎不可能使用例如适当的操作系统特定的钥匙串实现,同时还提供用于验证远程服务器的根证书。

【问题讨论】:

澄清一下,我不想重新实现默认行为(例如,通过复制 sun.security.ssl.SSLContextImpl 或 javax.net.ssl.DefaultSSLContext 的实现)作为默认加载行为在不同平台上可能会有所不同,并且可能会在未来的 JVM 版本中发生变化。我正在专门寻找一种重用默认行为的方法,例如通过从默认 SSLContext 中提取 KeyManager。 【参考方案1】:

编写具有默认行为的 KeyManager 并不难。这只是几行代码。令人惊讶的是,SSLContext 的行为并非都像 w.r.t 那样。 KeyManager,就像他们在 w.r.t.信任管理器。 IBM 的 JSSE 确实如此。不过自己合成也不难:

SSLContext  context = SSLContext.getInstance("TLS");
String  keyStore = System.getProperty("javax.net.ssl.keyStore");
String  keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
String  keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword","");
KeyManager[]    kms = null;
if (keyStore != null)

    KeyManagerFactory   kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    KeyStore    ks = KeyStore.getInstance(keyStoreType);
    if (keyStore != null && !keyStore.equals("NONE")) 
        fs = new FileInputStream(keyStore);
    ks.load(fs, keyStorePassword.toCharArray());
    if (fs != null)
        fs.close();
    char[]  password = null;
    if (keyStorePassword.length() > 0)
        password = keyStorePassword.toCharArray();
    kmf.init(ks,password);
    kms = kmf.getKeyManagers();

context.init(kms,null,null);

【讨论】:

如您所见,不同的 JSSE 实现有不同的行为,我不想实现自己的 KeyManager,然后在 JVM、体系结构和 JSSE 实现的每种可能组合上对其进行测试。顺便说一句,JDK7 的默认实现只有不到 100 行代码。 @StephenNelson 嗯,我的 21 岁:-| 你的没有 cmets 或处理 PKCS11 :-) 否则它很有用,谢谢。 @StephenNelson 它应该处理 PKCS#11,如果那是指定的 javax.net.ssl.keyStoreType。【参考方案2】:

听起来您面临与this question 类似的问题,因为在SSLContext.init(...) 中将null 用于信任管理器参数会恢复为默认信任管理器,而对于密钥管理器则不会。

话虽如此,使用默认系统属性初始化 KeyManager 并不难。这样的东西应该可以工作(代码直接写在这个答案中,所以你可能需要修复一些小事情):

String provider = System.getProperty("javax.net.ssl.keyStoreProvider");
String keystoreType = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
KeyStore ks = null;
if (provider != null) 
    ks = KeyStore.getInstance(keystoreType, provider);
 else 
    ks = KeyStore.getInstance(keystoreType);

InputStream ksis = null;
String keystorePath = System.getProperty("javax.net.ssl.keyStore");
String keystorePassword = System.getProperty("javax.net.ssl.keyStorePassword");
if (keystorePath != null && !"NONE".equals(keystorePath)) 
    ksis = new FileInputStream(keystorePath);

try 
    ks.load(ksis, keystorePassword.toCharArray());
 finally 
     if (ksis != null)  ksis.close(); 


KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keystorePassword.toCharArray());
// Note that there is no property for the key password itself, which may be different.
// We're using the keystore password too.

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), ..., null);

(This utility class 也可能感兴趣,更具体地说是getKeyStoreDefaultLoader()。)

编辑:(根据您的附加评论)

当您只想自定义SSLContext 的一半时,恐怕Oracle 和IBM JSSE 似乎都没有默认行为。您在 Oracle JSSE 文档中链接到的部分说:“如果密钥库由 javax.net.ssl.keyStore 系统属性和适当的 javax.net.ssl.keyStorePassword 系统属性指定,则 KeyManager 由默认 SSLContext 将是用于管理指定密钥库的 KeyManager 实现。" 这在这里并不适用,因为您使用的是自定义SSLContext,而不是默认的(即使您正在自定义其中的一部分)。

无论如何,Oracle JSSE 参考指南和 IBM JSSE 参考指南在这个主题上有所不同。 (我不确定其中有多少是“标准”,以及原则上是否应该与另一个兼容,但显然情况并非如此。)

两个“创建SSLContext对象”部分几乎相同,但它们不同。

Oracle JSSE Reference guide 说:

如果 KeyManager[] 参数为空,那么一个空的 KeyManager 将 为这个上下文定义。

IBM JSSE Reference guide 说:

如果 KeyManager[] 参数为空,则安装的安全 提供者将被搜索最高优先级的实现 KeyManagerFactory,一个合适的 KeyManager 将从那里得到 获得。

不幸的是,如果您希望在具有不同规范的实现之间具有相同的行为,您将不得不编写一些代码,即使这实际上是在复制其中一个实现已经在做的事情。

【讨论】:

不是一个理想的答案,因为它需要重新实现默认行为而不是重用它,但非常有用。感谢您提供有关 jsslutils 的提示。 很遗憾,没有办法重用一半的 SSL 上下文。感谢您的详细回答。

以上是关于在 Java (JSSE) 中使用默认 KeyStore 时如何提供特定的 TrustStore的主要内容,如果未能解决你的问题,请参考以下文章

Java安全(JCA/JSSE):SSL/TLS

Java安全(JCA/JSSE):非对称加密

JSSE中是否有DTLS实现

在java中怎么验证ssl证书的有效性

JAVA SSL

在 WAS 9 上使用 Apache HttpClient 和 IBM Jsse2 的 SSLHandshakeException