Java 使用JSSE时,我应该如何进行主机名验证?

Java 使用JSSE时,我应该如何进行主机名验证?,java,ssl,jsse,Java,Ssl,Jsse,我正在用Java编写一个客户端(需要在桌面JRE和Android上工作),用于TLS附带的专有协议(特定于我的公司)。我试图找出用Java编写TLS客户机的最佳方法,特别是确保它正确地进行主机名验证。(编辑:我的意思是检查主机名是否与X.509证书匹配,以避免中间人攻击。) JSSE显然是编写TLS客户端的API,但我从“”论文(以及实验)中注意到,当使用SSLSocketFactory API时,JSSE不会验证主机名。(这是我必须使用的,因为我的协议不是HTTPS。) 因此,在使用JSSE时

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

JSSE显然是编写TLS客户端的API,但我从“”论文(以及实验)中注意到,当使用SSLSocketFactory API时,JSSE不会验证主机名。(这是我必须使用的,因为我的协议不是HTTPS。)

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

我的问题是:这是一个合理的行动方案吗?或者,我完全误解了事情的本质,实际上有一种更简单的方法可以在JSSE中进行主机名验证,而无需引入第三方库(如HttpComponents)

我还研究了BouncyCastle,它有一个用于TLS的非JSSE API,但它似乎更有限,因为它甚至不进行证书链验证,更不用说主机名验证,所以它看起来像是一个非初学者

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

再次编辑:为了使我关于“借用Apache HttpComponents”的建议更具体,我创建了一个包含从Apache HttpComponents中提取的HostnameVerifier实现(最著名的是StrictHostnameVerifier和BrowserCompatHostnameVerifier)的。(我意识到我需要的只是验证器,我不需要Apache的SSLSocketFactory,正如我最初所想的那样。)如果留给我自己的设备,这就是我将使用的解决方案。但首先,有什么理由我不应该这样做吗?(假设我的目标是以https相同的方式进行主机名验证。我意识到这一点值得商榷,并且已经在加密列表的线程中讨论过,但目前我坚持使用https式主机名验证,即使我没有使用https。)

假设我的解决方案没有“错误”,我的问题是:有没有“更好”的方法来做到这一点,同时仍然可以在Java6、Java7和Android上移植?(其中“更好”表示更地道、已经广泛使用和/或需要更少的外部代码。)

Java7(及以上版本) 您可以隐式使用Java 7中引入的
X509ExtendedTrustManager
(请参阅:

安卓 我对Android不太熟悉,但Apache HTTP客户端应该与它捆绑在一起,因此它不是一个真正的附加库。因此,您应该能够使用
org.Apache.HTTP.conn.ssl.StrictHostnameVerifier
(我没有尝试过此代码)

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

中有关于规则的更多详细信息

这是我写的一个实现。它肯定与被审查有关…欢迎评论和反馈

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;
}
public void验证主机名(SSLSession SSLSession)
抛出SSLPeerUnverifiedException{
试一试{
字符串hostname=sslSession.getPeerHost();
X509Certificate服务器证书=(X509Certificate)sslSession
.getPeerCertificates()[0];

集合要求jsse客户端(您)提供您自己的StrictHostnameVerifier有很多很好的理由。如果您信任公司的名称服务器,编写一个应该非常简单

  • 从您的主机所在的DNS提供商处获取主机名的IP 配置为使用
  • 返回匹配名称的结果
  • (可选)验证IP的反向查找是否返回 正确的名字
  • 如果你需要,我会给你提供一个验证器。
    如果你想让我提供“好的理由”,我也可以这样做。

    我真的不明白为什么这一点会被如此严重地否决,并被搁置下来作为离题。如何在使用JSSE时实现主机名验证显然是离题的!(问题还清楚地提到考虑了哪些API。)哪里是问这个问题的合适地方?我还没有找到JSSE的邮件列表。有吗?在等待(希望)重新打开这个问题时,您可能会对此感兴趣(这会给您一个Java 7的答案):谢谢@Bruno!这实际上是对我的问题的一个有用的回答。(不幸的是,我以前在搜索答案时没有找到这个线索。)也就是说,我可能无法使用它,因为尽管我可能可以不说我们必须在桌面上使用Java 7而不是Java 6,但问题是我还必须支持Android,我打赌Android不支持新的Java 7,因为它实际上并不符合Java。但现在也许答案是我应该在Android特定论坛上问这个问题。这个问题根本不是离题。这是一个重要的问题,在文档中没有得到很好的解决。虽然这个问题涉及到他的代码,但问题不是关于他的实现:而是关于如何解决使用Java SSL impl的任何人所面临的一般问题
    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.
    }
    
    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;
    }