如何在特定连接上使用不同的证书?
Posted
技术标签:
【中文标题】如何在特定连接上使用不同的证书?【英文标题】:How can I use different certificates on specific connections? 【发布时间】:2010-10-25 22:31:14 【问题描述】:我要添加到我们的大型 Java 应用程序的模块必须与另一家公司的 SSL 安全网站进行对话。问题是该站点使用自签名证书。我有一份证书副本以验证我没有遇到中间人攻击,我需要将此证书合并到我们的代码中,以便与服务器的连接成功。
这是基本代码:
void sendRequest(String dataPacket)
String urlStr = "https://host.example.com/";
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setMethod("POST");
conn.setRequestProperty("Content-Length", data.length());
conn.setDoOutput(true);
OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
o.write(data);
o.flush();
如果没有对自签名证书进行任何额外的处理,它会在 conn.getOutputStream() 处死掉,但有以下异常:
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
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
理想情况下,我的代码需要教 Java 接受这个自签名证书,用于应用程序中的这一点,而不是其他任何地方。
我知道我可以将证书导入 JRE 的证书颁发机构存储区,这将允许 Java 接受它。如果我能提供帮助,那不是我想采取的方法。在我们所有客户的机器上为他们可能不使用的一个模块进行操作似乎非常具有侵入性;它会影响使用相同 JRE 的所有其他 Java 应用程序,即使任何其他 Java 应用程序访问该站点的可能性为零,我也不喜欢这样。这也不是一个简单的操作:在 UNIX 上,我必须获得访问权限才能以这种方式修改 JRE。
我还看到我可以创建一个 TrustManager 实例来执行一些自定义检查。看起来我什至可以创建一个 TrustManager,它在除此证书之外的所有实例中都委托给真正的 TrustManager。但看起来 TrustManager 是全局安装的,我认为这会影响来自我们应用程序的所有其他连接,这对我来说也不太合适。
设置 Java 应用程序以接受自签名证书的首选、标准或最佳方法是什么?我能完成上面提到的所有目标,还是不得不妥协?是否有涉及文件和目录以及配置设置以及几乎没有代码的选项?
【问题讨论】:
小,工作修复:rgagnon.com/javadetails/… @Hasenpriester:请不要推荐这个页面。它禁用所有信任验证。您不仅要接受所需的自签名证书,还要接受 MITM 攻击者提供给您的任何证书。 【参考方案1】:自己创建一个SSLSocket
工厂,连接前设置在HttpsURLConnection
上。
...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...
您需要创建一个SSLSocketFactory
并保留它。这是如何初始化它的草图:
/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ...
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();
如果您在创建密钥库时需要帮助,请发表评论。
这是加载密钥库的示例:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();
要使用 PEM 格式证书创建密钥库,您可以使用 CertificateFactory
编写自己的代码,或者直接使用 JDK 中的 keytool
导入它(keytool 不会适用于一个“关键条目”,但对于“受信任的条目”来说很好)。
keytool -import -file selfsigned.pem -alias server -keystore server.jks
【讨论】:
非常感谢!其他人帮助我指出了正确的方向,但最终你的方法是我采取的方法。我有一项艰巨的任务是将 PEM 证书文件转换为 JKS Java 密钥库文件,我在这里找到了帮助:***.com/questions/722931/… 我很高兴它成功了。很抱歉你在钥匙店里苦苦挣扎;我应该把它包含在我的答案中。 CertificateFactory 并不太难。事实上,我想我会为以后来的人做一个更新。 这段代码所做的只是复制您可以通过设置 JSSE 参考指南中描述的三个系统属性来完成的工作。 @EJP:这是不久前的事了,所以我记不太清了,但我猜原因是在“大型 Java 应用程序”中,可能还有其他 HTTP建立的连接。设置全局属性可能会干扰工作连接,或允许这一方欺骗服务器。这是根据具体情况使用内置信任管理器的示例。 正如 OP 所说,“我的代码需要教 Java 接受这个自签名证书,用于应用程序中的这一点,而不是其他任何地方。”【参考方案2】:我在网上阅读了很多地方来解决这个问题。 这是我为使其工作而编写的代码:
ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream);
String alias = "alias";//cert.getSubjectX500Principal().getName();
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);
trustStore.setCertificateEntry(alias, cert);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(trustStore, null);
KeyManager[] keyManagers = kmf.getKeyManagers();
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
URL url = new URL(someURL);
conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
app.certificateString 是一个包含证书的字符串,例如:
static public String certificateString=
"-----BEGIN CERTIFICATE-----\n" +
"MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" +
"BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" +
... a bunch of characters...
"5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" +
"-----END CERTIFICATE-----";
我已经测试过你可以在证书字符串中放入任何字符,如果它是自签名的,只要你保持上面的确切结构。我通过笔记本电脑的终端命令行获取了证书字符串。
【讨论】:
感谢分享@Josh。我创建了一个小型 Github 项目来演示您正在使用的代码:github.com/aasaru/ConnectToTrustedServerExample【参考方案3】:如果无法创建SSLSocketFactory
,只需将密钥导入JVM
检索公钥:
$openssl s_client -connect dev-server:443
,然后创建一个文件dev-server.pem,看起来像
-----BEGIN CERTIFICATE-----
lklkkkllklklklklllkllklkl
lklkkkllklklklklllkllklkl
lklkkkllklk....
-----END CERTIFICATE-----
导入密钥:#keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem
。
密码:changeit
重启JVM
来源:How to solve javax.net.ssl.SSLHandshakeException?
【讨论】:
我不认为这解决了原来的问题,但它解决了我的问题,所以谢谢! 这样做也不是一个好主意:现在您有了这个随机的额外系统范围的自签名证书,所有 Java 进程默认信任它。【参考方案4】:我们复制 JRE 的信任库并将我们的自定义证书添加到该信任库,然后告诉应用程序使用具有系统属性的自定义信任库。这样我们就不用管默认的 JRE 信任库了。
缺点是,当您更新 JRE 时,您的新信任库不会自动与您的自定义信任库合并。
您可以通过安装程序或启动例程来验证信任库/jdk 并检查不匹配或自动更新信任库来处理这种情况。我不知道如果您在应用程序运行时更新信任库会发生什么。
此解决方案并非 100% 优雅或万无一失,但它简单、有效且无需代码。
【讨论】:
【参考方案5】:在使用 commons-httpclient 访问带有自签名证书的内部 https 服务器时,我不得不这样做。是的,我们的解决方案是创建一个自定义的 TrustManager,它只传递所有内容(记录调试消息)。
这归结为拥有我们自己的 SSLSocketFactory,它从我们的本地 SSLContext 创建 SSL 套接字,它被设置为只有我们的本地 TrustManager 与之关联。您根本不需要靠近密钥库/证书库。
所以这是在我们的 LocalSSLSocketFactory 中:
static
try
SSL_CONTEXT = SSLContext.getInstance("SSL");
SSL_CONTEXT.init(null, new TrustManager[] new LocalSSLTrustManager() , null);
catch (NoSuchAlgorithmException e)
throw new RuntimeException("Unable to initialise SSL context", e);
catch (KeyManagementException e)
throw new RuntimeException("Unable to initialise SSL context", e);
public Socket createSocket(String host, int port) throws IOException, UnknownHostException
LOG.trace("createSocket(host => , port => )", new Object[] host, new Integer(port) );
return SSL_CONTEXT.getSocketFactory().createSocket(host, port);
连同其他实现 SecureProtocolSocketFactory 的方法。 LocalSSLTrustManager 是前面提到的虚拟信任管理器实现。
【讨论】:
如果您禁用所有信任验证,那么首先使用 SSL/TLS 就没有什么意义了。在本地测试是可以的,但如果你想在外面连接就不行了。 在 Java 7 上运行时出现此异常。javax.net.ssl.SSLHandshakeException: no cipher suites in common 你能帮忙吗?以上是关于如何在特定连接上使用不同的证书?的主要内容,如果未能解决你的问题,请参考以下文章
为什么两个在不同连接上调用HttpSendRequest(新的InternetOpen和新的InternetConnect)会带来不同的结果?
如何在与 SQLAlchemy 和 psycopg2 的 PostgreSQL 连接上设置“lock_timeout”?
是否可以在不同的连接上执行 CallableStament?