如何将证书身份验证与 HttpsURLConnection 结合使用?

我正在尝试连接到 HTTPS URL,但我需要使用客户端身份验证和第三方软件放置在系统上的证书。

我丝毫不知道我应该如何查找或使用它,我所要做的就是C#示例代码,这与我发现的所有Java答案有很大不同。(例如,密钥库显然需要某种密码?

这是我拥有的 C# 示例代码

System.Security.Cryptography.X509Certificates.X509CertificateCollection SSC_Certs = 
    new System.Security.Cryptography.X509Certificates.X509CertificateCollection();

Microsoft.Web.Services2.Security.X509.X509CertificateStore WS2_store =
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.CurrentUserStore(
    Microsoft.Web.Services2.Security.X509.X509CertificateStore.MyStore);

WS2_store.OpenRead();

Microsoft.Web.Services2.Security.X509.X509CertificateCollection WS2_store_Certs = WS2_store.Certificates;

然后,它只是循环访问WS2_store_Certs CertificateCollection 并以这种方式检查它们。再往前走一点,它像这样设置证书:

HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url_string);
httpWebRequest.ClientCertificates = SSC_Certs;

这一切看起来都相当合乎逻辑,即使我不知道它是如何找到证书的,但我仍然无法找到Java的等效物。

更新

我正在建立的连接是依赖于JDK 5的更大应用程序的一部分,但是我已经设法使用sumscapi jar来查找我正在寻找的证书。当我尝试使用Windows密钥库进行连接时,它会出错,所以我认为我通过从Windows存储中获取所需的证书并将其插入默认的java中来解决这个问题。现在我得到一个EOFException,后跟一个SSLHandshakeException,上面写着“握手时远程主机关闭连接”。ssl调试跟踪不会向我揭示直接问题,因为我需要的证书显示在此处的证书链中。

它完成了整个 ClientKeyExchange 的事情,说它已经完成了,然后我从调试日志中得到的最后一条消息是

[write] MD5 and SHA1 hashes:  len = 16
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
Padded plaintext before ENCRYPTION:  len = 32
0000: 14 00 00 0C D3 E1 E7 3D   C2 37 2F 41 F9 38 26 CC  .......=.7/A.8&.
0010: CB 10 05 A1 3D C3 13 1C   EC 39 ED 93 79 9E 4D B0  ....=....9..y.M.
AWT-EventQueue-1, WRITE: TLSv1 Handshake, length = 32
[Raw write]: length = 37
0000: 16 03 01 00 20 06 B1 D8   8F 9B 70 92 F4 AD 0D 91  .... .....p.....
0010: 25 9C 7D 3E 65 C1 8C A7   F7 DA 09 C0 84 FF F4 4A  %..>e..........J
0020: CE FD 4D 65 8D                                     ..Me.
AWT-EventQueue-1, received EOFException: error

我用来设置连接的代码是

KeyStore jks = KeyStore.getInstance(KeyStore.getDefaultType());
jks.load(null, null);

KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
jks.setCertificateEntry("alias", cert1); //X509Certificate obtained from windows keystore
kmf.init(jks, new char[0]);

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(kmf.getKeyManagers(), new TrustManager[]{tm}, null);

sslsocketfactory = sslContext.getSocketFactory();
System.setProperty("https.proxyHost", proxyurl);
System.setProperty("https.proxyPort", proxyport);

Authenticator.setDefault(new MyAuthenticator("proxyID", "proxyPassword"));
URL url = new URL(null, urlStr, new sun.net.www.protocol.https.Handler());
HttpsURLConnection uc = (HttpsURLConnection) url.openConnection();
uc.setSSLSocketFactory(sslsocketfactory);
uc.setAllowUserInteraction(true);
uc.setRequestMethod("POST");
uc.connect();

(我还没有尝试过HttpClient,因为我不知道如何找到证书文件,我也不确定这是否在每个客户端系统上都一样。

另一个更新

我已经在WINDOWS-ROOT密钥库中找到了我需要的证书的CA(并使用.verify()进行了检查以查看它们是否都签出),我也将它们添加到java密钥库中,但仍然没有任何更改。我想他们应该进入TrustStore,但我还没有找到一种方法来以编程方式做到这一点。(我宁愿不依赖最终用户来做这种事情,因为我只能向他们保证,由于这个荒谬的长问题开头提到的第三方软件,证书和CA将存在。

还有更多更新

在上一次更新的基础上,我得出的结论是,我的问题一定在于我的CA不在Java的cacerts文件中,因此它从服务器获取受信任的CA列表,但无法识别它们,并且随后不会发送导致连接失败的单个证书。所以问题仍然存在,我如何让Java使用它的密钥库作为信任库,或者以编程方式将证书添加到cacerts(不需要文件路径)?因为如果这些是不可能的,那只会给我留下秘密选项C,巫毒教。我会开始用针刺一个杜克娃娃,以防万一。


答案 1

好的,所以你的问题的标题是“如何通过HttpsURLConnection使用证书身份验证”?我有一个工作的例子。要使它工作,它有一个先决条件:

  1. 您必须有一个包含证书文件的密钥库。(我知道你的场景并不完全允许这样做,但请在这里关注我,以便我们可以缩小你的问题范围,因为它有点太复杂了,无法立即回答。)

因此,首先要了解实际的证书文件。如果您使用的是Windows 7,则可以通过以下步骤完成此操作:

  1. 打开 Internet Explorer
  2. 打开工具(在Internet Explorer 9中,它是齿轮图标),
  3. 单击互联网选项
  4. 转到内容选项卡,
  5. 单击“证书”,
  6. 找到手头的证书,
  7. 单击它,然后单击“导出”并将其保存到文件(DER编码的二进制X.509)。

(导出后,将其从其他证书中删除,确保Java不会以某种方式使用它。我不知道它是否可以使用它,但它不会伤害。)

现在,您必须创建一个密钥库文件并将导出的证书导入其中,这可以通过以下方式完成。

> keytool -importcert -file <certificate> -keystore <keystore> -alias <alias>

(显然,keytool必须在你的工作道路上。它是JDK的一部分。)

它会提示您输入密码(密钥库的密码;它不必对证书执行任何操作),我不知道现在如何将其设置为密码,因此请将其设置为“让它成为”或其他任何内容。""password

在此之后,通过以下步骤,您可以通过代理与终端节点建立安全连接。

  1. 首先,加载密钥库文件。

    InputStream trustStream = new FileInputStream("path/to/<keystore>");
    char[] trustPassword = "<password>".toCharArray();
    
  2. 初始化密钥库

    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(trustStream, trustPassword);
    
  3. 初始化 TrustManager 对象。(我认为这些处理证书解析或类似的东西,但是就我而言,这是魔术。)

    TrustManagerFactory trustFactory =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustFactory.init(trustStore);
    TrustManager[] trustManagers = trustFactory.getTrustManagers();
    
  4. 创建一个新的 SSLContext,将 TrustManager 对象加载到其中,并将其设置为默认值。请注意,因为SSLContext.getDefault()返回该类的不可修改实例(或者更像默认的无法重新初始化的实例,但无论如何),这就是为什么我们必须使用SSLContext.getInstance(“SSL”)的原因。另外,不要忘记将此新的SSLContext设置为默认值,因为没有它,代码就会发出麻烦

    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManagers, null);
    SSLContext.setDefault(sslContext);
    
  5. 创建代理并为其设置身份验证。(不要使用 System.setProperty(...),而是使用 Proxy 类。哦,不要被 Type.HTTP 误导。)

    Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("<host>", <port>));
    
  6. 为代理设置身份验证。(我使用了一个不需要身份验证的免费代理,所以我现在无法测试问题的这一部分。)

    Authenticator.setDefault(new Authenticator() {
    
      @Override
      protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("<user>", "<password>".toCharArray());
      }
    });
    
  7. 通过将以前创建的代理传递到连接来连接到终结点。(我使用了公司服务的一个 URL,它要求提供证书, 当然,我在自己的密钥存储中导入了哪个证书。)

    URL url = new URL("<endpoint>");
    URLConnection connection = url.openConnection(proxy);
    connection.connect();
    

如果它不能像这样工作(你得到错误),那么尝试使用HttpsURLConnection

   HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
   httpsConnection.setAllowUserInteraction(true);
   httpsConnection.setRequestMethod("POST");

基本上,如果服务器(您连接到的URL所指向的资源所在的位置)要求提供凭据,则setAllowUserInteraction会启动,对吗?现在,我无法测试它本身,但是当我看到您是否可以让这个婴儿使用不需要身份验证即可访问其资源的服务器时,那么您就可以了,因为服务器只会要求您仅在已经建立连接后进行身份验证

如果在这些之后,您仍然收到一些错误,那么请发布它。


答案 2

根据我上次尝试这样做的记忆,你不能使用.您可以查看支持此功能的Apache HttpClient库。HttpsURLConnection

下面是一个代码示例,给出了该过程的概念:

String server = "example.com";
int port = 443;
EasySSLProtocolSocketFactory psf = new EasySSLProtocolSocketFactory();
InputStream is = readFile("/path/to/certificate");
KeyMaterial km = new KeyMaterial(is, "certpasswd".toCharArray());
easy.setKeyMaterial(km);

Protocol proto = new Protocol("https", (ProtocolSocketFactory) psf, port);
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setHost(server, port, proto);

编辑(关于汤姆评论):

以下是有关如何获取存储在 Windows 密钥存储中的证书的一些想法:

  • 您需要使用 Sun Cryptography 套件(即 Sun Java 6 JDK)
  • 您可以像这样获取密钥库:ks = KeyStore.getInstance("Windows-MY");
  • 您可以通过以下方式加载它:.JVM 将加载 Windos 密钥库并负责请求密钥库密码。ks.load(null, null);
  • 然后,您可以像导航任何其他密钥库一样导航密钥库。

推荐