Ошибка Jetty Invalid SNI, даже если сертификат верен

В нашем продукте имеется сервер, использующий встроенный Jetty (версия 11.0.11), обеспечивающий функциональность подключения клиентов посредством HTTP-вызовов. Его можно настроить для HTTPS, и многие наши клиенты используют его с этой конфигурацией. Недавно мы столкнулись с проблемой от одного из наших клиентов, который обнаружил странную проблему с конфигурацией https. Запрос иногда работает правильно, а иногда выдает ошибку «Неверный SNI» от Jetty. Мы знаем, что проблемы такого типа обычно возникают, когда SNI, отправленный в запросе, не соответствует тому, что указано в сертификате. В данном случае в рамках упражнений по устранению неполадок мы собрали снимки Wireshark, чтобы посмотреть, что отправляется, и сравнить с тем, что указано в сертификате.

В самом сертификате в альтернативном имени субъекта указан сервер, например «server.xyz.abc.com». Из журналов Wireshark мы видим, что уровень TLS иногда отправляет SNI как «server.xyz.abc.com», а иногда как «SERVER.xyz.abc.com». Обратите внимание на то, что часть имени сервера написана заглавными буквами.

Запрос выполняется успешно, когда уровень TLS отправляет SNI как «server.xyz.abc.com», и кажется, что запрос завершается неудачей, когда SNI поступает в «SERVER.xyz.abc.com».

Мы получаем сообщение «org.eclipse.jetty.http.BadMessageException: 400: Invalid SNI».

Чтобы исправить это, мы попросили клиента исправить сертификат, добавив «SERVER.xyz.abc.com» в альтернативное имя субъекта, и развернули этот сертификат на сервере. Итак, сертификат теперь содержит:

server.xyz.abc.com

СЕРВЕР.xyz.abc.com

Однако это все еще не устранило проблему, и сообщение о недопустимом SNI по-прежнему продолжает приходить с перерывами, хотя Wireshark показывает, что SNI Client Hello поступает точно так же, как в сертификате. Мы заметили одну вещь: сбой по-прежнему происходит, когда имя в верхнем регистре входит в качестве SNI в запросе Client Hello.

У меня есть следующие вопросы:

  • Есть ли какая-либо известная проблема с Jetty с чувствительностью к регистру SNI, и правильно ли добавлять в сертификат имя сервера в верхнем регистре?
  • Почему уровень TLS поочередно отправляет имя в верхнем и нижнем регистре, и есть ли где-нибудь какая-либо настройка, позволяющая заставить его отправлять его, скажем, в нижнем регистре?
  • В худшем случае нам может потребоваться отключить проверку SNI, чтобы исправить это. Это единственный способ сделать это, поскольку для отключения этой проверки нам потребуется внести изменения в код. Судя по другим сообщениям, я думаю, что в Jetty нет настраиваемого параметра, позволяющего отключить эту проверку.

Пожалуйста, помогите с любыми идеями, которые могут у вас возникнуть, поскольку мы не знаем, как продолжить помощь нашему клиенту.

[ОБНОВЛЯТЬ]: При дальнейшем копании, включив ведение журнала отладки Jetty, мы обнаружили, что хост SNI имеет значение null. Таким образом, этот вызов внутри кода Jetty (SecureRequestCustomizer.java) для получения значения хоста SNI возвращает нулевое значение для «sniHost» и вызывает исключение. Есть идеи, почему это будет равно нулю и что мы можем с этим поделать?

protected void checkSni(Request request, SSLSession session)
{
    if (isSniRequired() || isSniHostCheck())
    {
        String sniHost = (String)session.getValue(SslContextFactory.Server.SNI_HOST);

        X509 x509 = getX509(session);
        if (x509 == null)
            throw new BadMessageException(400, "Invalid SNI");
        String serverName = Request.getServerName(request);
        if (LOG.isDebugEnabled())
            LOG.debug("Host = {}, SNI = {}, SNI Certificate = {}", serverName, sniHost, x509);

        if (isSniRequired() && (sniHost == null || !x509.matches(sniHost)))
            throw new BadMessageException(400, "Invalid SNI");

        if (isSniHostCheck() && !x509.matches(serverName))
            throw new BadMessageException(400, "Invalid SNI");
    }
}

Я думал, что реализации веб-сервера НЕ проверяют соответствие сертификата имени хоста SNI. Вместо этого они проверяют, соответствует ли имя хоста SNI имени хоста в их конфигурации, например VirtualHost в Apache. По крайней мере, имена DNS следует считать нечувствительными к регистру, поэтому, если это имеет значение, то это где-то ошибка.

President James K. Polk 15.06.2024 15:52

Я чувствую, что это может быть проблема с кодом Jetty, но я в этом не уверен. Что еще более важно, мы пытаемся выяснить, есть ли какое-то обходное решение, которое могло бы помочь клиенту.

RBK 17.06.2024 15:40

Я пытался выяснить исходный код причала и не уверен, но похоже, что это исключение выдается только во время попытки возобновления сеанса TLS. И в этом случае SNI сверяется с именем хоста в сертификате, так что вы были правы. Однако Jetty преобразует имена хостов в сертификатах в нижний регистр и имя хоста в SNI, поэтому сравнения не учитывают регистр. У меня нет ответа, но есть подозрение, что проблема здесь на стороне клиента.

President James K. Polk 17.06.2024 16:16

Спасибо за проверку исходного кода. Как вы думаете, в чем проблема на стороне клиента? Wireshark действительно показывает чередующиеся прописные и строчные буквы, как я уже упоминал в вопросе, но, похоже, это происходит на уровне TLS, и не похоже, что клиент имеет над этим какой-либо контроль. Если есть способ изменить/контролировать эти настройки, я могу попробовать их.

RBK 17.06.2024 17:43

Мне также интересно, есть ли какие-либо дополнительные журналы, которые мы можем включить, чтобы увидеть, что Jetty делает с SNI.

RBK 17.06.2024 18:32
Поддержка сообщества Jetty 11 прекращена, вы должны использовать поддерживаемую версию Jetty (на данный момент Jetty 12)
Joakim Erdfelt 24.06.2024 22:01
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
6
148
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я мог бы подумать о некоторых из следующих вариантов:

  1. Дважды проверьте свое приложение, чтобы убедиться, что оно постоянно использует один и тот же регистр для имени хоста.
  2. Убедитесь, что ваша конфигурация Jetty правильна и не требует соблюдения чувствительности к регистру.
  3. Обновите Jetty до последней версии и проверьте, сохраняется ли проблема.

Мы уже пробовали пункт 1 и это не помогло. Я не уверен, что я бы проверил по пункту 2. А пункт 3 сейчас не является простым вариантом, поскольку Jetty упаковывается/поставляется вместе с нашим продуктом.

RBK 17.06.2024 15:38
Ответ принят как подходящий

Мы обнаружили проблему после долгих копаний. Проблема заключалась в используемой версии Java. Проблема возникла с версией Oracle Java 11 (+28), и когда наш клиент перешел на использование Adoptium 11, она исчезла.

Ниже приведены сведения о том, где возникла проблема:

Во-первых, правильный исходный код Jetty 11.0.x, из которого возникла ошибка, приведен ниже. Исключение возникло, поскольку сертификат был недействительным. По какой-то причине в Oracle Java 11 сертификат возвращался либо с нулевым значением, либо с длиной 0 из вызова getLocalCertificates().

    protected void customize(SSLEngine sslEngine, Request request)
{
    SSLSession sslSession = sslEngine.getSession();

    if (isSniRequired() || isSniHostCheck())
    {
        String sniHost = (String)sslSession.getValue(SslContextFactory.Server.SNI_HOST);
        X509 x509 = (X509)sslSession.getValue(X509_CERT);
        if (x509 == null)
        {
            Certificate[] certificates = sslSession.getLocalCertificates();
            if (certificates == null || certificates.length == 0 || !(certificates[0] instanceof X509Certificate))
                throw new BadMessageException(400, "Invalid SNI");
            x509 = new X509(null, (X509Certificate)certificates[0]);
            sslSession.putValue(X509_CERT, x509);
        }
        String serverName = request.getServerName();
        if (LOG.isDebugEnabled())
            LOG.debug("Host = {}, SNI = {}, SNI Certificate = {}", serverName, sniHost, x509);

        if (isSniRequired() && (sniHost == null || !x509.matches(sniHost)))
            throw new BadMessageException(400, "Invalid SNI");

        if (isSniHostCheck() && !x509.matches(serverName))
            throw new BadMessageException(400, "Invalid SNI");
    }

    request.setAttributes(new SslAttributes(request, sslSession));
}

Другие вопросы по теме