В нашем продукте имеется сервер, использующий встроенный 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 имеет значение 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");
}
}
Я чувствую, что это может быть проблема с кодом Jetty, но я в этом не уверен. Что еще более важно, мы пытаемся выяснить, есть ли какое-то обходное решение, которое могло бы помочь клиенту.
Я пытался выяснить исходный код причала и не уверен, но похоже, что это исключение выдается только во время попытки возобновления сеанса TLS. И в этом случае SNI сверяется с именем хоста в сертификате, так что вы были правы. Однако Jetty преобразует имена хостов в сертификатах в нижний регистр и имя хоста в SNI, поэтому сравнения не учитывают регистр. У меня нет ответа, но есть подозрение, что проблема здесь на стороне клиента.
Спасибо за проверку исходного кода. Как вы думаете, в чем проблема на стороне клиента? Wireshark действительно показывает чередующиеся прописные и строчные буквы, как я уже упоминал в вопросе, но, похоже, это происходит на уровне TLS, и не похоже, что клиент имеет над этим какой-либо контроль. Если есть способ изменить/контролировать эти настройки, я могу попробовать их.
Мне также интересно, есть ли какие-либо дополнительные журналы, которые мы можем включить, чтобы увидеть, что Jetty делает с SNI.
Я мог бы подумать о некоторых из следующих вариантов:
Мы уже пробовали пункт 1 и это не помогло. Я не уверен, что я бы проверил по пункту 2. А пункт 3 сейчас не является простым вариантом, поскольку Jetty упаковывается/поставляется вместе с нашим продуктом.
Мы обнаружили проблему после долгих копаний. Проблема заключалась в используемой версии 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));
}
Я думал, что реализации веб-сервера НЕ проверяют соответствие сертификата имени хоста SNI. Вместо этого они проверяют, соответствует ли имя хоста SNI имени хоста в их конфигурации, например VirtualHost в Apache. По крайней мере, имена DNS следует считать нечувствительными к регистру, поэтому, если это имеет значение, то это где-то ошибка.