试图理解 Java 中的 SSL 证书

Posted

技术标签:

【中文标题】试图理解 Java 中的 SSL 证书【英文标题】:Trying to understand SSL certificates in Java 【发布时间】:2014-03-28 02:14:58 【问题描述】:

首先,我没有良好的(阅读:任何)安全背景,我认为缺乏理解可能是这个问题的根源。

我正在使用来自here 的一些代码来处理设置RTMPS 连接。基本上,我希望了解它是如何工作的,虽然我对 RTMP 协议没有任何问题,但我很难理解为什么 SSL 代码是这样编写的。它似乎会从服务器获取证书,然后将其存储在 cacerts 文件或 cents 文件夹中。

我一直在尝试跟踪连接过程以查看证书在哪里被检索和存储,但一直无法弄清楚。这是一段代码,据我所知,它的行为正常:

注意:SavingTrustManager 只是 X509TrustManager 的包装器,它还存储证书链。

/**
 * Opens the socket with the default or a previously saved certificate
 *
 * @return A special TrustManager to save the certificate if necessary
 * @throws IOException
 */
private SavingTrustManager openSocketWithCert() throws IOException 
try 
    // Load the default KeyStore or a saved one
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    File file = new File("certs/" + server + ".cert");
    if (!file.exists() || !file.isFile())
        file = new File(System.getProperty("java.home") + "/lib/security/cacerts");

    InputStream in = new FileInputStream(file);
    ks.load(in, passphrase);

    // Set up the socket factory with the KeyStore
    SSLContext context = SSLContext.getInstance("TLS");
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ks);
    X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
    SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
    context.init(null, new TrustManager[]  tm , null);
    SSLSocketFactory factory = context.getSocketFactory();

    sslsocket = (SSLSocket)factory.createSocket(server, port);

    return tm;

catch (Exception e) 
    // Hitting an exception here is very bad since we probably won't
    // recover
    // (unless it's a connectivity issue)

    // Rethrow as an IOException
    throw new IOException(e.getMessage());



/**
 * Downloads and installs a certificate if necessary
 *
 * @throws IOException
 */
private void getCertificate() throws IOException 
try 
    SavingTrustManager tm = openSocketWithCert();

    // Try to handshake the socket
    boolean success = false;
    try 
        sslsocket.startHandshake();
        success = true;
    
    catch (SSLException e) 
        sslsocket.close();
    

    // If we failed to handshake, save the certificate we got and try
    // again
    if (!success) 
        // Set up the directory if needed
        File dir = new File("certs");
        if (!dir.isDirectory()) 
            dir.delete();
            dir.mkdir();
        

        // Reload (default) KeyStore
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        File file = new File(System.getProperty("java.home") + "/lib/security/cacerts");

        InputStream in = new FileInputStream(file);
        ks.load(in, passphrase);

        // Add certificate
        X509Certificate[] chain = tm.chain;
        if (chain == null)
            throw new Exception("Failed to obtain server certificate chain");

        X509Certificate cert = chain[0];
        String alias = server + "-1";
        ks.setCertificateEntry(alias, cert);

        // Save certificate
        OutputStream out = new FileOutputStream("certs/" + server + ".cert");
        ks.store(out, passphrase);
        out.close();
        System.out.println("Installed cert for " + server);
    

catch (Exception e) 
    // Hitting an exception here is very bad since we probably won't
    // recover
    // (unless it's a connectivity issue)

    // Rethrow as an IOException
    e.printStackTrace();
    throw new IOException(e.getMessage());



/**
 * Attempts to connect given the previous connection information
 *
 * @throws IOException
 */
public void connect() throws IOException 
try 
    sslsocket = (SSLSocket)SSLSocketFactory.getDefault().createSocket(server, port);
    in = new BufferedInputStream(sslsocket.getInputStream());
    out = new DataOutputStream(sslsocket.getOutputStream());

    doHandshake();

catch (IOException e) 
    // If we failed to set up the socket, assume it's because we needed
    // a certificate
    getCertificate();
    // And use the certificate
    openSocketWithCert();

    // And try to handshake again
    in = new BufferedInputStream(sslsocket.getInputStream());
    out = new DataOutputStream(sslsocket.getOutputStream());

    doHandshake();


// Start reading responses
pr = new RTMPPacketReader(in);

// Handle preconnect Messages?
// -- 02 | 00 00 00 | 00 00 05 | 06 00 00 00 00 | 00 03 D0 90 02

// Connect
Map<String, Object> params = new HashMap<String, Object>();
params.put("app", app);
params.put("flashVer", "WIN 10,1,85,3");
params.put("swfUrl", swfUrl);
params.put("tcUrl", "rtmps://" + server + ":" + port);
params.put("fpad", false);
params.put("capabilities", 239);
params.put("audioCodecs", 3191);
params.put("videoCodecs", 252);
params.put("videoFunction", 1);
params.put("pageUrl", pageUrl);
params.put("objectEncoding", 3);

byte[] connect = aec.encodeConnect(params);

out.write(connect, 0, connect.length);
out.flush();

while (!results.containsKey(1))
    sleep(10);
TypedObject result = results.get(1);
DSId = result.getTO("data").getString("id");

connected = true;

我想我无法了解服务器证书的收集方式。从我的角度来看,似乎从未调用过 getCertificate() 方法。这是因为证书已经存储在某个地方了吗?没有 certs 目录,所以我假设它在口音文件中。如果我清除了我的 cacerts 文件,它会被调用吗?我觉得那是个坏主意。

很抱歉问了这么一个含糊的问题,过去几天我一直在思考这段代码,并且对现有资源(javadocs 等)不太满意。谢谢!

【问题讨论】:

SO 不适合提出含糊的问题,当然也不适合我们通过对话询问您“以评估您的理解”。您实际上是在要求一对一的教程,尽管这里有人可以做到这一点,但他们没有责任通过 SO 做到这一点。我建议您从代码中退后一步,思考(或阅读)SSL 的整体用途以及它对代码的要求,然后在相关 API 文档的帮助下尝试解释上述内容。然后形成一个具体的问题,例如“他们为什么做 A 而不是 B?”。 我会尽量减少。 【参考方案1】:

SSLSocket 对象可以访问它应该信任的证书,因为它是由 SSLContext 间接创建的,而 SSLContext 已由具有本地信任证书的 KeyStore 对象间接初始化。当在 SSLSocket 对象上调用 connect() 时,它会从服务器检索证书,然后可以将其与 KeyStore 对象中的证书进行比较,以确定证书链是否合法。所有这一切都发生在幕后,因此作为应用程序程序员的您无需担心代码中的证书。这就是为什么示例代码的 getCertificate() 方法在正常情况下不会被调用的原因。

但是,当身份验证失败时,示例代码会在其 getCertificate() 方法中获取服务器的证书并将其安装在本地密钥库中。下一次连接服务器的尝试通常会成功,因为服务器将发送的证书的新副本将与刚刚安装在密钥库中的副本相匹配。

当然,这会破坏整个身份验证方案,因为最终此示例代码会信任服务器发送的任何证书,而无需对其进行验证。这段代码的作者很可能并不关心身份验证,只是想使用 SSL 连接,即使它是到一个虚假的服务器。这当然是大多数人在浏览具有无法识别的证书的站点时所做的,并且只是接受该证书。

该代码的坏处在于它将未经验证的证书安装在本地计算机的主 JRE 密钥库中。这意味着机器上运行的所有其他 Java 应用程序将从此信任未经验证的证书,这是一个主要的安全漏洞。您可能希望避免在自己的代码中这样做。

【讨论】:

好的!我认为这对我来说很清楚。如果他们创建一个简单地接受任何连接的 TrustManager 会更好/等效吗? 如果“他们”是指编写示例代码的人,如果他们遇到不受信任的服务器证书,最好至少需要用户干预,而不是以编程方式信任它。但是,创建和使用接受所有证书的 TrustManager,同时将它们暴露给中间人攻击,至少不会使其他应用程序暴露于这些攻击。 这正是我的意思。感谢您帮助澄清一些事情。【参考方案2】:
Below key points you can google and understand.

1. Trusted certificates and keystores/truststores
2. Concept of private and public key. How browser send the certificate key to server and it gets authenticated.
3. Keytool utility, to convert certificates from one form to another.
4. Importing the trusted certificates and keys to trustore, though java has its "cacerts" file by default, but you can create you own and import certificates into it.
5. Explore certificates, read about thumbprint, algorithms used
6. Create you own custom factory as well.

【讨论】:

以上是关于试图理解 Java 中的 SSL 证书的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 客户端中接受服务器的自签名 ssl 证书

将 SSL 证书添加到 JRE 以访问 HTTPS 站点

SSL证书常见的问题汇总

在java中怎么验证ssl证书的有效性

SSL:如何保护证书免受中间人攻击?

带有 iOS 应用程序的 SSL 证书