Тикеты сессий TLS ускоряют переподключение, но тикет, который выдал сервер и вы вернули без изменений, — это ещё и связываемый ID без cookie и JavaScript.
Большинство рассказов о слежке заканчиваются на уровне браузера: cookie, localStorage, отпечаток, вычисленный в JavaScript. Но под всем этим у HTTPS есть ещё один слой, и он хранит собственное состояние. После TLS-рукопожатия сервер может передать браузеру небольшой непрозрачный блок данных — тикет сессии (session ticket) — который браузер сохраняет и предъявляет при следующем подключении, чтобы пропустить большую часть рукопожатия. Он существует, чтобы веб работал быстрее. Но он же оказывается идентификатором, который выдаёт сервер, вы возвращаете без изменений, и до которого не дотягивается ни одна кнопка очистки cookie.
Ключевые выводы
- Возобновление сессии — легитимный механизм ускорения. В TLS 1.3 сервер после рукопожатия отправляет сообщение
NewSessionTicket; клиент использует его при следующем соединении, чтобы возобновить сессию на основе предварительно разделённого ключа, а не проходить полное рукопожатие заново. - Тикет по своей природе — это ID. Его создаёт сервер, клиент хранит его, не имея возможности прочитать, и возвращает байт в байт. Если сервер зашил в тикет идентификатор или просто запомнил, какой тикет кому выдал, он узнаёт вернувшегося клиента — без JavaScript, cookie и API хранилищ.
- Срок жизни — это окно слежки, и оно может продлеваться цепочкой. RFC 8446 ограничивает заявленный срок жизни одного тикета семью сутками, но сервер может выдавать новый тикет при каждом возобновлении и продлевать связь от визита к визиту, пока клиент возвращается в пределах окна.
- Тикет виден в сети. Он передаётся в ClientHello, поэтому пассивный наблюдатель на маршруте может сопоставить соединения, использующие один и тот же тикет. Именно поэтому RFC 8446 требует от клиентов не использовать тикет повторно в нескольких соединениях.
- На практике это не глобальный суперкуки. Современные браузеры изолируют кеш сессий так, что третья сторона не может возобновить сессию на не связанных между собой сайтах, а на границе приватного режима кеш сбрасывается, — но конкретное поведение зависит от браузера и версии, поэтому проверяйте, а не предполагайте.
Зачем нужно возобновление сессии
Полное рукопожатие TLS стоит времени: один круг обмена (RTT) на согласование параметров, сертификат, который нужно отправить и проверить, подпись, которую нужно вычислить. Если страница открывает десяток соединений, задержки складываются. Возобновление позволяет клиенту и серверу, уже аутентифицировавшим друг друга, пропустить самую дорогую часть.
В TLS 1.3, описанном в RFC 8446, это работает через предварительно разделённые ключи (PSK). После завершения рукопожатия сервер может отправить одно или несколько сообщений NewSessionTicket (раздел 4.6.1). Каждое содержит непрозрачный тикет, срок жизни и материал, из которого клиент выводит PSK. При следующем соединении клиент предлагает этот тикет в расширении pre_shared_key своего ClientHello (раздел 4.2.11), подтверждает владение секретом с помощью binder, и сервер может принять возобновление вместо полного рукопожатия с сертификатом.
Тот же механизм даёт 0-RTT early data (раздел 2.3): клиент может отправить данные приложения уже в первой порции сообщений (first flight). RFC прямо говорит, что за это приходится платить — прежде всего тем, что ранние данные можно воспроизвести повторно (replay), — и подробно разбирает это в разделе 8. Для этой статьи важнее другое: возобновление — полезная функция, и никакого бэкдора здесь нет. Проблема приватности возникает из-за того, кто хранит состояние и как долго оно живёт, а не из-за изъяна в криптографии.
В TLS 1.2 та же идея существовала в двух формах — идентификаторы сессий и тикеты сессий (RFC 5077), — так что ничего из описанного ниже не является новинкой TLS 1.3. На проблему связываемости в научной литературе указали давно; обычно ссылаются на статью Sy, Burkert, Federrath и Fischer Tracking Users across the Web via TLS Session Resumption (ACSAC 2018).
Как тикет становится идентификатором
Тикет непрозрачен для клиента. Браузер не может заглянуть внутрь, он просто хранит то, что дал сервер. Именно непрозрачность делает тикет удобным строительным блоком для сервера — и потенциальным вектором слежки.
Обычно встречаются два подхода:
- Тикеты, зашифрованные сервером. Сервер упаковывает состояние сессии в тикет, шифрует ключом, который есть только у него, и сам ничего не хранит. Любой сервер с этим ключом может расшифровать тикет.
- Поиск на стороне сервера. Тикет — просто случайный дескриптор; состояние лежит в таблице, ключом которой служит этот дескриптор.
В обоих случаях сервер контролирует содержимое тикета и видит точные байты, которые вы возвращаете. Ничто в протоколе не мешает серверу поместить туда уникальный для клиента идентификатор или просто записывать, какое значение тикета вернулось. TLS 1.3, правда, маскирует возраст тикета (поле obfuscated_ticket_age), но сам тикет передаётся как есть. В итоге получается стабильный, узнаваемый сервером ID, который передаётся внутри TLS-рукопожатия, а не в HTTP-заголовке или в хранилище, доступном скрипту. Это тот же приём, что и суперкуки на основе ETag, только слоем ниже — и, в отличие от ETag, JavaScript страницы не может его ни прочитать, ни записать, ни удалить.
Проблема цепочки
Семидневный лимит звучит как естественное ограничение. RFC 8446 действительно требует, чтобы сервер не использовал срок жизни тикета больше 604800 секунд — семи суток. Но лимит относится к одному тикету, а не к стоящей за ним личности клиента.
После каждого успешного возобновления сервер вправе отправить новый NewSessionTicket. Клиент заменяет старый тикет новым и при следующем визите предъявляет его. Каждое звено цепочки обновляет окно, поэтому клиента, который подключается хотя бы раз в несколько дней, можно отслеживать бесконечно: каждый тикет переносит дальше одну и ту же метку. Заявленный срок ограничивает один тикет, но не сервер, который постоянно выдаёт новые.
Именно этот момент большинство объяснений пропускает, и поэтому фраза «тикеты истекают через неделю» недооценивает риск для сайта, который вы посещаете ежедневно. Впрочем, на практике браузеры часто ограничивают повторное использование сохранённой сессии собственным, более коротким сроком, а кеш в памяти исчезает при полном закрытии браузера, — так что реальный потолок обычно задаёт клиент, а не семь суток из RFC.
Почему на практике это не суперкуки
Было бы неверно заключить, что любой когда-либо посещённый сайт может следить за вами через TLS-тикеты. Риск ограничен тем, как браузеры задают область видимости кеша сессий:
- Кто какую сессию может возобновить. Сессию, установленную, пока вы были на одном сайте, не должен возобновлять тот же сторонний сервер, встроенный в другой, не связанный сайт. Общий подход — разделение сетевого состояния (соединений, HTTP-кеша, TLS-сессий) по сайту верхнего уровня; то же самое браузеры сделали с HTTP-кешем, когда слежка через кеш стала общеизвестной.
- Приватный режим и очистка. Приватное окно должно начинать с пустого кеша сессий и отбрасывать его при закрытии, а полная очистка данных или перезапуск браузера обычно сбрасывает кеш в памяти.
Считайте это описанием класса защит, а не обещанием для конкретного релиза: то, как каждый браузер разделяет или очищает кеш сессий, со временем менялось, поэтому номера версий мы здесь намеренно не называем. Остаётся слежка в пределах одного сайта: сам сайт (или CDN, который его обслуживает) по-прежнему узнаёт ваши возвращающиеся соединения, а сетевой наблюдатель по-прежнему может сопоставить соединения, явно использующие один тикет.
Приложение C.4 самого RFC 8446, посвящённое защите клиентов от отслеживания, формулирует замысел: клиенты не должны использовать тикет повторно в нескольких соединениях, потому что повторное использование позволяет пассивным наблюдателям их сопоставить. Браузер, следующий этому совету, ограничивает то, что узнаёт наблюдатель на маршруте, но ничего не меняет для сервера, выдавшего тикет.
Второй эффект: возобновление меняет отпечаток
Есть побочный эффект, важный для остальных статей блога о сетевом уровне. Возобновлённое рукопожатие выглядит в сети иначе, чем полное. В нём появляется расширение pre_shared_key (оно обязано быть последним в ClientHello) с тикетом и binder, а также может добавиться расширение early_data. Поэтому список расширений и общая длина ClientHello отличаются от первого соединения.
TLS-отпечаток вроде JA3 или JA4 вычисляется из ClientHello, поэтому тот же браузер может давать другое значение при возобновлённом соединении, чем при новом. Если вы строите детектирование на TLS-отпечатке — или пытаетесь понять, почему у одного клиента два хеша, — возобновление становится источником разброса, который нужно учитывать. Подробности — в статьях TLS-фингерпринтинг и набор JA4+; для более нового транспорта см. HTTP/3 и QUIC-фингерпринтинг — там та же история в своём варианте.
Место среди постоянных идентификаторов
| Уровень | Идентификатор | Кто хранит | Сбрасывается очисткой cookie? |
|---|---|---|---|
| Скрипт | Постоянный Visitor ID | JavaScript страницы, разбросан по хранилищам | Часто частично |
| HTTP-кеш | Суперкуки ETag | Кеш браузера | Нет |
| TLS | Тикет сессии | Кеш сессий TLS-стека | Не гарантированно |
Вывод не в том, что каждый уровень — катастрофа (у каждого есть защитные меры в браузерах), а в том, что «я очистил cookie» описывает лишь верхнюю строку этого стека мест, где может лежать состояние.
Что можно сделать на практике
Почти ничего из этого нельзя увидеть изнутри страницы — в этом и проблема. На практике:
- Не рассчитывайте, что очистка cookie сбросит кеш сессий TLS. Удаляет ли «очистка cookie» заодно и сохранённые TLS-сессии, зависит от браузера; полное закрытие и перезапуск браузера или действие «очистить всё» — более надёжный способ.
- Приватные окна — самая чистая граница, поскольку они задуманы так, чтобы начинаться и заканчиваться без перенесённого состояния сессий.
- Не ждите, что какой-либо инструмент покажет вам ваши тикеты. BrowserInsight не проверяет ваши TLS-тикеты и не сообщает о состоянии возобновления сессий. Зато он показывает, что сервер видит в вашем TLS-рукопожатии, — версию протокола, шифронабор, длину ClientHello и хеш расширений — в сетевой карточке проверки отпечатка; тот же сигнал используется в инструменте обнаружения ботов. Учтите, что эти значения описывают соединение, по которому пришёл запрос, а оно само могло быть возобновлённым.


