我正在将SSL客户端连接到我的SSL服务器。 当客户端由于客户端密钥存储区中不存在根证书而无法验证证书时,我需要选择将该证书添加到代码中的本地密钥存储区并继续。 有一些示例总是接受所有证书,但我希望用户验证证书并将其添加到本地密钥库而无需离开应用程序。
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等的东西,我真的不明白它们是如何组合在一起的,或者哪条路是我目标的最短途径。
答案 0 :(得分:19)
您可以使用X509TrustManager实现此目的。
使用
获取SSLContextSSLContext ctx = SSLContext.getInstance("TLS");
然后使用SSLContext#init使用您的自定义X509TrustManager
初始化它。 SecureRandom和KeyManager []可能为null。后者仅在您执行客户端身份验证时才有用,如果在您的方案中只有服务器需要进行身份验证,则无需进行身份验证。
从这个SSLContext中,使用SSLContext#getSocketFactory获取您的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.
*/
}
};
编辑:
Ryan是对的,我忘了解释如何将新root添加到现有的root中。假设您当前的受信任根密钥库来自cacerts
(JDK附带的'Java默认信任库',位于jre / lib / security下)。我假设您使用KeyStore #load(InputStream,char [])加载了该密钥库(它是JKS格式)。
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream in = new FileInputStream("<path to cacerts"");
ks.load(in, "changeit".toCharArray);
cacerts
的默认密码是“changeit”,如果你还没有改变它。
然后您可以使用KeyStore#setEntry添加addtional trusted roots。您可以省略ProtectionParameter(即null),KeyStore.Entry将是TrustedCertificateEntry,它将新根作为参数提供给其构造函数。
KeyStore.Entry newEntry = new KeyStore.TrustedCertificateEntry(newRoot);
ks.setEntry("someAlias", newEntry, null);
如果您希望在某个时候保留更改后的信任存储区,则可以使用KeyStore#store(OutputStream, char[]来实现此目的。
答案 1 :(得分:5)
在JSSE API(负责SSL / TLS的部分)中,检查证书是否可信并不一定涉及KeyStore
。在绝大多数情况下都会出现这种情况,但假设总有一种情况不正确。这是通过TrustManager
完成的。
此TrustManager
可能使用默认信任库KeyStore
或通过javax.net.ssl.trustStore
系统属性指定的信任库,但情况不一定如此,此文件不一定是{{1} (所有这些都取决于JRE安全设置)。
在一般情况下,您无法获取您在应用程序中使用的信任管理器所使用的$JAVA_HOME/jre/lib/security/cacerts
,如果使用默认信任库而没有任何系统属性设置,则更是如此。
即使你能够找出KeyStore
是什么,你仍然会面临两个可能的问题(至少):
KeyStore
可能甚至不是基于文件的(例如,它可能是OSX上的Keychain),您也可能没有写入权限。我建议围绕默认KeyStore
(更具体地说,TrustManager
)编写一个包装器,对默认信任管理器执行检查,如果此初始检查失败,则执行回调到用户界面以检查是否将其添加到“本地”信任库。
如果您想使用this example之类的内容,可以按brief unit test(带jSSLutils)所示完成。
准备X509TrustManager
就像这样:
SSLContext
(当然,您不需要使用此库及其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
对应的套接字不同),因此应该尽可能少地模糊用户操作导致弹出窗口(或者您想要实现回调)。如果建立了多个连接,则不希望验证错误的连接。
似乎可以通过使用一个独立的回调,SSLContext和SSLSocketFactory来解决这个问题,这个SSLSocket应该建立一个新的连接,某种方式将SSLSocket和用户采取的动作绑定到第一个连接尝试的地方。