java - 如何使用 MSCAPI 提供程序进行客户端 SSL 身份验证

标签 java security authentication ssl

我正在编写一个程序,它需要与需要使用 SSL 客户端身份验证的 Web 服务器建立 HTTPS 连接。

此程序的用户将使用来自 windows 环境的证书来验证自己。

我找到了很多展示如何设置客户端身份验证的示例,如果我先将我的证书导出为 pkcs12 格式,它就可以正常工作,但我不想强制我的用户这样做。但是,当我尝试使用 MSCAPI 时,它总是会出现异常:

javax.net.ssl.SSLHandshakeException: Error signing certificate verify
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source)
        at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
        at sun.security.ssl.ClientHandshaker.serverHelloDone(Unknown Source)
        at sun.security.ssl.ClientHandshaker.processMessage(Unknown Source)
        at sun.security.ssl.Handshaker.processLoop(Unknown Source)
        at sun.security.ssl.Handshaker.process_record(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.readDataRecord(Unknown Source)
        at sun.security.ssl.AppInputStream.read(Unknown Source)
        at java.io.BufferedInputStream.fill(Unknown Source)
        at java.io.BufferedInputStream.read1(Unknown Source)
        at java.io.BufferedInputStream.read(Unknown Source)
        at sun.net.www.http.HttpClient.parseHTTPHeader(Unknown Source)
        at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
        at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
        at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(Unknown Source)
        at com.example.Win2.main(Win2.java:62)
Caused by: java.security.SignatureException: Bad Key.

        at sun.security.mscapi.RSASignature.signHash(Native Method)
        at sun.security.mscapi.RSASignature.engineSign(RSASignature.java:390)
        at java.security.Signature$Delegate.engineSign(Unknown Source)
        at java.security.Signature.sign(Unknown Source)
        at sun.security.ssl.RSASignature.engineSign(Unknown Source)
        at java.security.Signature$Delegate.engineSign(Unknown Source)
        at java.security.Signature.sign(Unknown Source)
        at sun.security.ssl.HandshakeMessage$CertificateVerify.<init>(Unknown Source)
        ... 16 more

我真的无法从那个异常中判断出 key 可能有什么问题。

我做了一个小测试程序来重现我遇到的问题:

String passwd = .....";
URL url = new URL("https://.........");

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance("Windows-MY");
keyStore.load(null, passwd.toCharArray());
keyManagerFactory.init(keyStore, passwd.toCharArray());

SSLContext context = SSLContext.getInstance("TLS");
context.init(keyManagerFactory.getKeyManagers(), null, null);
SSLSocketFactory socketFactory = context.getSocketFactory();

HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(socketFactory);

这里的 API 显然有一些我不理解的地方。它怎么知道我想使用 keystore 中的哪个 key ?我原以为 Windows 会提示我,就像它对其他需要身份验证的应用程序所做的那样,但我怀疑它只是选择它找到的第一个应用程序。

我是否需要实现自己的 key 管理器,以便它可以选择要使用的 key ?

如果我遍历 keystore ,我可以看到其中的 key ,并且可以通过调用 getKey 来提取 key 。使事情复杂化的是,商店中有多个具有相同别名(但有效性不同)的 key 。其他(非 Java)应用程序,例如。 Chrome,似乎能够以某种方式确定使用哪些键。

编辑:我忘了说,我使用的是 Java 1.7。

最佳答案

负责选择将使用 keystore 中的哪个 key 的是 KeyManager 对象。 您可以获得调用 keyManagerFactory.getKeyManagers() 的那些对象的 vector 。

图书馆通常会获得他们在商店中找到的第一个 key 条目(可能与本例中提供的服务器证书兼容)。 MS-CAPI API 也不异常(exception)。

要在 keystore 中选择要使用的 key ,您应该做 3 件事:

  1. 实现接口(interface)X509KeyManager

  2. 制作方法chooseClientAlias上述接口(interface)返回所需的别名您的 key

  3. 将对象设置为您的 SSLContext。

提醒您,您的 keystore 必须包含从您的个人证书开始到根授权机构的所有证书链。您应该使用 certmgr.msc 程序导入证书和/或检查所有证书是否都存在于文件夹Personal(您的证书)、中间证书颁发机构(您链中的任何中间 CA)和受信任的根证书颁发机构

设置您的信任 keystore 也很重要 - 它存储您信任的根证书,该存储将用于验证服务器证书。如果是 MS-CAPI,您可以使用 KeyStore.getInstance("Windows-ROOT") 命令获取它(Trusted Root Certification Authorities 文件夹中的证书)。

修改您的代码以完成此操作:

URL url = new URL("https://.........");

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance("Windows-MY");
keyStore.load(null, null);
keyManagerFactory.init(keyStore);

/* You must also set your trust store */
KeyStore ts = KeyStore.getInstance("Windows-ROOT");
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);

/* Here you can implement a way to set your key alias 
** You can run through all key entries and implement a way
** to prompt the user to choose one - for simplicity I just set a
** name*/
String alias = "user1_alias";

/* Get your current KeyManager from the factory */
final X509KeyManager okm = (X509KeyManager)keyManagerFactory.getKeyManagers()[0];

/* Implement the Interface X509KeyManager */
X509KeyManager km = new X509KeyManager() {
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
         /* Implement your own logic to choose the alias
            according to the validity if the case, 
            or use the entry id or any other way, you can get 
            those values outside this class*/
         return alias;
    }

    public X509Certificate[] getCertificateChain(String alias) {
         return okm.getCertificateChain(alias);
    }
   /* Implement the other methods of the interface using the okm object */
};
SSLContext context = SSLContext.getInstance("TLS");
/* set the keymanager in the SSLContext */
context.init(new KeyManager[]{km}, tmf.getTrustManagers(), new SecureRandom());
SSLSocketFactory socketFactory = context.getSocketFactory();

HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(socketFactory);

关于java - 如何使用 MSCAPI 提供程序进行客户端 SSL 身份验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20321984/

相关文章:

java - 如何在java中的套接字之间传递音频

php - 上传表单中的空字节注入(inject)

c# - 页面方法和安全

java - 我需要在flex中加密和解密Sha或Md5算法

java - 服务器进行客户端身份验证

java - 当我需要 1 个时,JSP 多个 CDATA 部分

java - Map中的Json对象,在java中如何获取它?

java - JPA:反向级联删除

java - 使用基本身份验证保护 Spring Boot REST 应用程序

ruby-on-rails - 使用 Omniauth on Rails 3 检索 Google 个人资料图片