【问题标题】:Java SSL connect, add server cert to keystore programmaticallyJava SSL连接,以编程方式将服务器证书添加到密钥库
【发布时间】:2011-10-08 23:17:06
【问题描述】:

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

【问题讨论】:

标签: java ssl


【解决方案1】:

您可以使用X509TrustManager 来实现这一点。

使用

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

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

从此 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 是对的,我忘了解释如何将新根添加到现有根中。假设您当前的受信任根 KeyStore 源自 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 添加其他受信任的根。您可以省略 ProtectionParameter(即 null),KeyStore.Entry 将是一个 TrustedCertificateEntry,它将新根作为其构造函数的参数。

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

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

【讨论】:

  • 请注意,这是 100% 不安全的。您可能根本不使用 SSL。这很容易受到中间人攻击。
  • @EJP:请解释一下。通常只有在您忽略主机名验证时才可以使用带有 SSL 的 MITM。这仍然由 DefaultHostnameVerifier 在内部执行。
  • @emboss 但谁能说你刚刚盲目信任的证书实际上来自它说它来自的主机?
  • @emboss 在 SSL 中没有可以忽略的主机名验证步骤。它是 HTTPS 的一部分,而不是 SSL。在 Java 中,它是由 HttpsURLConnection 实现的,而不是由 SSLSocket 实现的。这里没有证据表明他甚至在使用 HTTPS。他提到的只是SSL。另见标签。
  • @emboss 你的观点让我无法理解。看来你是在这里绕圈子的人。 (1) 他似乎没有使用 HTTPS,因此不会出现主机名验证,除非您建议他为其添加手动代码。 (2) HTTPS 不使用主机名验证代替证书验证,它两者都使用。
【解决方案2】:

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

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

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

我的建议是围绕默认的TrustManager(更具体地说,X509TrustManager)编写一个包装器,它针对默认的信任管理器执行检查,如果初始检查失败,则对用户执行回调-检查是否将其添加到“本地”信任存储的接口。

如果您想使用jSSLutils 之类的东西,可以按照this example(带有brief unit test)所示完成。

准备您的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 不同),因此对于用户的操作应该尽可能少含糊导致该弹出窗口(或者您想要实现回调)。如果建立了多个连接,您将不想验证错误的连接。 似乎可以通过对每个 SSLSocket 使用不同的回调、SSLContext 和 SSLSocketFactory 来解决这个问题,这应该建立一个新的连接,某种绑定 SSLSocket 的方式以及用户为触发该连接尝试而采取的操作的回调地点。

【讨论】:

  • 所有有趣的点!看到不同的语言如何实现这样的事情,以及有多少种方法可以解决相同的问题,真是令人惊讶。就我而言,我碰巧为每个客户端使用一个套接字,以及由系统属性指定的自定义密钥库。 emboss 建议的答案似乎最简单地满足了我对这项任务的要求,但我很高兴知道一些更大的图景以及它可能如何影响这个和未来的项目。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-13
  • 1970-01-01
  • 2013-01-02
  • 2013-06-15
  • 2021-11-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多