我正在用Java编写一个通过HTTPS连接到两个web服务器的应用程序。一个通过默认的信任链获得受信任的证书,另一个使用自签名证书。当然,连接到第一台服务器是开箱即用的,而连接到使用自签名证书的服务器是无法工作的,直到我用来自该服务器的证书创建了trustStore。但是,到默认受信任服务器的连接不再工作,因为一旦我创建了自己的trustStore,显然默认的trustStore将被忽略。
我发现的一个解决方案是将默认trustStore中的证书添加到我自己的trustStore中。但是,我不喜欢这个解决方案,因为它要求我继续管理trustStore。(我不能假设这些证书在可预见的将来保持静态,对吗?)除此之外,我发现两个5年前的线程有类似的问题:
在JVM中注册多个密钥库
如何为Java服务器提供多个SSL证书
它们都深入Java SSL基础设施。我希望现在有一个更方便的解决方案,我可以在我的代码的安全审查中很容易地解释。
您可以使用与我在前面的回答(针对不同的问题)中提到的类似的模式。
实质上,获取默认信任管理器,创建第二个使用您自己的信任存储的信任管理器。将它们都包装在一个自定义的信任管理器实现中,该实现将调用委托给两者(当其中一个失败时返回到另一个)。
TrustManagerFactory tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here initialises the TMF with the default trust store.
tmf.init((KeyStore) null);
// Get hold of the default trust manager
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
defaultTm = (X509TrustManager) tm;
break;
}
}
FileInputStream myKeys = new FileInputStream("truststore.jks");
// Do the same with your trust store this time
// Adapt how you load the keystore to your needs
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "password".toCharArray());
myKeys.close();
tmf = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(myTrustStore);
// Get hold of the default trust manager
X509TrustManager myTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
myTm = (X509TrustManager) tm;
break;
}
}
// Wrap it in your own class.
final X509TrustManager finalDefaultTm = defaultTm;
final X509TrustManager finalMyTm = myTm;
X509TrustManager customTm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
// If you're planning to use client-cert auth,
// merge results from "defaultTm" and "myTm".
return finalDefaultTm.getAcceptedIssuers();
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
try {
finalMyTm.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
// This will throw another CertificateException if this fails too.
finalDefaultTm.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// If you're planning to use client-cert auth,
// do the same as checking the server.
finalDefaultTm.checkClientTrusted(chain, authType);
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { customTm }, null);
// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);
你不需要把这个context设置为默认的context。如何使用它取决于你正在使用的客户端库(以及它从哪里获得套接字工厂)。
话虽如此,原则上,无论如何您都必须根据需要更新信任库。Java 7 JSSE参考指南对此有一个"重要说明",现在在同一指南的版本8中降级为"说明":
JDK附带了有限数量的可信根证书java-home/lib/security/cacerts文件。正如在keytool中记录的那样参考页面,维护(即添加)是您的责任并删除包含在此文件中的证书,如果您使用此文件作为信任库。
取决于您使用的服务器的证书配置联系时,可能需要添加其他根证书。获得需要来自适当供应商的特定根证书。
也许我已经晚了6年才回答这个问题,但它可能对其他开发人员也有帮助。在加载默认信任库和我自己的自定义信任库时,我也遇到了同样的挑战。在对多个项目使用相同的自定义解决方案后,我认为创建一个库并将其公开发布以回馈社区会很方便。请看这里:Github - SSLContext-Kickstart
用法:
import nl.altindag.ssl.SSLFactory;
import javax.net.ssl.SSLContext;
import java.security.cert.X509Certificate;
import java.nio.file.Path;
import java.util.List;
public class App {
public static void main(String[] args) {
Path trustStorePath = ...;
char[] password = "password".toCharArray();
SSLFactory sslFactory = SSLFactory.builder()
.withDefaultTrustMaterial() // JDK trusted CA's
.withSystemTrustMaterial() // OS trusted CA's
.withTrustMaterial(trustStorePath, password)
.build();
SSLContext sslContext = sslFactory.getSslContext();
List<X509Certificate> trustedCertificates = sslFactory.getTrustedCertificates();
}
}
我不太确定我是否应该在这里发表这篇文章,因为它也可以被视为一种推广"我的图书馆"的方式。但我认为它可能对有同样挑战的开发人员有帮助。
你可以用下面的代码片段添加依赖项:
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>sslcontext-kickstart</artifactId>
<version>8.1.5</version>
</dependency>
您可以通过调用TrustManagerFactory.init((KeyStore)null)
来检索默认信任存储并获得其X509Certificate
s。将其与您自己的证书结合使用。您可以使用KeyStore.load
从.jks
或.p12
文件加载自签名证书,也可以通过CertificateFactory
加载.crt
(或.cer
)文件。
SSLHandshakeException
,但如果保留其中一个,它将返回状态码200。
import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.*;
public class HttpsWithCustomCertificateDemo {
public static void main(String[] args) throws Exception {
// Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
// Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
trustStore.load(null, null);
// If you comment out the following, the request will fail
trustStore.setCertificateEntry(
"stackoverflow",
// To test, download the certificate from stackoverflow.com with your browser
loadCertificate(new File("stackoverflow.crt"))
);
// Uncomment to following to add the installed certificates to the keystore as well
//addDefaultRootCaCertificates(trustStore);
SSLSocketFactory sslSocketFactory = createSslSocketFactory(trustStore);
URL url = new URL("https://stackoverflow.com/");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
// Alternatively, to use the sslSocketFactory for all Http requests, uncomment
//HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
conn.setSSLSocketFactory(sslSocketFactory);
System.out.println(conn.getResponseCode());
}
private static SSLSocketFactory createSslSocketFactory(KeyStore trustStore) throws GeneralSecurityException {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustManagers, null);
return sslContext.getSocketFactory();
}
private static X509Certificate loadCertificate(File certificateFile) throws IOException, CertificateException {
try (FileInputStream inputStream = new FileInputStream(certificateFile)) {
return (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(inputStream);
}
}
private static void addDefaultRootCaCertificates(KeyStore trustStore) throws GeneralSecurityException {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Loads default Root CA certificates (generally, from JAVA_HOME/lib/cacerts)
trustManagerFactory.init((KeyStore)null);
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
for (X509Certificate acceptedIssuer : ((X509TrustManager) trustManager).getAcceptedIssuers()) {
trustStore.setCertificateEntry(acceptedIssuer.getSubjectDN().getName(), acceptedIssuer);
}
}
}
}
}
这是布鲁诺的回答的更清晰的版本
public void configureTrustStore() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException,
CertificateException, IOException {
X509TrustManager jreTrustManager = getJreTrustManager();
X509TrustManager myTrustManager = getMyTrustManager();
X509TrustManager mergedTrustManager = createMergedTrustManager(jreTrustManager, myTrustManager);
setSystemTrustManager(mergedTrustManager);
}
private X509TrustManager getJreTrustManager() throws NoSuchAlgorithmException, KeyStoreException {
return findDefaultTrustManager(null);
}
private X509TrustManager getMyTrustManager() throws FileNotFoundException, KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException {
// Adapt to load your keystore
try (FileInputStream myKeys = new FileInputStream("truststore.jks")) {
KeyStore myTrustStore = KeyStore.getInstance("jks");
myTrustStore.load(myKeys, "password".toCharArray());
return findDefaultTrustManager(myTrustStore);
}
}
private X509TrustManager findDefaultTrustManager(KeyStore keyStore)
throws NoSuchAlgorithmException, KeyStoreException {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore); // If keyStore is null, tmf will be initialized with the default trust store
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
return (X509TrustManager) tm;
}
}
return null;
}
private X509TrustManager createMergedTrustManager(X509TrustManager jreTrustManager,
X509TrustManager customTrustManager) {
return new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
// If you're planning to use client-cert auth,
// merge results from "defaultTm" and "myTm".
return jreTrustManager.getAcceptedIssuers();
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
customTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
// This will throw another CertificateException if this fails too.
jreTrustManager.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// If you're planning to use client-cert auth,
// do the same as checking the server.
jreTrustManager.checkClientTrusted(chain, authType);
}
};
}
private void setSystemTrustManager(X509TrustManager mergedTrustManager)
throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { mergedTrustManager }, null);
// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);
}
正如我所发现的,您还可以使用Apache HttpComponents库中的SSLContextBuilder
类将您的自定义密钥库添加到SSLContext
:
SSLContextBuilder builder = new SSLContextBuilder();
try {
keyStore.load(null, null);
builder.loadTrustMaterial(keyStore, null);
builder.loadKeyMaterial(keyStore, null);
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException
| UnrecoverableKeyException e) {
log.error("Can not load keys from keystore '{}'", keyStore.getProvider(), e);
}
return builder.build();