Подтвердить что ты не робот

Как я должен проверять имя хоста при использовании JSSE?

Я пишу клиента на Java (должен работать как на рабочем столе JRE, так и на Android) для проприетарного протокола (определенного для моей компании), перенесенного через TLS. Я пытаюсь найти лучший способ написать клиент TLS в Java и, в частности, убедиться, что он правильно проверяет имя хоста. ( Изменить:. Под этим я подразумеваю проверку того, что имя хоста соответствует сертификату X.509, чтобы избежать атак типа "человек в середине".)

JSSE - это очевидный API для написания клиента TLS, но я заметил из документа Самый опасный код в мире" (как а также от экспериментов), что JSSE не проверяет имя хоста при использовании SSLSocketFactory API. (Это то, что я должен использовать, поскольку мой протокол не HTTPS.)

Итак, похоже, что при использовании JSSE я должен сам проверять имя хоста. И, вместо того, чтобы писать этот код с нуля (поскольку я почти наверняка ошибаюсь), кажется, что я должен "заимствовать" какой-то существующий код, который работает. Итак, наиболее вероятным кандидатом, который я нашел, является использование библиотеки Apache HttpComponents (иронично, поскольку я фактически не выполняю HTTP) и использую класс org.apache.http.conn.ssl.SSLSocketFactory вместо стандартный класс javax.net.ssl.SSLSocketFactory.

Мой вопрос: это разумный ход действий? Или я полностью неправильно понял, ушел с глубокого конца, и на самом деле гораздо проще получить подтверждение имени хоста в JSSE, не затягивая стороннюю библиотеку, такую ​​как HttpComponents?

Я также посмотрел на BouncyCastle, который имеет API-интерфейс JSSE для TLS, но он, кажется, еще более ограничен, поскольку он даже не проверяет цепочку сертификатов, а тем более проверку имени хоста, поэтому казалось, как не стартер.

Изменить: Этот вопрос ответил на для Java 7, но мне все еще интересно, что такое "лучшая практика" для Java 6 и Android. (В частности, я должен поддерживать Android для своего приложения.)

Отредактировано снова: Чтобы сделать мое предложение "заимствовать у Apache HttpComponents" более конкретным, я создал небольшую библиотеку , который содержит реализации HostnameVerifier (в первую очередь StrictHostnameVerifier и BrowserCompatHostnameVerifier), извлеченные из Apache HttpComponents. (Я понял, что все, что мне нужно, это верификаторы, и мне не нужен Apache SSLSocketFactory, как я изначально думал). Если оставить его на своих устройствах, это решение, которое я буду использовать. Но, во-первых, есть ли какая-то причина, по которой я не должен этого делать? (Предполагая, что моя цель - сделать проверку имени хоста таким же образом, как это делает https. Я понимаю, что сама открыта для обсуждения и обсуждалась в потоке в списке криптографии, но пока я придерживаюсь HTTPS-подобного имени хоста валидация, хотя я не делаю HTTPS.)

Предполагая, что с моим решением ничего не "неправильно", мой вопрос заключается в следующем: есть ли "лучший" способ сделать это, оставаясь при этом переносимым на Java 6, Java 7 и Android? (Где "лучше" означает больше идиоматического, уже широко используемого и/или нуждающегося в меньшем внешнем коде.)

4b9b3361

Ответ 1

Java 7 (и выше)

Вы можете неявно использовать X509ExtendedTrustManager, введенный в Java 7, используя это (см. этот ответ:

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. (Я не пробовал этот код.)

SSLSocketFactory ssf = (SSLSocketFactory) SSLSocketFactory.getDefault();
// It 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.

Более подробные сведения о правилах в этом недавнем ответе.

Вот реализация, которую я написал. Это, безусловно, может быть связано с пересмотром... Комментарии и отзывы приветствуются.

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 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 only one AVA in this RDN.
        cn = IETFUtils.valueToString(rdn.getFirst().getValue());
    }
    return cn;
}

Существуют две реализации getCommonName: один использует javax.naming.ldap и один использует BouncyCastle, в зависимости от того, что доступно.

Основные тонкости:

  • Соответствие IP-адреса только в SAN (Этот вопрос посвящен сопоставлению IP-адресов и альтернативных имен объектов.). Возможно, что-то может быть сделано и для соответствия IPv6.
  • Подстановочный знак.
  • Только возврат на CN, если нет DNS SAN.
  • Что означает "наиболее конкретный" CN. Я предположил, что это последний из них. (Я даже не рассматриваю ни одного CN RDN CN с несколькими утверждениями атрибутов (AVA): BouncyCastle может справиться с ними, но это очень редкий случай, насколько мне известно.)
  • Я не проверял вообще, что должно произойти для интернационализированных (не ASCII) доменных имен (см. RFC 6125.)

ИЗМЕНИТЬ

Сделать предложение "заимствовать у Apache HttpComponents" больше бетон, я создал небольшую библиотеку, которая содержит Реализации HostnameVerifier (в первую очередь StrictHostnameVerifier и BrowserCompatHostnameVerifier), извлеченные из Apache HttpComponents. [...] Но, во-первых, есть ли какая-то причина, которую я не должен делать это так?

Да, есть причины не делать этого таким образом.

Во-первых, вы эффективно разветвляете библиотеку, и теперь вам придется ее поддерживать, в зависимости от дальнейших изменений, внесенных в эти классы в исходные Apache HttpComponents. Я не против создания библиотеки (я сделал это сам, и я не препятствую вам это делать), но вы должны принять это во внимание. Вы действительно пытаетесь сэкономить место? Разумеется, есть инструменты, которые могут удалить неиспользуемый код для вашего конечного продукта, если вам нужно освободить место (ProGuard приходит на ум).

Во-вторых, даже StrictHostnameVerifier не соответствует RFC 2818 или RFC 6125. Насколько я могу судить по его коду:

  • Он будет принимать IP-адреса в CN, если это не так.
  • Он не будет просто возвращаться к CN, если нет DNS SAN, но также рассматривают CN как первый выбор. Это может привести к сертификату с CN=cn.example.com и SAN для www.example.com, но SAN для cn.example.com не будет действительным для cn.example.com, если это не так.
  • Я немного скептически отношусь к тому, как CN извлекается. Сериализация строки DN DN может быть немного забавной, особенно если некоторые RDN включают запятые и неудобный случай, когда некоторые RDN могут иметь несколько AVA.

Трудно увидеть общий "лучший способ". Предоставление этой обратной связи библиотеке Apache HttpComponents было бы одним из способов. Копирование и вставка кода, который я написал ранее, конечно, не очень хорошо звучит (фрагменты кода на SO обычно не поддерживаются, не проверяются на 100% и могут быть подвержены ошибкам).

Лучше всего попытаться попытаться убедить группу разработчиков Android поддерживать те же SSLParameters и X509ExtendedTrustManager, как это было сделано для Java 7. Это все еще оставляет проблему унаследованных устройств.

Ответ 2

Существует много веских причин требовать, чтобы клиент jsse (ваш) предоставлял собственный StrictHostnameVerifier. Если вы доверяете серверу имен своих компаний, писать нужно довольно просто.

  • получить IP-адрес имени хоста от поставщика DNS, на котором размещен ваш хост настроенный для использования.
  • возвращает результат сопоставления имен.
  • необязательно, убедитесь, что обратный поиск для IP возвращает правильное имя.

если вам это нужно, я предоставит вам верификатор. Если вы хотите, чтобы я дал "хорошие причины", я тоже могу это сделать.