Задача: наш сервис обращается к внешнему сервису по HTTPS, хотелось бы записать дамп трафика и посмотреть, - при помощи Wireshark/tshark, например, - какие запросы и как ходят. Такая проблема возникает нередко при отладке работы сервисов. Но трафик зашифрован TLS. Чтобы разобрать при помощи анализатора протоколов, нужны сессионные ключи. Вообще, всё штатное "раскрытие TLS" основано на экспорте сессионных ключей из приложения. Ключи можно "подложить" в тот же Wireshark, и он автоматически расшифрует полезные данные. Если сервис собственной разработки, то экспорт TLS-ключей в файл тоже хорошо бы предусмотреть в исходном коде.

Посмотрим, как это сделать, на примере Go и crypto/tls. Простой код (пояснения будут ниже):

import (
	"fmt"
	"net"
	"time"
	"crypto/tls"
	"os"
)

func main(){
	var TLSConfig tls.Config
	SessionKeysFile, err := os.OpenFile("s-keys.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	// В SessionKeysFile и будут записываться ключи, тут всё просто.
	if err != nil {
		fmt.Printf("File error: %s\n", err.Error())
		os.Exit(1) 
	}
	hostname := "example.com"

	TLSConfig.KeyLogWriter = SessionKeysFile // Указываем, куда выводить сессионные ключи.
	TLSConfig.MinVersion = tls.VersionTLS13
	TLSConfig.MaxVersion = tls.VersionTLS13
	TLSConfig.RootCAs = nil
	TLSConfig.InsecureSkipVerify = true

	timeout := 3 * time.Second
	TCPConn, err := net.DialTimeout("tcp", hostname + ":443", timeout)
	if err != nil {
		fmt.Printf("Connection error: %s\n", err.Error())
		os.Exit(1)
	}

	TCPConn.SetReadDeadline(time.Now().Add(timeout))
	TCPConn.SetWriteDeadline(time.Now().Add(timeout))

	TLSConn := tls.Client(TCPConn, &TLSConfig)

	HTTPGet := "GET / HTTP/1.1\r\nHost: " + 
				hostname + "\r\n" + 
				"Connection: close\r\n" +
				"User-Agent: TLS-keys-dump" + 
				"\r\n\r\n"
				
	_, err = TLSConn.Write([]byte(HTTPGet))
	if err != nil {
		fmt.Printf("Connection (TLS) write error: %s\n", err.Error())
		TLSConn.Close()
		os.Exit(1)
	}
	TLSConn.Close()
	os.Exit(0)
}

Итак, сам принцип довольно простой: при установлении TLS-соединения библиотека будет выводить сеансовые симметричные секреты и сопроводительный параметр (поле Random) в заданный файл, который позже использует Wireshark (или tshark) для раскрытия трафика. В TLS для защиты трафика в рамках сессии используются симметричные шифры и набор симметричных ключей (ключи согласовываются сторонам на начальном этапе соединения; обычно, по протоколу Диффи-Хеллмана). Для расшифрования не нужно получать “ключи сервера” или “ключи от сертификата сервера”. Достаточно выгрузить сессионный секрет, из которого получаются ключи шифров. Остальное анализатор трафика, который поддерживает обработку TLS, сделает автоматически. Симметричных ключей на каждой стороне TLS-сессии используется два: один для отправки данных, второй – для получения (на клиенте и сервере эти ключи просто меняются местами).

Чтобы это заработало в Go, достаточно в структуре конфигурации TLS-клиента (или TLS-сервера) указать (KeyLogWriter) имя интерфейса для записи в файл (writer в терминологии Go), а сам интерфейс предварительно создать и направить вывод в нужный файл. Именно это и проделано в примере выше, вот в этих фрагментах:

SessionKeysFile, err := os.OpenFile("s-keys.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
[...]
}
[...]
TLSConfig.KeyLogWriter = SessionKeysFile

После того как сессионные ключи попали в файл, не трудно посмотреть, что получилось. Например, если дамп трафика записывается привычным tcpdump, а для анализа используется tshark, то выглядит всё примерно так:

$ tshark -r dump-ens.pcap -o tls.keylog_file:tls-export-keys/s-keys.txt -O tls -S "-----PACKET-----" -x

Здесь: dump-ens.pcap – дамп трафика, записанный tcpdump; s-keys.txt – файл, в который экспортированы TLS-секреты.

В tshark файл с сессионными секретами передаётся при помощи опции tls.keylog_file (в Wireshark, где есть графический интерфейс, можно загрузить имя файла через диалог редактирования параметров TLS-парсера).

Фрагмент результата работы:

Decrypted TLS (83 bytes):
	0000  47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a   GET / HTTP/1.1..
	0010  48 6f 73 74 3a 20 65 78 61 6d 70 6c 65 2e 63 6f   Host: example.co
	0020  6d 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 63   m..Connection: c
	0030  6c 6f 73 65 0d 0a 55 73 65 72 2d 41 67 65 6e 74   lose..User-Agent
	0040  3a 20 54 4c 53 2d 6b 65 79 73 2d 64 75 6d 70 0d   : TLS-keys-dump.
	0050  0a 0d 0a

Внутренняя работа с ключами отличается для TLS 1.3 и предыдущих версий протокола. Но, опять же, при должном экспорте всё выполняется автоматически. Интересно, что в TLS 1.3 без секретных ключей сессии не удалось бы посмотреть даже серверные сертификаты, а это привычный видимый кусочек TLS-трафика предыдуших версий. То есть, если бы ключи не подошли, то tshark не смог бы разобрать и TLS-сертификаты, присланные сервером безо всяких HTTP-запросов. Впрочем, в TLS 1.3 для зашифрования сертификатов и зашифрования прикладного трафика используются разные ключи и это влияет на содержание файла сессионных ключей. Этот файл имеет стандартный формат (KeyLog), но если для TLS версий до 1.3 секрет будет один для сессии, то для TLS 1.3 разные секреты соответствуют разным этапам сессии - то есть, секретов будет несколько. Пример содержания файла сессионных ключей для TLS 1.3 приведён ниже.

CLIENT_HANDSHAKE_TRAFFIC_SECRET [%RANDOM%] [%SECRET%]
SERVER_HANDSHAKE_TRAFFIC_SECRET [%RANDOM%] [%SECRET%]
CLIENT_TRAFFIC_SECRET_0 [%RANDOM%] [%SECRET%]
SERVER_TRAFFIC_SECRET_0 [%RANDOM%] [%SECRET%]

Здесь [%RANDOM%] и [%SECRET%] - это более понятные обозначения для "случайного" значения (что это - разберёмся ниже) и секрета. В исходном файле, конечно, на месте этих обозначений будут длинные наборы шестнадцатеричных цифр.

Что это за поле с именем [%RANDOM%]? Анализатору протоколов нужно как-то сопоставлять сессии, записанные в дампе трафика, и ключи. Можно было бы использовать пробное расшифрование - для современных версий TLS не так трудно с уверенностью сказать, что ключ подошёл, потому что есть код аутентификации сообщений: если код сходится с сообщением, значит ключ верный, а в детали трафика можно не вдаваться. Однако гораздо удобнее использовать другую метку. Установление TLS-соединения начинается с начального сообщения клиента - ClientHello. В этом сообщении есть поле Random, в которое, согласно спецификации, клиент записывает случайные байты. То есть, получаем автоматическую уникальную метку сессии, по которой анализатор протокола может быстро сопоставить конкретный секрет и конкретную TLS-сессию в записанном трафике. (Если вдруг байты Random оказались не слишком уникальными, то можно вернуться к пробному расшифрованию.) Сам же секрет, позволяющий получить ключи для шифров, указывается в поле [%SECRET%]. Конкретные ключи вычисляются на основе данного секрета при помощи соответствующей функции HKDF. Функция зафиксирована, известна анализатору протоколов, поэтому он получит нужный набор ключей для шифров без труда.

Все прочие параметры сессии: какие шифры использовались, с какой длиной ключа и пр. - определяются по идентификаторам, которые передаются в открытом виде.

В файле-примере выше указаны клиентские и серверные секреты для разных этапов соединения TLS 1.3. CLIENT(SERVER)_HANDSHAKE_TRAFFIC_SECRET – это ключи для защиты начальных сообщений. CLIENT(SERVER)_TRAFFIC_SECRET_0 – это ключи первого поколения для защиты трафика. В TLS 1.3 возможно обновление ключей в рамках сессии, то есть могли бы быть и TRAFFIC_SECRET_n следующих поколений. В файле возможны и поля других типов, которые тут не рассматриваются – они устроены аналогично, но сдержат секреты других типов, анализаторы трафика обрабатывают их автоматически.

Соответственно, для TLS 1.2 файл с экспортируемыми секретами, генерируемый библиотекой Go из примера кода, будет будет использовать другой формат:

CLIENT_RANDOM [%RANDOM%] [%SECRET%]

Насколько это безопасно?

Безопасно ли экспортировать ключи TLS из приложения? Строго говоря, нет, не безопасно. Тут нужно подходить с пониманием того, что же именно происходит и учитывать реальные сценарии использования. Во-первых, экспорт ключей можно включить только для отладки, предусмотрев специальную опцию. Во-вторых, предполагается, что доступ к экспортированным ключам имеют только инженеры DevOps (или администратор), занятые непосредственно в проекте или обеспечивающие работу сервиса. Ну и в-третьих, речь тут про сервисы, которые не передают особенно "чувствительных данных": HTTPS сейчас повсюду, поэтому отладка даже с публичными данными требует раскрытия трафика. Не забывайте про стандартную практику TLS-терминирования, да и практическую стойкость TLS не стоит преувеличивать.

Комментарии (0)