使用JSSE时如何进行主机名验证? [英] How should I do hostname validation when using JSSE?

查看:113
本文介绍了使用JSSE时如何进行主机名验证?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在用Java编写一个客户端(需要在桌面JRE和Android上工作)以获得通过TLS承载的专有协议(特定于我的公司)。我正在尝试找出用Java编写TLS客户端的最佳方法,特别是确保它正确地进行主机名验证。 (编辑:通过它,我的意思是检查主机名是否与X.509证书匹配,以避免中间人攻击。)

I'm writing a client in Java (needs to work both on the desktop JRE and on Android) for a proprietary protocol (specific to my company) carried over TLS. I'm trying to figure out the best way to write a TLS client in Java, and in particular, make sure that it does hostname validation properly. ( By which, I mean checking that the hostname matches the X.509 certificate, to avoid man-in-the-middle attacks.)

JSSE是编写TLS客户端的明显API,但我注意到了世界上最危险的代码文章(以及实验)JSSE在使用SSLSocketFactory API时不验证主机名。 (这是我必须使用的,因为我的协议不是HTTPS。)

JSSE is the obvious API for writing a TLS client, but I noticed from the "Most Dangerous Code in the World" paper (as well as from experimentation) that JSSE doesn't validate the hostname when one is using the SSLSocketFactory API. (Which is what I have to use, since my protocol is not HTTPS.)

因此,看来在使用JSSE时,我必须自己进行主机名验证。而且,不是从头开始编写代码(因为我几乎肯定会弄错),似乎我应该借用一些有效的现有代码。所以,我发现的最有可能的候选者是使用Apache HttpComponents库(具有讽刺意味,因为我实际上并不是在做HTTP)并使用org.apache.http.conn.ssl.SSLSocketFactory类代替标准的javax.net.ssl.SSLSocketFactory类

So, it appears that when using JSSE, I have to do hostname validation myself. And, rather than writing that code from scratch (since I would almost certainly get it wrong), it seems that I should "borrow" some existing code that works. So, the most likely candidate I've found is to use the Apache HttpComponents library (ironic, since I'm not actually doing HTTP) and use the org.apache.http.conn.ssl.SSLSocketFactory class in place of the standard javax.net.ssl.SSLSocketFactory class.

我的问题是:这是一个合理的行动方案吗?或者我完全误解了事情,走到了尽头,实际上有一种更简单的方法可以在JSSE中获得主机名验证,而不需要像HttpComponents那样引入第三方库?

My question is: is this a reasonable course of action? Or have I completely misunderstood things, gone off the deep end, and there's actually a much easier way to get hostname validation in JSSE, without pulling in a third-party library like HttpComponents?

我也看过BouncyCastle,它有一个非JSSE的TLS API,但它似乎更有限,因为它甚至没有进行证书链验证,更不用说主机名验证,所以看起来似乎就像一个非首发。

I also looked at BouncyCastle, too, which has a non-JSSE API for TLS, but it appears to be even more limited, in that it doesn't even do certificate chain validation, much less hostname validation, so it seemed like a non-starter.

编辑:这个问题,但我仍然很好奇Java 6和Android的最佳实践是什么。 (特别是,我必须为我的应用程序支持Android。)

This question has been answered for Java 7, but I'm still curious what the "best practice" is for Java 6 and Android. (In particular, I have to support Android for my application.)

再次编辑:提出我的建议从Apache HttpComponents借用更具体的是,我创建了一个小型库,其中包含HostnameVerifier实现(最值得注意的是StrictHostnameVerifier和BrowserCompatHostnameVerifier)从Apache HttpComponents中提取。 (我意识到我需要的只是验证器,我不需要像我原先想的那样使用Apache的SSLSocketFactory。)如果留给我自己的设备,这就是我将使用的解决方案。但首先,有什么理由我不应该这样做吗? (假设我的目标是以与https相同的方式进行主机名验证。我意识到它本身是有争议的,并且已经在加密列表的线程中进行了讨论,但是现在我坚持使用类似HTTPS的主机名验证,即使我没有使用HTTPS。)

Edited again: To make my proposal of "borrow from Apache HttpComponents" more concrete, I've created a small library which contains the HostnameVerifier implementations (most notably StrictHostnameVerifier and BrowserCompatHostnameVerifier) extracted from Apache HttpComponents. (I realized all I need are the verifiers, and I don't need Apache's SSLSocketFactory as I was originally thinking.) If left to my own devices, this is the solution I will use. But firstly, is there any reason I shouldn't do it this way? (Assuming that my goal is to do my hostname validation the same way https does. I realize that itself is open to debate, and has been discussed in the thread on the cryptography list, but for now I'm sticking with HTTPS-like hostname validation, even though I'm not doing HTTPS.)

假设我的解决方案没有错误,我的问题是:有没有更好的方法这样做,同时仍然可以在Java 6,Java 7和Android上保持可移植性? (其中更好意味着更惯用,已经广泛使用,和/或需要更少的外部代码。)

Assuming there's nothing "wrong" with my solution, my question is this: is there a "better" way to do it, while still remaining portable across Java 6, Java 7, and Android? (Where "better" means more idiomatic, already widely in use, and/or needing less external code.)

推荐答案

Java 7 (及以上)



您可以隐式使用Java 7中引入的 X509ExtendedTrustManager (参见这个答案

SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
sslSocket.setSSLParameters(sslParams); // also works on SSLEngine



Android



我对Android不太熟悉,但是Apache HTTP Client应该捆绑它,所以它是不是真正的附加库。因此,您应该能够使用 org.apache.http.conn.ssl.StrictHostnameVerifier 。(我还没有尝试过这段代码。)

Android

I'm less familiar with Android, but Apache HTTP Client should be bundled with it, so it's not really an additional library. As such, you should be able to use org.apache.http.conn.ssl.StrictHostnameVerifier. (I haven't tried this code.)

SSLSocketFactory ssf = (SSLSocketFactory) SSLSocketFactory.getDefault();
// It's important NOT to resolve the IP address first, but to use the intended name.
SSLSocket socket = (SSLSocket) ssf.createSocket("my.host.name", 443);

socket.startHandshake();
SSLSession session = socket.getSession();

StrictHostnameVerifier verifier = new StrictHostnameVerifier();
if (!verifier.verify(session.getPeerHost(), session)) {
    // throw some exception or do something similar.
}



其他



不幸的是,验证者需要手动实现。 Oracle JRE显然有一些主机名验证器实现,但据我所知,它不能通过公共API获得。

Other

Unfortunately, the verifier needs to be implemented manually. The Oracle JRE obviously has some host name verifier implementation, but as far as I'm aware, it's not available via the public API.

有关规则的更多细节在最近的答案中。

There are more details about the rules in this recent answer.

这是一个实现我'写了。它肯定可以通过审核...评论和反馈欢迎。

Here is an implementation I've written. It could certainly do with being reviewed... Comments and feedback welcome.

public void verifyHostname(SSLSession sslSession)
        throws SSLPeerUnverifiedException {
    try {
        String hostname = sslSession.getPeerHost();
        X509Certificate serverCertificate = (X509Certificate) sslSession
                .getPeerCertificates()[0];

        Collection<List<?>> subjectAltNames = serverCertificate
                .getSubjectAlternativeNames();

        if (isIpv4Address(hostname)) {
            /*
             * IP addresses are not handled as part of RFC 6125. We use the
             * RFC 2818 (Section 3.1) behaviour: we try to find it in an IP
             * address Subject Alt. Name.
             */
            for (List<?> sanItem : subjectAltNames) {
                /*
                 * Each item in the SAN collection is a 2-element list. See
                 * <a href=
                 * "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29"
                 * >X509Certificate.getSubjectAlternativeNames()</a>. The
                 * first element in each list is a number indicating the
                 * type of entry. Type 7 is for IP addresses.
                 */
                if ((sanItem.size() == 2)
                        && ((Integer) sanItem.get(0) == 7)
                        && (hostname.equalsIgnoreCase((String) sanItem
                                .get(1)))) {
                    return;
                }
            }
            throw new SSLPeerUnverifiedException(
                    "No IP address in the certificate did not match the requested host name.");
        } else {
            boolean anyDnsSan = false;
            for (List<?> sanItem : subjectAltNames) {
                /*
                 * Each item in the SAN collection is a 2-element list. See
                 * <a href=
                 * "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29"
                 * >X509Certificate.getSubjectAlternativeNames()</a>. The
                 * first element in each list is a number indicating the
                 * type of entry. Type 2 is for DNS names.
                 */
                if ((sanItem.size() == 2)
                        && ((Integer) sanItem.get(0) == 2)) {
                    anyDnsSan = true;
                    if (matchHostname(hostname, (String) sanItem.get(1))) {
                        return;
                    }
                }
            }

            /*
             * If there were not any DNS Subject Alternative Name entries,
             * we fall back on the Common Name in the Subject DN.
             */
            if (!anyDnsSan) {
                String commonName = getCommonName(serverCertificate);
                if (commonName != null
                        && matchHostname(hostname, commonName)) {
                    return;
                }
            }
            throw new SSLPeerUnverifiedException(
                    "No host name in the certificate did not match the requested host name.");
        }
    } catch (CertificateParsingException e) {
        /*
         * It's quite likely this exception would have been thrown in the
         * trust manager before this point anyway.
         */
        throw new SSLPeerUnverifiedException(
                "Unable to parse the remote certificate to verify its host name: "
                        + e.getMessage());
    }
}

public boolean isIpv4Address(String hostname) {
    String[] ipSections = hostname.split("\\.");
    if (ipSections.length != 4) {
        return false;
    }
    for (String ipSection : ipSections) {
        try {
            int num = Integer.parseInt(ipSection);
            if (num < 0 || num > 255) {
                return false;
            }
        } catch (NumberFormatException e) {
            return false;
        }
    }
    return true;
}

public boolean matchHostname(String hostname, String certificateName) {
    if (hostname.equalsIgnoreCase(certificateName)) {
        return true;
    }
    /*
     * Looking for wildcards, only on the left-most label.
     */
    String[] certificateNameLabels = certificateName.split(".");
    String[] hostnameLabels = certificateName.split(".");
    if (certificateNameLabels.length != hostnameLabels.length) {
        return false;
    }
    /*
     * TODO: It could also be useful to check whether there is a minimum
     * number of labels in the name, to protect against CAs that would issue
     * wildcard certificates too loosely (e.g. *.com).
     */
    /*
     * We check that whatever is not in the first label matches exactly.
     */
    for (int i = 1; i < certificateNameLabels.length; i++) {
        if (!hostnameLabels[i].equalsIgnoreCase(certificateNameLabels[i])) {
            return false;
        }
    }
    /*
     * We allow for a wildcard in the first label.
     */
    if ("*".equals(certificateNameLabels[0])) {
        // TODO match wildcard that are only part of the label.
        return true;
    }
    return false;
}

public String getCommonName(X509Certificate cert) {
    try {
        LdapName ldapName = new LdapName(cert.getSubjectX500Principal()
                .getName());
        /*
         * Looking for the "most specific CN" (i.e. the last).
         */
        String cn = null;
        for (Rdn rdn : ldapName.getRdns()) {
            if ("CN".equalsIgnoreCase(rdn.getType())) {
                cn = rdn.getValue().toString();
            }
        }
        return cn;
    } catch (InvalidNameException e) {
        return null;
    }
}

/* BouncyCastle implementation, should work with Android. */
public String getCommonName(X509Certificate cert) {
    String cn = null;
    X500Name x500name = X500Name.getInstance(cert.getSubjectX500Principal()
            .getEncoded());
    for (RDN rdn : x500name.getRDNs(BCStyle.CN)) {
        // We'll assume there's only one AVA in this RDN.
        cn = IETFUtils.valueToString(rdn.getFirst().getValue());
    }
    return cn;
}

有两个 getCommonName 实现:一个使用 javax.naming.ldap ,一个使用BouncyCastle,具体取决于可用的内容。

There are two getCommonName implementations: one using javax.naming.ldap and one using BouncyCastle, depending on what's available.

主要细微之处在于:


  • 仅在SAN中匹配IP地址(此问题是关于IP地址匹配和主题备用名称。)。或许也可以做一些关于IPv6匹配的事情。

  • 通配符匹配。

  • 如果没有DNS SAN,则只返回CN。

  • 最具体的CN真正意味着什么。我以为这是最后一个。 (我甚至没有考虑具有多个属性值断言(AVA)的单个CN RDN:BouncyCastle可以处理它们,但据我所知,这是极其罕见的情况。)

  • 我还没有检查国际化(非ASCII)域名应该发生什么(参见 RFC 6125 。)

  • Matching IP address only in SANs (This question is about IP address matching and Subject Alternative Names.). Perhaps something could be done about IPv6 matching too.
  • Wildcard matching.
  • Only falling back on the CN if there is no DNS SAN.
  • What the "most specific" CN really means. I've assumed this is the last one here. (I'm not even considering a single CN RDN with multiple Attribute-Value Assertions (AVA): BouncyCastle can deal with them, but this is an extremely rare case anyway as far as I know.)
  • I haven't checked at all what should happen for internationalised (non-ASCII) domain names (see RFC 6125.)

编辑


为了使我的从Apache HttpComponents借更多
具体,我创建了一个包含
的小型库从Apache
HttpComponents中提取的HostnameVerifier实现(最值得注意的是StrictHostnameVerifier
和BrowserCompatHostnameVerifier)。 [...]但首先,有什么理由我不应该以这种方式做

To make my proposal of "borrow from Apache HttpComponents" more concrete, I've created a small library which contains the HostnameVerifier implementations (most notably StrictHostnameVerifier and BrowserCompatHostnameVerifier) extracted from Apache HttpComponents. [...] But firstly, is there any reason I shouldn't do it this way?

是的,有理由不这样做。

Yes, there are reasons not to do it this way.

首先,你已经有效地分叉了一个库,你现在必须维护它,这取决于所做的进一步改动在原始Apache HttpComponents中的这些类。我并不反对创建一个图书馆(我自己这样做了,我并不劝你这样做),但你必须考虑到这一点。你真的想节省一些空间吗?当然,如果您需要回收空间,有些工具可以删除最终产品的未使用代码(请注意ProGuard)。

Firstly, you've effectively forked a library, and you'll now have to maintain it, depending on further changes made to these classes in the original Apache HttpComponents. I'm not against creating a library (I've done so myself, and I'm not discouraging you to do so), but you have to take this into account. Are you really trying to save some space? Surely, there are tools that can remove unused code for your final product if you need to reclaim space (ProGuard comes to mind).

其次,即使是StrictHostnameVerifier也不是符合RFC 2818或RFC 6125.据我所知,它的代码:

Secondly, even the StrictHostnameVerifier isn't compliant with RFC 2818 or RFC 6125. As far as I can tell from its code:

  • It will accept IP addresses in the CN, when it shouldn't.
  • It will not just fall back on the CN when no DNS SANs are present, but also treat the CN as a first choice too. This could lead to a cert with CN=cn.example.com and a SAN for www.example.com but no SAN for cn.example.com be valid for cn.example.com when it shouldn't.
  • I'm a bit sceptical about the way the CN is extracted. Subject DN string serialisation can be a bit funny, especially if some RDNs include commas, and the awkward case where some RDNs can have multiple AVAs.

很难看到一般的更好的方式。将此反馈提供给Apache HttpComponents库当然是一种方法。复制和粘贴我上面写的代码肯定听起来不是一个好方法(SO上的代码片段通常不会被维护,不会100%经过测试并且可能容易出错)。

It's hard to see a general "better way". Giving this feedback to the Apache HttpComponents library would be one way of course. Copying and pasting the code I wrote earlier above certainly doesn't sound like a good way either (snippets of code on SO generally aren't maintained, are not 100% tested and may be prone to errors).

更好的方法可能是尝试说服Android开发团队支持相同的 SSLParameters X509ExtendedTrustManager 就像为Java 7所做的那样。这仍然留下遗留设备的问题。

A better way might be to try to try to convince the Android development team to support the same SSLParameters and X509ExtendedTrustManager as it was done for Java 7. This still leaves the issue of legacy devices.

这篇关于使用JSSE时如何进行主机名验证?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆