如何为HTTPS调用指定出站证书别名?

20

我正在调用一个需要客户端证书认证的 Web 服务。

如果我指定了一个包含单个证书(服务所需的客户端证书)的 Java 密钥库,那么一切都正常。但是,如果我使用包含多个证书的密钥库,则似乎无法指定客户端应该选择哪个证书,客户端似乎会选择第一个可用的证书(按字母顺序排列)。

我尝试了以下属性,但没有达到预期的结果:

System.setProperty("com.sun.enterprise.security.httpsOutboundKeyAlias", "my-client-certificate alias");

我该如何指定应使用哪个客户端证书别名?


密钥库是否还包含客户端的私钥,或者您只是导入了证书列表? - Cratylus
是的,它包含客户私钥以及其他私钥。 - aksamit
4个回答

16

Jakub提供的链接可以带您找到答案,但是我想在这里发布一个更简单的回答,因为我们在解决这个问题之前花了很长时间。

我们的情况是有几个可用证书,并且我们需要使用具有特定别名的证书来执行连接。我们通过创建自己的KeyManager实现来完成此操作,该实现将其大部分功能传递给默认的X509KeyManager,但具有在执行连接时选择正确别名的功能。

首先是我们创建的密钥管理器:

public class FilteredKeyManager implements X509KeyManager {

private final X509KeyManager originatingKeyManager;
private final X509Certificate[] x509Certificates;

public FilteredKeyManager(X509KeyManager originatingKeyManager, X509Certificate[] x509Certificates) {
    this.originatingKeyManager = originatingKeyManager;
    this.x509Certificates = x509Certificates;
}

public X509Certificate[] getCertificateChain(String alias) {
    return x509Certificates;
}

public String[] getClientAliases(String keyType, Principal[] issuers) {
    return new String[] {"DesiredClientCertAlias"};
}

所有其他需要实现的方法都是对 originatingKeyManager 的转发。

然后,在实际设置上下文时:

SSLContext context = SSLContext.getInstance("TLSv1");
context.init(new KeyManager[] { new FilteredKeyManager((X509KeyManager)originalKeyManagers[0], desiredCertsForConnection) },
    trustManagerFactory.getTrustManagers(), new SecureRandom());

希望这样说清楚了,对于其他试图解决这个问题的人也有帮助。


如果感兴趣的话,这就是密钥管理器的包装器在这里所做的事情:http://code.google.com/p/jsslutils/wiki/SSLContextFactory。很容易将[FixedServerAliasKeyManager](http://code.google.com/p/jsslutils/source/browse/trunk/jsslutils/src/main/java/org/jsslutils/sslcontext/keymanagers/FixedServerAliasKeyManager.java)更改为客户端而不是服务器端使用。 - Bruno

10

简短回答: 默认的Java SSL实现无法完成。

长篇回答: 我查看了 SSL 握手在 sun.security.ssl.ClientHandshaker 中是如何实现的。在它的方法serverHelloDone中调用了 X509ExtendedKeyManager.chooseClientAlias。其实现方式确实是返回第一个别名,其条目匹配给定的密钥算法和其他一些条件。没有办法调整别名的选择。

对于那些可以更改代码的人来说,这看起来像是一个有前途的解决方法: http://www.44342.com/java-f392-t785-p1.htm


我找到了一个更复杂的解决方案链接:http://www.angelfire.com/or/abhilash/site/articles/jsse-km/customKeyManager.html - Jakub

7
这是一个完整可用的代码片段。
我用它在Android上创建一个SSL连接,并使用一个智能卡中包含多个证书的密钥库来匹配标准过滤条件。
感谢zarniwoop的贡献。
/**
 * filters the SSLCertificate we want to use for SSL
 * <code>
 * KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
 * kmf.init(keyStore, null);
 * String SSLCertificateKeyStoreAlias = keyStore.getCertificateAlias(sslCertificate);
 * KeyManager[] keyManagers = new KeyManager[] { new FilteredKeyManager((X509KeyManager)kmf.getKeyManagers()[0], sslCertificate, SSLCertificateKeyStoreAlias) };
 * </code>
 */
private class FilteredKeyManager implements X509KeyManager {

    private final X509KeyManager originatingKeyManager;
    private final X509Certificate sslCertificate;
    private final String SSLCertificateKeyStoreAlias;

    /**
     * @param originatingKeyManager,       original X509KeyManager
     * @param sslCertificate,              X509Certificate to use
     * @param SSLCertificateKeyStoreAlias, Alias of the certificate in the provided keystore
     */
    public FilteredKeyManager(X509KeyManager originatingKeyManager, X509Certificate sslCertificate, String SSLCertificateKeyStoreAlias) {
        this.originatingKeyManager = originatingKeyManager;
        this.sslCertificate = sslCertificate;
        this.SSLCertificateKeyStoreAlias = SSLCertificateKeyStoreAlias;
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
        return SSLCertificateKeyStoreAlias;
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return originatingKeyManager.chooseServerAlias(keyType, issuers, socket);
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return new X509Certificate[]{ sslCertificate };
    }

    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return originatingKeyManager.getClientAliases(keyType, issuers);
    }

    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return originatingKeyManager.getServerAliases(keyType, issuers);
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        return originatingKeyManager.getPrivateKey(alias);
    }
}

1
我的印象是,一旦KeyManager使用密钥库进行初始化,它就会使用私钥条目的别名来查找相关的证书和证书链。
否则,我认为它会根据主机识别的密钥类型和证书颁发机构选择一个链。
所以在你的情况下,你的描述中没有提到密钥库中的私钥条目,因此我猜测KeyManager会选择最合适的证书。
我完全不知道你提到的系统属性。
- 尝试更改密钥库以具有私钥和相关链
- 或者(不确定是否有效)将要发送到服务器的证书的别名更改为证书的主题名称

它包含私钥和证书链。使用System.setProperty("javax.net.debug", "ssl");,我能够验证当KeyStore只包含一个条目时,它会被选中并且一切正常。当使用包含多个私钥的KeyStore时,应用程序似乎会按照keytool命令列出的顺序选择第一个证书。 - aksamit
@aksamit:多个私钥?您在密钥库中有多个私钥及其链吗?因为从您的问题中我认为您只有多个证书(仅受信任的证书)。 - Cratylus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接