概述

在这个教程中,我们将探讨在发起SSL请求时可能会遇到的一些常见问题。

1. 证书存储错误

当Java应用程序与远程方建立SSL连接时,它需要通过验证证书来确定服务器是否可信。如果根证书不在证书存储文件中,将会出现安全异常:

Untrusted: Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

我们需要记住,默认情况下,这个文件的位置是$JAVA_HOME/lib/security/cacerts

2. 自签名证书

在非生产环境中,通常会发现由不受信任的颁发机构(CA)签发的证书,这些被称为自签名证书。

我们可以在https://wrong.host.badssl.com/https://self-signed.badssl.com/ 等URL找到一些不被信任的证书示例。在任何浏览器中打开这两个URL都会导致安全异常。我们可以查看它们,了解证书之间的差异。

打开https://self-signed.badssl.com/ 后,我们会看到浏览器返回一个“证书颁发机构无效”的错误,因为证书是由浏览器未知的权威机构签发的:

另一方面,打开https://wrong.host.badssl.com/ 会导致“证书通用名称无效”的错误,这表明证书的颁发给的主机名与提供的主机名不同:

对于运行在JDK或JRE中的应用程序,这些安全异常将阻止我们连接到这些不受信任的方。

3. 管理证书存储以信任我们的证书

幸运的是,JDK和JRE提供了与证书存储交互以管理其内容的工具,即Keytool,它位于$JAVA_HOME/bin/keytool

重要提示:Keytool与之交互需要密码。默认密码是“changeit”。

3.1. 列出证书

要获取JVM证书存储中注册的所有证书列表,我们需要执行以下命令:

keytool -list -keystore $JAVA_HOME/lib/security/cacerts

这将返回所有条目列表,如下所示:

Your keystore contains 227 entries

Alias name: accvraiz1
Creation date: Apr 14, 2021
Entry type: trustedCertEntry

Owner: C=ES, O=ACCV, OU=PKIACCV, CN=ACCVRAIZ1
Issuer: C=ES, O=ACCV, OU=PKIACCV, CN=ACCVRAIZ1
....

3.2. 添加证书

为了手动将证书添加到列表中,以便我们在发起SSL请求时进行验证,我们需要执行以下命令:

keytool -import -trustcacerts -file [certificate-file] -alias [alias] -keystore $JAVA_HOME/lib/security/cacerts

例如:

keytool -import -alias ss-badssl.com -keystore $JAVA_HOME/lib/security/cacerts -file ss-badssl.pem

3.3. 定制证书存储路径

如果上述方法不起作用,可能是因为我们的Java应用程序正在使用不同的证书存储。为了确认这一点,我们可以在运行Java应用程序时指定使用的证书存储:

java -Djavax.net.ssl.trustStore=CustomTrustStorePath ...

这样,我们确保它使用了我们之前编辑过的证书存储。如果这没有帮助,我们还可以通过应用VM选项来调试SSL连接:

-Djavax.net.debug=all

4. 自动化脚本

最后,我们可以创建一个简单但实用的脚本来自动化整个过程:

#!/bin/sh
# cacerts.sh
/usr/bin/openssl s_client -showcerts -connect $1:443 </dev/null 2>/dev/null | /usr/bin/openssl x509 -outform PEM > /tmp/$1.pem
$JAVA_HOME/bin/keytool -import -trustcacerts -file /tmp/$1.pem -alias $1 -keystore $JAVA_HOME/lib/security/cacerts
rm /tmp/$1.pem

在脚本中,首先尝试与传递的第一个参数作为DNS的域名建立SSL连接并请求显示证书。然后,证书信息通过openssl处理并保存为PEM文件。

最后,我们将使用这个PEM文件,通过指示keytool将证书导入cacerts文件,并使用DNS作为别名。

例如,我们可以尝试添加https://self-signed.badssl.com 的证书:

cacerts.sh self-signed.badssl.com

运行后,我们可以检查cacerts文件现在包含该证书:

keytool -list -keystore $JAVA_HOME/lib/security/cacerts

最后,我们将在新证书中看到:

#5: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
[CertificatePolicyId: [2.5.29.32.0]

Alias name: self-signed.badssl.com
Creation date: Oct 22, 2021
Entry type: trustedCertEntry

Owner: CN=*.badssl.com, O=BadSSL, L=San Francisco, ST=California, C=US
Issuer: CN=*.badssl.com, O=BadSSL, L=San Francisco, ST=California, C=US
Serial number: c9c0f0107cc53eb0
Valid from: Mon Oct 11 22:03:54 CEST 2021 until: Wed Oct 11 22:03:54 CEST 2023
Certificate fingerprints:
....

5. 手动添加证书

如果我们出于任何原因不想使用openssl,也可以使用浏览器提取证书并通过keytool添加。在基于Chromium的浏览器中,打开网站如https://self-signed.badssl.com/,然后打开开发者工具(Windows和Linux上的F12)。点击“安全”标签,然后点击“查看证书”。证书信息将显示出来:

转到“详细信息”标签,点击“导出”按钮并保存。这就是我们的PEM文件:

最后,我们使用keytool导入它:

$JAVA_HOME/bin/keytool -import -trustcacerts -file CERTIFICATEFILE -alias ALIAS -keystore $JAVA_HOME/lib/security/cacerts

6. 总结

在这篇文章中,我们学习了如何将自签名证书添加到我们的JDK/JRE证书存储中。现在,当Java应用程序连接到包含这些证书的站点时,它们可以信任服务器端。