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

Java SSL连接,以编程方式将服务器证书添加到密钥库,java,ssl,Java,Ssl,我正在将SSL客户端连接到SSL服务器。 当客户端由于根不存在于客户端的密钥存储中而无法验证证书时,我需要在代码中将该证书添加到本地密钥存储并继续。 这里有一些总是接受所有证书的例子,但我希望用户验证证书并将其添加到本地密钥存储,而不离开应用程序 SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket sslsocket = (SSLSocket) sslsoc

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

您可以使用

使用获取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不使用主机名验证而不是证书验证,两者都使用。所有有趣的地方!看到不同的语言如何实现这样的事情,以及有多少种方法可以解决相同的问题,真是令人惊讶。在我的例子中,我碰巧为每个客户机使用一个套接字,以及由系统属性指定的自定义密钥库。浮雕建议的答案似乎最简单地满足了我的要求