Практикум: экспорт ключей TLS на примере библиотеки Go

Иногда при отладке программ требуется просматривать трафик, защищённый TLS. Распространённый случай – HTTPS. Чтобы просматривать содержание TLS-трафика при помощи анализаторов протоколов (Wireshark) нужны сессионные ключи. В этой записке посмотрим, с примерами кода, как экспортировать эти ключи, если используется типовая библиотека Go crypto/tls.

Принцип довольно простой: при установлении TLS-соединения библиотека будет выводить сеансовые симметричные секреты и сопроводительный параметр (поле Random) в заданный файл, который позже использует Wireshark (или tshark, как в примерах ниже) для раскрытия трафика. Чтобы это заработало в 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

Так сконфигурированный экземпляр TLS-соединения (в смысле кода Go) будет писать в заданный файл s-keys.txt сведения о ключах, в текстовом формате. Сам формат очень простой, но отличается для TLS 1.3 и других версий (см. ниже). Полный пример работающего кода для TLS-клиента (логика кода экспорта ключей для сервера – не отличается):

package main

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)
	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)
}

Используя привычные многим tcpdump и tshark нетрудно посмотреть, что получилось. В tshark файл с сессионными секретами передаётся при помощи опции tls.keylog_file (в Wireshark – можно загрузить через интерфейс редактирования параметров TLS-парсера). Вызов 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-секреты.
Фрагмент результата работы:

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 без секретных ключей сессии не удалось бы посмотреть даже серверные сертификаты. Так что, вообще говоря, для того, чтобы убедится в работоспособности метода, можно было бы даже и не отправлять GET-запрос HTTP, как написано в коде выше: для минимальной проверки в TLS 1.3 достаточно сразу разорвать TLS-соединение, при этом факт успешной записи части сессионных ключей в файл отразится в том, что tshark смог разобрать TLS-сертификаты, присланные сервером. Другое дело, что в TLS 1.3 для зашифрования сертификатов используются другие ключи, не те, что для защиты трафика (см. ниже).

Почему это работает? Прежде всего потому, что в TLS для защиты трафика в рамках сессии используются симметричные шифры и, соответственно, симметричный набор секретов для получения не менее симметричных секретных ключей. Так что для расшифрования не нужно получать “ключи сервера” или “ключи от сертификата сервера”. Симметричных ключей используется пара: один для отправки данных, второй – для получения (на клиенте и сервере эти ключи меняются местами). Соответственно, если анализатор трафика знает симметричные ключи, то он может расшифровать записанный трафик. Посмотрим, какой формат у файла с экспортированными ключами. Для 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%] записывается значение из поля Random начального сообщения ClientHello. Это может показаться загадочным, если вы не сталкивались с TLS ранее, однако тут данное значение нужно лишь для того, чтобы анализатор протокола мог быстро сопоставить конкретный секрет и конкретную TLS-сессию в записанном трафике: то есть, в данном случае, это всего лишь метка. Сам секрет, позволяющий получить ключи для шифров, указывается в поле [%SECRET%]. (Ключи вычисляются на основе данного секрета при помощи соответствующей функции HKDF.)

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

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

CLIENT_RANDOM [%RANDOM%] [%SECRET%]

Это обусловлено тем, что в TLS 1.2 используется другая схема получения сессионных ключей, в ней нет таких этапов, как в TLS 1.3, а защита трафика включается позже.

***

Некоторые другие записки по этой же теме:

TLS для DevOps
Секретные ключи в трафике и симметричные шифры
TLS в виртуальных машинах и извлечение ключей хостингом

Технические детали устройства TLS – можно узнать в техническом описании.

Адрес записки: https://dxdt.ru/2024/10/06/13985/

Похожие записки:



Далее - мнения и дискуссии

(Сообщения ниже добавляются читателями сайта, через форму, расположенную в конце страницы.)

Написать комментарий

Ваш комментарий:

Введите ключевое слово "34QU5" латиницей СПРАВА НАЛЕВО (<--) без кавычек: (это необходимо для защиты от спама).

Если видите "капчу", то решите её. Это необходимо для отправки комментария ("капча" не применяется для зарегистрированных пользователей). Обычно, комментарии поступают на премодерацию, которая нередко занимает продолжительное время.