Java SSL connect,以编程方式将服务器证书添加到密钥库

我正在将SSL客户端连接到我的SSL服务器。 当客户端由于根目录不存在于客户端的密钥存储区中而无法validation证书时,我需要选择将该证书添加到代码中的本地密钥存储区并继续。 有一些示例始终接受所有证书,但我希望用户validation证书并将其添加到本地密钥库而无需离开应用程序。

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等的东西,我真的不明白它们是如何组合在一起的,或者哪条是我目标的最短路径。

您可以使用X509TrustManager实现此function。

获取SSLContext

 SSLContext ctx = SSLContext.getInstance("TLS"); 

然后使用SSLContext#init使用您的自定义X509TrustManager初始化它。 SecureRandom和KeyManager []可能为null。 后者仅在您执行客户端身份validation时才有用,如果在您的方案中只有服务器需要进行身份validation,则无需进行身份validation。

从这个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(" 

cacerts的默认密码是“changeit”,如果你还没有改变它。

然后,您可以使用KeyStore#setEntry添加addtional受信任的根。 您可以省略ProtectionParameter(即null),KeyStore.Entry将是一个TrustedCertificateEntry ,它将新根作为参数提供给其构造函数。

 KeyStore.Entry newEntry = new KeyStore.TrustedCertificateEntry(newRoot); ks.setEntry("someAlias", newEntry, null); 

如果您希望在某些时候保留更改的信任存储,可以使用KeyStore #store(OutputStream,char [])实现此目的。

在JSSE API(负责SSL / TLS的部分)中,检查证书是否可信并不一定涉及KeyStore 。 在绝大多数情况下都会出现这种情况,但假设总有一种情况不正确。 这是通过TrustManager完成的。 此TrustManager可能使用默认信任库KeyStore或通过javax.net.ssl.trustStore系统属性指定的那个,但不一定是这种情况,并且此文件不一定是$JAVA_HOME/jre/lib/security/cacerts (所有这些取决于JRE安全设置)。

在一般情况下,您无法获取您在应用程序中使用的信任管理器所使用的KeyStore ,如果使用默认信任库而没有任何系统属性设置,则更是如此。 即使你能够找到KeyStore是什么,你仍然会面临两个可能的问题(至少):

  • 您可能无法写入该文件(如果您未永久保存更改,则不一定会出现问题)。
  • 这个KeyStore甚至可能不是基于文件的(例如,它可能是OSX上的Keychain),您也可能没有写入权限。

我建议的是在默认的TrustManager (更具体地说, X509TrustManager )周围编写一个包装器,它对默认的信任管理器执行检查,如果初始检查失败,则执行对用户界面的回调以检查是否添加它到“本地”信托商店。

如果您想使用类似jSSLutils的内容 ,可以按照此示例中所示(使用简短的unit testing )完成此 操作 。

准备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 ,“包装”或不是默认的。)

您必须考虑的另一个因素是用户与此回调的交互。 当用户决定单击“接受”或“拒绝”(例如)时,握手可能已超时,因此您可能必须在用户接受证书时再次尝试连接。

在回调设计中需要考虑的另一点是信任管理器不知道正在使用哪个套接字(与X509KeyManager对应的不同),因此应该对用户操作导致该弹出窗口产生的模糊性一样少。 (或者你想要实现回调)。 如果建立了多个连接,则不希望validation错误的连接。 似乎有可能通过使用一个独立的回调,SSLContext和SSLSocketFactory来解决这个问题,这个SSLSocket应该建立一个新连接,某种方式将SSLSocket和用户采取的动作绑定到第一个连接尝试地点。