![](https://habrastorage.org/files/e0e/a29/a67/e0ea29a67019461983eeaf7315bf5015.png)
Через некоторое время у нас были записи логов ошибок и pcap-файлы со схожим содержимым:
![](https://habrastorage.org/files/bf9/d8f/3d4/bf9d8f3d477f4484b395311bb8e9f9ac.png)
Всё выглядит так, будто сервер ответил некорректно и клиент прекратил хендшейк. Проанализировав «корректные» (принятые клиентом) и «некорректные» ответы сервера, мы поняли, что они идентичны.
Анализ дампов показал, что проблема возникает только в случае использования клиентом TLS Ticket (механизм session reuse), и если при этом тикет был зашифрован не на ключе по умолчанию (в нашем случае получен до ротации ключей, но менее чем 28 часов назад).
Как я уже говорил — в Поиске используется свой Балансер, поэтому сначала мы стали искать ошибку в нем. Однако, позже предположили, что проблема может быть связана и с поведением клиента — она возникает, когда браузер пытается одновременно создать несколько SSL-соединений к веб-серверу. Такое поведение со стороны браузера (несколько соединений) в общем случае (забудем про prefetch и пр.) может вызвать HTML вида:
<img src="https://domain.com/x"><img src="https://domain.com/y"><img src="https://domain.com/z">
Объединив эти теории, мы смогли воспроизвести проблему на связке Chromium + Nginx и поняли, что код Балансера ни при чем. Затем нам удалось окончательно выяснить причину такого поведения.
Дальше немного деталей про TLS и его клиентский state machine в реализации BoringSSL
Итак, как вы уже знаете, TLS-хендшейк бывает длинный и короткий.
При первом обращении к серверу длинный хендшейк с точки зрения клиента выглядит примерно так (я специально не стал прописывать обработку некоторых TLS extensions, чтобы было проще для понимания):
![](https://habrastorage.org/files/099/7e3/300/0997e33002df4ef498fcf6828f1486a5.png)
Состояния с префиксом SSL3_ST_CR — клиент читает сообщение (record) от сервера, с префиксом SSL3_ST_CW — клиент шлет сообщение на сервер. (Не так давно Chromium перешел на использование форка OpenSSL — Boringssl, поэтому все вышеописанные состояния справедливы для него.).
Посмотрим на структуру некоторых сообщений протокола TLS:
![](https://habrastorage.org/files/901/451/18f/90145118f6404078936ec11434846435.png)
Назначение полей (опустил некоторые TLS extensions):
• Version — клиентская версия протокола (SSL 3.0 / TLS 1.0 / TLS 1.1 / TLS 1.2),
• Random — клиентский random,
• Session ID length — длина поля Session ID (0 при первом обращении),
• Session ID — идентификатор предыдущей сессии (пустой при первом обращении),
• SessionTicket TLS — TLS extension, Length — длина данных в расширении, Data -— значение.
(При первом обращении, соответственно, длина 0 и пустое значение).,
• Cipher Suites — поддерживаемые клиентом шифры,
• Server Name — SNI TLS extension, позволяющее сказать серверу, к какому именно домену обращается клиент.
Для того чтобы при следующем обращении не делать полный — «дорогой» и медленный — хендшейк, сервер может предложить клиенту воспользоваться одним из двух способов session reuse. Для этого он может вернуть клиенту в ServerHello либо Session ID, указывающий на сохраненный на стороне сервера state (RFC 5246), либо Session ID и Session Ticket TLS (RFC 5077). О них я несколько раз подробно рассказывал.
Так как RFC 5077 появился позже, он дополняет механизм сессий в RFC 5246 и внутри клиента строится вокруг одной и той же реализации. Сегодня разбираем только механизм TLS Tickets.
![](https://habrastorage.org/files/289/625/81b/28962581b11c449689ec3cec7b15586c.png)
Назначение полей:
• Version — серверная версия протокола (SSL 3.0 / TLS 1.0 / TLS 1.1 / TLS 1.2),
• Random — серверный random,
• Session ID length — длина поля Session ID (при выдаче сервером нового тикета, должен быть выставлен в 0),
• Session ID — идентификатор предыдущей сессии (при первой выдаче тикета равен 0),
• SessionTicket TLS — TLS extension, наличие данного расширения означает, что сервер собирается выдать клиенту новый TLS Ticket, передав в состоянии ST_CR_FINISHED_A сообщение New Session Ticket и переведя сервер в состояние SSL3_ST_CR_SESSION_TICKET_A.
![](https://habrastorage.org/files/993/5ce/87f/9935ce87f0ba48da9f83ff20c072443c.png)
Назначение полей:
• Session Ticket Lifetime Hint — время жизни тикета, после которого он должен быть удален клиентом (клиент может сам решить сам, когда удалить тикет в пределах заданного периода времени, 0 — на усмотрение клиента),.
• Session Ticket Length — длина данных тикета,
• Session Ticket — значение тикета.
Значение и параметры тикета сохраняются в памяти клиента:
![](https://habrastorage.org/files/6b1/049/eb6/6b1049eb6467497eaa63721e6a0f871c.png)
Следует отметить, что для клиента значение тикета — это ничего не значащий бинарный блоб, который нужно либо передавать на сервер, либо сохранять/обновлять при получении, референсным полем является Session ID. Сервер же использует первые 16 байт значения тикета для идентификации набора ключей, которые будут использоваться для проверки его целостности и расшифровки. Таким образом сервер может ротировать значения ключей, продолжая принимать от клиентов тикеты, выданные на старых ключах.
Так выглядит короткий хендшейк с использованием впервые выданного тикета:
![](https://habrastorage.org/files/2f2/058/ef0/2f2058ef082b41c9b915165ef3c971b7.png)
где в ClientHello задаются следующие значения:
• Session ID length — длина поля Session ID (обычно 32 байта),
• Session ID — Ззначение Session ID из структуры SSL_SESSION,
• SessionTicket TLS — TLS extension, length — длина данных тикета, data — значение тикета.
В случае если тикет принят, сервер должен ответить в ServerHello таким образом, что
• Session ID length и Session ID равны соответствующим полям из ClientHello.
При этом если полученный тикет не будет обновлен сервером (используется текущий ключ), то поле SessionTicket TLS в ServerHello отсутствует.
Если же тикет был принят сервером, но ключ изменился, то хендшейк выглядит следующим образом:
![](https://habrastorage.org/files/6a2/7f9/07c/6a27f907cded4279a27cec13d735e141.png)
Значения Session ID length и Session ID равны соответствующим полям из ClientHello, в ServerHello добавлено поле SessionTicket TLS. Это переводит клиент в состояние SSL3_ST_CR_SESSION_TICKET_A, и он ожидает сообщение New Session Ticket. Получив сообщение New Session Ticket, клиент проверяет, что значение Session ID из ServerHello равно сохраненному в SSL_SESSION, записывает значение Session Ticket в структуру SSL_SESSION и обновляет (!) значение Session ID, делая его равным результату хеш-функции SHA-256 от значения Session Ticket, выставляет состояние в SSL3_ST_CR_CHANGE.
Место в коде Chromium, отвечающее за session reuse, выглядело так:
![](https://habrastorage.org/files/f43/d2a/d90/f43d2ad904c143b9b370f0a70c238697.png)
Здесь GetSessionCacheKey() однозначно идентифицирует домен, порт, версию протокола. То есть для одного origin в пределах шарда всегда хранится не более одного экземпляра session.
Функция SSL_set_session() не копирует экземпляр session в заданное соединение, а передает в него указатель на этот экземпляр.
Таким образом, при инициализации, например, трех соединений подряд, клиент отправит одинаковые значения Session ID и SessionTicket TLS. Первое из соединений будет успешным и перейдет в состояние SSL3_ST_CR_SESSION_TICKET_A, после чего значение Session_ID будет изменено, а для второго и последующих клиент получит в ServerHello не пустой Session ID и, увидев, что значение, которое вернул сервер (то же самое, что прислал клиент), не равно значению в структуре SSL_SESSION (его уже изменило первое соединение), перейдет в состояние SSL3_ST_CR_CERT_A (полный хендшейк). Сервер, справедливо считая, что клиент ожидает от него новый тикет (SSL3_ST_CR_SESSION_TICKET_A), отправит сообщение New Session Ticket, что не соответствует ожидаемому состоянию и приведет к Unexpected message alert.
Проблема уже исправлена в Яндекс.Браузере 15.9 и Chromium 46.
Комментарии (9)
nmk2002
29.10.2015 19:01-1Жаль, что google теперь использует свой форк openssl. От такого гиганта помощь в развитии openssl была бы не лишней.
kyprizel
29.10.2015 19:03На самом деле они продолжают поддерживать OpenSSL, часть команды проекта работает в Google.
подробнее:
www.imperialviolet.org/2015/10/17/boringssl.htmlnmk2002
29.10.2015 19:08+1Спасибо за ссылку. Это хорошие новости. Я думал, что они полностью сконцентрируются на своем продукте.
Дальнейшее использование openssl было бы значимым вкладом в проект.
Makc666
02.11.2015 14:10+2Дико извиняюсь, но никак не получается добиться адекватного ответа от поддержки Яндекс [1509281351*******].
1. httpS//www.yandex.ru/
2. Любой поиск.
3. В результатах клики «onmousedown» ведут на: http://yandex.ru/clck/jsredir?from=yandex.ru%3Bsearch%2F%3Bweb%3B%3B&text=&etext=
Почему нет HTTPS?
При этом, если вручную, то успешно работает: httpS://yandex.ru/clck/jsredir?from=yandex.ru%3Bsearch%2F%3Bweb%3B%3B&text=&etext=
zorgrhrd
Люблю когда дебаг одного, заканчивается исправлением ошибки другого.
Stalker_RED
Особенно когда есть возможность «исправить ошибку другого», а не как обычно — нашли ошибку отрепортили, а исправления не видать. И приходится городить костыли на своей стороне, ага.