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

2022-09-01 13:19:35

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


答案 1

您可以使用 X509TrustManager 实现此功能。

获取 SSLContext

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

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

从这个SSLContext,使用SSLContext#getSocketFactory获取你的SSSocketFactory,并按计划进行。

至于您的 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是对的,我忘了解释如何将新根添加到现有根中。让我们假设您当前的可信根的 KeyStore 派生自(JDK 附带的“Java 默认信任存储”,位于 jre/lib/security 下)。我假设你用KeyStore#load(InputStream,char[])加载了该密钥存储(它是JKS格式的)。cacerts

KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream in = new FileInputStream("<path to cacerts"");
ks.load(in, "changeit".toCharArray);

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

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

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

如果你想在某个时候保留更改的信任存储,你可以使用KeyStore#store(OutputStream,char[])来实现这一点。


答案 2

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

在一般情况下,您无法从应用程序获取您正在使用的信任管理器所使用的信任管理器,如果使用默认信任存储区而不进行任何系统属性设置,则更是如此。即使您能够找出那是什么,您仍然会面临两个可能的问题(至少):KeyStoreKeyStore

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

我的建议是围绕缺省信任管理器编写一个包装器(更具体地说,),它对缺省信任管理器执行检查,如果此初始检查失败,则对用户界面执行回调,以检查是否将其添加到“本地”信任存储区。TrustManagerX509TrustManager

这可以按照此示例中所示完成(带有简短的单元测试),如果你想使用类似jSSLutils的东西。

准备你的将是这样的: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.

(当然,您不需要使用此库及其 ,而是实现自己的“包装”或不是默认库,如您所愿。SSLContextFactoryX509TrustManager

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

在回调设计中要考虑的另一点是,信任管理器不知道正在使用哪个套接字(与它的对应项不同),因此对于导致该弹出窗口的用户操作(或者您希望实现回调的方式)应该尽可能少的歧义。如果建立了多个连接,则您不希望验证错误的连接。似乎可以通过为每个SSSocket使用不同的回调,SSLContext和SSSocketFactory来解决这个问题,这应该建立一个新的连接,某种方式将SSSLSocket和回调到用户首先为触发该连接尝试而执行的操作。X509KeyManager