Java SSL连接,以编程方式将服务器证书添加到密钥库
我正在将SSL客户端连接到SSL服务器。 当客户端由于根不存在于客户端的密钥存储中而无法验证证书时,我需要在代码中将该证书添加到本地密钥存储并继续。 这里有一些总是接受所有证书的例子,但我希望用户验证证书并将其添加到本地密钥存储,而不离开应用程序Java SSL连接,以编程方式将服务器证书添加到密钥库,java,ssl,Java,Ssl,我正在将SSL客户端连接到SSL服务器。 当客户端由于根不存在于客户端的密钥存储中而无法验证证书时,我需要在代码中将该证书添加到本地密钥存储并继续。 这里有一些总是接受所有证书的例子,但我希望用户验证证书并将其添加到本地密钥存储,而不离开应用程序 SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket sslsocket = (SSLSocket) sslsoc
SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", 23467);
try{
sslsocket.startHandshake();
} catch (IOException e) {
//here I want to get the peer's certificate, conditionally add to local key store, then reauthenticate successfully
}
关于定制SocketFactory、TrustManager、SSLContext等有很多东西,我真的不明白它们是如何结合在一起的,或者哪条路是实现我目标的最短路径。您可以使用 使用获取SSLContext
SSLContext ctx = SSLContext.getInstance("TLS");
然后使用自定义的X509TrustManager
初始化它。SecureRandom和KeyManager[]可能为空。后者仅在执行客户端身份验证时有用,如果在您的场景中只有服务器需要身份验证,则不需要设置
在此SSLContext中,使用SSLSocketFactory并按计划进行
关于X509TrustManager实现,它可能如下所示:
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
//do nothing, you're the client
}
public X509Certificate[] getAcceptedIssuers() {
//also only relevant for servers
}
public void checkServerTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
/* chain[chain.length -1] is the candidate for the
* root certificate.
* Look it up to see whether it's in your list.
* If not, ask the user for permission to add it.
* If not granted, reject.
* Validate the chain using CertPathValidator and
* your list of trusted roots.
*/
}
};
KeyStore keyStore = // ... Create and/or load a keystore from a file
// if you want it to persist, null otherwise.
// In ServerCallbackWrappingTrustManager.CheckServerTrustedCallback
CheckServerTrustedCallback callback = new CheckServerTrustedCallback {
public boolean checkServerTrusted(X509Certificate[] chain,
String authType) {
return true; // only if the user wants to accept it.
}
}
// Without arguments, uses the default key managers and trust managers.
PKIXSSLContextFactory sslContextFactory = new PKIXSSLContextFactory();
sslContextFactory
.setTrustManagerWrapper(new ServerCallbackWrappingTrustManager.Wrapper(
callback, keyStore));
SSLContext sslContext = sslContextFactory.buildSSLContext();
SSLSocketFactory sslSocketFactory = sslContext.getSslSocketFactory();
// ...
// Use keyStore.store(...) if you want to save the resulting keystore for later use.
编辑:
Ryan是对的,我忘了解释如何将新根添加到现有根。让我们假设您当前的受信任根密钥库来自cacerts
(JDK附带的“Java默认信任库”,位于jre/lib/security下)。我假设您使用KeyStore#load(InputStream,char[])加载了密钥存储(它是JKS格式的)
如果您希望在某个时候持久化修改后的信任存储,可以通过在JSSE API(负责SSL/TLS的部分)中使用来实现这一点。检查证书是否受信任不一定涉及密钥库。绝大多数情况下都是这样,但假设总是有一种情况是不正确的。这是通过信任管理器
完成的。
此TrustManager
可能使用默认的信任存储库KeyStore
或通过javax.net.ssl.trustStore
系统属性指定的信任存储库,但情况并非如此,并且此文件不一定是$JAVA\u HOME/jre/lib/security/cacerts
(所有这些都取决于jre安全设置)
在一般情况下,您无法从应用程序中获取您正在使用的信任管理器所使用的密钥库
,如果在没有任何系统属性设置的情况下使用默认信任存储,则更是如此。
即使您能够找到该密钥库是什么,您仍将面临两个可能的问题(至少):
- 您可能无法写入该文件(如果您没有永久保存更改,这不一定是一个问题)
- 这个
KeyStore
甚至可能不是基于文件的(例如,它可能是OSX上的密钥链),您可能也没有对它的写访问权
我建议在默认的TrustManager
(更具体地说,X509TrustManager
)周围编写一个包装器,它对默认的信任管理器执行检查,如果初始检查失败,则执行对用户界面的回调,以检查是否将其添加到“本地”信任存储
如果您想使用类似的方法,可以按照(使用a)中所示的方式完成此操作
准备SSLContext
的过程如下:
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
//do nothing, you're the client
}
public X509Certificate[] getAcceptedIssuers() {
//also only relevant for servers
}
public void checkServerTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
/* chain[chain.length -1] is the candidate for the
* root certificate.
* Look it up to see whether it's in your list.
* If not, ask the user for permission to add it.
* If not granted, reject.
* Validate the chain using CertPathValidator and
* your list of trusted roots.
*/
}
};
KeyStore keyStore = // ... Create and/or load a keystore from a file
// if you want it to persist, null otherwise.
// In ServerCallbackWrappingTrustManager.CheckServerTrustedCallback
CheckServerTrustedCallback callback = new CheckServerTrustedCallback {
public boolean checkServerTrusted(X509Certificate[] chain,
String authType) {
return true; // only if the user wants to accept it.
}
}
// Without arguments, uses the default key managers and trust managers.
PKIXSSLContextFactory sslContextFactory = new PKIXSSLContextFactory();
sslContextFactory
.setTrustManagerWrapper(new ServerCallbackWrappingTrustManager.Wrapper(
callback, keyStore));
SSLContext sslContext = sslContextFactory.buildSSLContext();
SSLSocketFactory sslSocketFactory = sslContext.getSslSocketFactory();
// ...
// Use keyStore.store(...) if you want to save the resulting keystore for later use.
(当然,您不需要使用此库及其SSLContextFactory
,而是实现您自己的X509TrustManager
,“包装”或不使用默认库,如您所愿。)
您必须考虑的另一个因素是用户与此回调的交互。当用户决定单击“接受”或“拒绝”时(例如),握手可能已超时,因此您可能必须在用户接受证书后再次尝试连接
在设计回调时要考虑的另一点是,信任管理器不知道正在使用哪个套接字(与其对应的X509KeyManager
不同),因此应该尽可能少地不清楚是什么用户操作导致了弹出(或者您希望以何种方式实现回调)。如果建立了多个连接,则不希望验证错误的连接。
解决这个问题的方法似乎是使用一个不同的回调函数,即SSLContext和SSLSocketFactory,每个SSLSocket应该建立一个新的连接,通过某种方式将SSLSocket和回调函数绑定到用户首先触发连接尝试的动作上。注意,这是100%不安全的。您完全可以不使用SSL。这对中间人攻击是无效的。@EJP:请解释一下。通常,只有在忽略主机名验证的情况下,才可能使用带有SSL的MITM。这仍然是由DefaultHostnameVerifier在内部执行的。@emboss但谁能说您刚才盲目信任的证书实际上来自它所说的来自的主机?@emboss SSL中没有主机名验证步骤可忽略。它是HTTPS的一部分,而不是SSL。在Java中,它是通过HttpsURLConnection实现的,而不是通过SSLSocket实现的。这里没有证据表明他甚至在使用HTTPS。他所提到的只是SSL。另请参见标签。@emboss我无法理解您的观点。看来是你在这里兜圈子。(1) 他似乎没有使用HTTPS,所以主机名验证不会出现,除非您建议他为其添加手动代码。(2) HTTPS不使用主机名验证而不是证书验证,两者都使用。所有有趣的地方!看到不同的语言如何实现这样的事情,以及有多少种方法可以解决相同的问题,真是令人惊讶。在我的例子中,我碰巧为每个客户机使用一个套接字,以及由系统属性指定的自定义密钥库。浮雕建议的答案似乎最简单地满足了我的要求