В TLS 1.3 клиент в первом же сообщении присылает серверу не только список поддерживаемых групп, но и готовые обмены ключами для некоторых из них. Если угадал с группой — рукопожатие идёт в один круг; если нет — сервер просит повторить, и добавляется лишний round-tripодин полный обмен сообщениями туда и обратно между клиентом и сервером. Разбираем, как устроен этот выбор в BoringSSL и OpenSSL, чем плох статический набор групп и что меняется при динамическом.
Зачем вообще key_share в первом полёте
Клиент формирует расширение key_share в ClientHello, выбирая группы для обмена ключами и генерируя для каждой отдельную пару ключей. Список групп определяется при подготовке первого полёта: если клиент обязан использовать конкретную группу вместо своего обычного выбора (это происходит, когда сервер уже запросил её, например после HelloRetryRequestповторный запрос сервера, когда первый ClientHello не дал подходящей группы), берётся только эта группа; иначе, если сервер прислал supported_groups, выбирается первая группа из supported_group_list, которая есть в server_hint_list; иначе используются явно заданные client_key_share_selections; иначе по умолчанию берётся supported_group_list[0] и добавляется одна группа другого типа (пост-квантовая или классическая).
Для каждой выбранной группы создаётся объект SSLKeyShare, в буфер добавляются group_id, длина и результат генерации обмена ключами, а сам объект сохраняется в hs->key_shares. Отправка нескольких KeyShareEntry увеличивает размер первого полёта: каждая запись добавляет group_id и key_exchange в key_share_bytes, которые затем целиком копируются в расширение. Стоимость генерации ключей растёт, потому что генерация вызывается для каждой группы: для эллиптических кривых генерируется приватный ключ и вычисляется публичная точка, для X25519 — вызывается X25519_keypair.
RFC 8446 разрешает клиенту предлагать столько KeyShareEntry, сколько групп он поддерживает, и требует, чтобы значения key_exchange для каждой записи генерировались независимо.
Что происходит, если группа не угадана
Если клиент не прислал подходящий key_share — например, только неприемлемые или неподдерживаемые сервером группы DHE/ECDHE — сервер исправляет несоответствие с помощью HelloRetryRequest, и клиент должен перезапустить рукопожатие с подходящим расширением key_share.
HelloRetryRequest имеет тот же формат, что и ServerHelloответное сообщение сервера, которым он подтверждает параметры рукопожатия и завершает согласование версии и шифра, и его расширения MUST содержать supported_versions, а также SHOULD содержать минимальный набор расширений, необходимых клиенту для формирования корректного ClientHello. В частности, он может содержать расширение key_share с указанием выбранной группы (selected_group), чтобы клиент заменил исходное расширение key_share на новое, содержащее только новый KeyShareEntry для указанной группы.
Клиент, получив HelloRetryRequest, должен проверить legacy_version, legacy_session_id_echo, cipher_suite и legacy_compression_method, а затем обработать расширения, начиная с supported_versions. Если HelloRetryRequest не приведёт к изменению ClientHello, клиент MUST прервать рукопожатие с алертом illegal_parameter, а при получении второго HelloRetryRequest в одном соединении — с алертом unexpected_message.
Дополнительный RTT возникает потому, что после HelloRetryRequest клиент отправляет новый ClientHello, и только затем сервер отвечает ServerHello:
ClientHello + key_share --------> HelloRetryRequest <-------- + key_share
ClientHello + key_share --------> ServerHello + key_shareКак устроен выбор группы на клиенте
BoringSSL
ssl_setup_key_shares сначала очищает hs->key_shares и hs->key_share_bytes, затем не создаёт key shares, если hs->max_version < TLS1_3_VERSION или задан hs->pake_prover — признак того, что клиент предлагает PAKEобмен ключами по паролю, при котором стороны аутентифицируются общим секретом вместо сертификатов. Если supported_group_list пуст, возвращается ошибка SSL_R_NO_GROUPS_SPECIFIED.
Далее выбирается набор групп: при override_group_id != 0 используется только эта группа; иначе, если есть server_supported_groups_hint, выбирается первая группа из supported_group_list, которая есть в подсказке; иначе, если задан client_key_share_selections, используется он; иначе по умолчанию берётся supported_group_list[0] и добавляется одна группа с противоположным значением is_post_quantum_group, после чего список обрезается до двух групп.
В ext_key_share_add_clienthello для каждой выбранной группы создаётся SSLKeyShare::Create(group_id), генерируется ключ и добавляется в буфер; если override_group_id == 0 и включён GREASEмеханизм добавления заведомо неизвестных значений, чтобы проверить, что партнёр корректно их игнорирует, перед этим добавляется фиктивная группа — она нужна, чтобы сервер не полагался на точный список групп. Число групп в key_share зависит от выбранного набора: при override_group_id — одна, при подсказке сервера — одна, при явных client_key_share_selections — сколько задано, иначе — до двух (одна пост-квантовая и одна классическая).
Пост-квантовыми считаются только две группы: SSL_GROUP_X25519_MLKEM768 и SSL_GROUP_MLKEM1024.
OpenSSL
tls_construct_ctos_key_share выбирает группы так: если s->s3.group_id != 0 и s->s3.tmp.pkey == NULL, добавляется ровно одна группа s->s3.group_id, а s->s3.tmp.num_ks_pkey сбрасывается в 0; иначе берётся список групп через tls1_get_requested_keyshare_groups, и если он содержит единственный элемент 0 (признак отсутствия префикса *), то список заменяется на tls1_get_supported_groups и включается режим add_only_one.
Далее в цикле по группам каждая проверяется на допустимость и валидность, и для подходящей вызывается add_key_share; при add_only_one цикл прерывается после первой добавленной группы. Внутри add_key_share, если номер итерации меньше s->s3.tmp.num_ks_pkey, берётся уже сохранённый ключ, иначе генерируется новый; публичный ключ кодируется и записывается в пакет как KeyShareEntry. Для первой группы сохраняются s->s3.tmp.pkey и s->s3.group_id, а также заполняются массивы сохранённых ключей и их групп.
После полученного HRRповторный запрос сервера, которым он просит клиента прислать key_share для другой группы проверяется, что запрошенная группа не входила в уже отправленные, что она поддерживается и допустима; затем s->s3.group_id обновляется, все старые ключи освобождаются, счётчик сохранённых ключей обнуляется и s->s3.tmp.pkey сбрасывается. При повторном построении ClientHello после HRR GREASE-элемент key_share не добавляется, если HRR ожидается, группа уже выбрана и публичный ключ ещё не сгенерирован, — чтобы запрошенная запись была единственной.
supported_groups и key_share — разные расширения с разной стоимостью
Список supported_groups формируется иначе: сначала проверяется, не используется ли PAKE, и если да, расширение не отправляется; затем создаётся вложенная структура из двух length-prefixed полей, при включённом GREASE добавляется фиктивная группа, после чего перебирается список поддерживаемых групп и для каждой, кроме пост-квантовых при max_version < TLS1_3_VERSION, добавляется её идентификатор. В OpenSSL список берётся через tls1_get_supported_groups, при SSL_OP_GREASE добавляется GREASE-группа, а затем каждая группа проверяется на валидность и допустимость и отбрасывается, если она несовместима с выбранными ECDHE/FFDHE или не разрешена политикой.
key_share — отдельное расширение: оно не отправляется при max_version < TLS1_3_VERSION или при PAKE, иначе упаковывает уже сгенерированные hs->key_share_bytes. Разная стоимость объясняется тем, что supported_groups содержит только идентификаторы групп, а key_share содержит реальные криптографические параметры: генерацию ключей и сериализацию публичного ключа. RFC 8446 также различает их: supported_groups перечисляет поддерживаемые группы, а key_share содержит криптографические параметры, причём клиент MAY отправить пустой client_shares, чтобы запросить выбор группы сервером ценой дополнительного round trip.
Как устроен выбор группы на сервере
OpenSSL
Функция tls_accept_ksgroup принимает выбранную группу обмена ключами: записывает её в s->s3.group_id и s->s3.group_id_candidate, кэширует в сессии, генерирует параметры группы и устанавливает открытый ключ клиента.
check_overlap ищет первое пересечение между списком приоритетных групп и списком кандидатов: перебирает кандидатов, для каждого проверяет наличие в приоритетном списке, допустимость и валидность, и если найденный индекс в приоритетном списке меньше ранее сохранённого, обновляет выбранную группу.
При включённой опции SSL_OP_SERVER_PREFERENCE сервер сначала ищет пересечение между группами текущего кортежа сервера и присланными key shares, и при наличии принимает эту группу; если пересечения с key shares нет, ищет пересечение с supported_groups клиента и при наличии запоминает группу для HRR. Без этой опции приоритет у клиента: сначала ищется пересечение между key shares клиента и текущим кортежем сервера, и при наличии группа принимается; иначе ищется пересечение supported_groups клиента с кортежем сервера и при наличии группа запоминается для HRR. Если ни key shares, ни supported_groups не пересекаются с текущим кортежем, сервер переходит к следующему кортежу.
Когда сервер отвечает ServerHello, а когда шлёт HRR
hrr_sends_key_share возвращает истину, только если s->s3.peer_tmp == NULL (то есть сервер ещё не получил от клиента параметры обмена ключами для выбранной группы) и s->s3.group_id != 0. В tls_construct_stoc_key_share, если сервер уже отправил HRR и hrr_sends_key_share ложно, расширение key_share не отправляется; иначе в ServerHello кладётся только выбранный s->s3.group_id без публичного ключа. Если HRR не ожидается и s->s3.peer_tmp == NULL, значит key_share от клиента не получен — это возможно только при возобновлении сессии, и тогда расширение не отправляется. Если сессия возобновляется и в режиме PSKpre-shared key — заранее согласованный секрет, позволяющий возобновить сессию без полного рукопожатия не выставлен флаг DHE, сервер тоже не отправляет key_share.
Во всех остальных случаях сервер формирует расширение key_share с выбранной группой и, в зависимости от типа группы, либо генерирует обычный ключ и выполняет вывод общего секрета, либо выполняет инкапсуляцию KEM, после чего помечает обмен ключами выполненным.
BoringSSL
do_send_hello_retry_request формирует HelloRetryRequest: собирает сообщение ServerHello со случайным значением-маркером, session_id, выбранным шифром и расширениями supported_versions и key_share, где в key_share указывается группа, которую сервер просит клиента использовать. После отправки HRR выставляется флаг used_hello_retry_request и состояние переходит к чтению второго ClientHello.
do_read_second_client_hello получает следующее сообщение, проверяет, что это ClientHello, и разбирает его; при ошибке разбора отправляется фатальный алерт decode_error. Проверка нового key_share выполняется при разборе расширения: расширение обязательно, иначе ошибка SSL_R_MISSING_KEY_SHARE с алертом missing_extension; список key shares разбирается, ищется запись с запрошенной группой, при повторной записи для той же группы возвращается SSL_R_DUPLICATE_KEY_SHARE с алертом illegal_parameter, а признак нахождения выставляется только если найден непустой ключ.
HelloRetryRequest: точные условия и последствия
Сервер отправляет HelloRetryRequest, если он может найти приемлемый набор параметров, но ClientHello не содержит достаточной информации для продолжения рукопожатия. В обновлённом ClientHello клиент должен заменить исходное расширение key_share на содержащее только новый KeyShareEntry для группы, указанной в selected_group. Также клиент должен обновить расширение pre_shared_key, если оно присутствует, пересчитав obfuscated_ticket_age и binder, и опционально удалив несовместимые PSK; может опционально добавить, удалить или изменить длину padding. Клиент не должен предлагать PSK, связанные с хешем, отличным от хеша выбранного cipher suite.
Обработка на клиенте
do_read_hello_retry_request читает сообщение, парсит ServerHello, проверяет, что выбранный шифр был предложен и поддерживается версией, инициализирует transcript hash и, если это HRR, обновляет его специальным образом. Затем разбираются расширения cookie, key_share, supported_versions и encrypted_client_hello; если нет ни cookie, ни key_share, возвращается ошибка EMPTY_HELLO_RETRY_REQUEST. Если key_share присутствует, извлекается group_id, проверяется, что группа поддерживается и что она не была в исходном ClientHello; затем заново формируются key shares. После этого transcript hash обновляется, а для принятого ECH — и внутренний transcript; PSK, чей хеш не совпадает с выбранным шифром, удаляются.
do_send_second_client_hello строит второй ClientHello (при принятом ECH — с пустым enc), добавляет его и переходит к чтению ServerHello.
Проверки в OpenSSL
tls_parse_stoc_key_share сначала читает group_id и отвергает GREASE-значения. Если это HelloRetryRequest, она получает список поддерживаемых групп и ищет group_id в нём. При нарушении — группа не найдена, не разрешена или невалидна — вызывается фатальная ошибка SSL_R_BAD_KEY_SHARE с алертом illegal_parameter. В обычном ServerHello вместо supported_groups проверяется, что group_id совпадает с одной из групп, для которых клиент уже отправил key share; если совпадения нет, также возвращается SSL_R_BAD_KEY_SHARE.
Пограничные случаи и защита от зацикливания
Повторный HRR блокируется при чтении ServerHello: если после уже отправленного второго ClientHello приходит ещё один HRR, клиент отвечает фатальным алертом UNEXPECTED_MESSAGE и завершает рукопожатие.
В do_read_hello_retry_request защита строится иначе: при разборе key_share в HRR проверяется, что запрошенная группа не входила в уже отправленные key shares, иначе — ILLEGAL_PARAMETER и ошибка WRONG_CURVE. Аналогичная проверка есть в tls_parse_stoc_key_share: перебор уже отправленных групп и сравнение с запрошенной даёт SSL_R_BAD_KEY_SHARE. Дополнительно требуется, чтобы HRR содержал хотя бы cookie или key_share, иначе EMPTY_HELLO_RETRY_REQUEST. После обработки HRR выставляется флаг used_hello_retry_request, а состояние переводится к отправке второго ClientHello, что исключает повторную обработку HRR в этом же полёте.
Дубликаты расширений
tls1_check_duplicate_extensions проверяет блок расширений ClientHello или ServerHello на дубликаты типов: сначала подсчитывает расширения, затем собирает их типы в массив, сортирует его и сравнивает соседние элементы — при совпадении типов дубликат обнаружен. Если расширений нет, проверка сразу завершается успешно. Порядок расширений при этом не проверяется.
ssl_parse_client_hello_with_trailing_data разбирает ClientHello: читает версию, случайное значение, session_id (с проверкой длины), для DTLS — cookie, затем cipher_suites (длина не меньше 2 и чётная) и compression_methods (длина не меньше 1); при ошибке любого шага разбор прерывается с ошибкой SSL_R_CLIENTHELLO_PARSE_FAILED.
Cookie в HRR
tls_parse_ctos_cookie сначала решает, нужно ли вообще проверять cookie: она игнорирует cookie, если нет ни одного колбэка проверки или если cookie не требуется. Затем разбирает cookie как длину с 2-байтовым префиксом и проверяет, что его длина не меньше SHA256_DIGEST_LENGTH, отделяя последние SHA256_DIGEST_LENGTH байт как HMAC. Далее вычисляет HMAC-SHA256 по оставшейся части cookie и сравнивает с полученным; при несовпадении возвращает ошибку SSL_R_COOKIE_MISMATCH.
После этого читается 2-байтовое поле format, и если оно не равно COOKIE_STATE_FORMAT_VERSION, cookie игнорируется. Затем проверяется версия: она должна совпадать с TLS 1.3, иначе ошибка SSL_R_BAD_PROTOCOL_VERSION_NUMBER. Далее читаются group_id и cipher, и если они не совпадают с текущими, возвращается ошибка SSL_R_BAD_CIPHER. Наконец, читаются оставшиеся поля, и проверяется, что после них осталось ровно SHA256_DIGEST_LENGTH байт.
Возраст cookie проверяется так: если время из cookie больше текущего или прошло больше 600 секунд, cookie считается устаревшим и игнорируется.
Что видит клиент и как это влияет на latency
При несовпадении параметров сервер отвечает HelloRetryRequest — сообщением того же формата, что и ServerHello, но с полем random, равным маркеру kHelloRetryRequest, и расширениями supported_versions, cookie и key_share. Клиент, получив HRR, обязан проверить служебные поля, затем обработать расширения и отправить второй обновлённый ClientHello; при этом transcript не сбрасывается, а дополняется специальным образом. Второй HRR в одном соединении запрещён — клиент обязан прервать рукопожатие с алертом unexpected_message. Сервер, отправляя HRR, обновляет transcript и переходит в состояние отправки HRR, а затем читает второй ClientHello.
0-RTT и early_data при HRR
При HRR второй ClientHello никогда не предлагает early data: в BoringSSL при установленном флаге used_hello_retry_request расширение не добавляется, а при разборе ServerHello стоит утверждение, что второй ClientHello early data не предлагает, поэтому расширения early data автоматически отвергаются как незапрошенные. Если сервер не прислал расширение early_data и HRR не использовался, причина отказа фиксируется как «сервер отклонил» при повторном использовании сессии или «сессия не возобновлена» иначе; при HRR причина уже была заполнена ранее. Принятие early data сервером требует, чтобы сессия была возобновлена: иначе возвращается unsupported_extension, а при успехе выставляются признаки принятия.
В OpenSSL early data при HRR отвергается с алертом illegal_parameter. PSK binder проверяется сравнением вычисленного значения с присланным; при несовпадении — ошибка SSL_R_DIGEST_CHECK_FAILED и алерт decrypt_error. Клиент BoringSSL предлагает только режим PSK_DHE_KE, а сервер принимает PSK только если в списке клиента есть этот режим; иначе расширение pre_shared_key игнорируется.
Компромиссы: почему отказываются от статического выбора
Отправка нескольких key_share в первом ClientHello снижает вероятность HRR, потому что сервер может найти подходящую группу среди предложенных. Однако каждый key_share увеличивает размер ClientHello, так как для каждого выбранного group_id создаётся отдельная запись с сгенерированным обменом ключами. Генерация ключей для каждого key_share требует вычислительных затрат: для эллиптических кривых — генерация приватного ключа и вычисление публичной точки.
В BoringSSL по умолчанию выбираются две группы: наиболее предпочтительная и одна пост-квантовая или классическая, что увеличивает и размер, и стоимость по сравнению с одной группой.
Динамический выбор групп в BoringSSL
Список групп по умолчанию — фиксированный массив из четырёх идентификаторов в порядке предпочтения: сначала гибридная пост-квантовая группа X25519MLKEM768, затем классическая X25519, затем P-256 и P-384. Полный перечень известных групп задаётся таблицей, где каждой группе сопоставлены NID, group_id, имя и алиас; функции поиска выполняют поиск по этой таблице. Конкретная реализация группы выбирается по group_id: для P-256/P-384/P-521 — эллиптическая, для X25519 — X25519, для X25519MLKEM768 — гибридная, для MLKEM1024 — пост-квантовая, иначе — nullptr.
Пост-квантовыми считаются только две группы. Компромисс между безопасностью и производительностью виден в том, что пост-квантовые группы пропускаются, если максимальная версия ниже TLS 1.3, то есть они не предлагаются в старых протоколах. При этом в списке по умолчанию гибридная пост-квантовая группа стоит первой, что даёт приоритет пост-квантовой стойкости, а классические группы оставлены для совместимости и как более дешёвые альтернативы.
Как сравнивали и что получилось
Стенд и методология
Использовался тестовый стенд: виртуальная машина KVM с Ubuntu 26.04 и OpenSSL 3.5.5, веб-сервер Angie 1.11.7, процессор AMD Ryzen 7 5800x (4 CPU в ВМ), 4096 МБ оперативной памяти; хостовая ОС — Ubuntu 24.04 с OpenSSL 3.0.13, нагрузочная утилита wrkx. Сравнивались три варианта: базовый Angie из пакетов с системным OpenSSL 3.5.5, статическая сборка Angie с OpenSSL 4.0.1 и статическая сборка Angie с AWS-LC 5.0.0; переключение между статическими сборками выполнялось через символическую ссылку /usr/sbin/angie.
В конфигурации ssl_protocols заданы TLSv1.2 и TLSv1.3, а директивы ssl_conf_command для Ciphersuites закомментированы. В тестовом сервере активны сертификаты ECDSA, а сертификаты RSA закомментированы.
Применялись три сценария нагрузки:
- Измерение скорости TLS-рукопожатий: запрос к локации
/zero/, отдающей минимальный ответ, с заголовкомConnection: close, при 4 потоках, 100 соединениях, длительности 20 с и ограничении 10000 запросов в секунду. - Загрузка малых ресурсов: запрос файла
1kb.txtпри 4 потоках, 100 соединениях, 20 с и 100000 запросов в секунду. - Тестирование пропускной способности: запрос файла
100mb.txtпри 4 потоках, 16 соединениях, 20 с и 32 запросах в секунду.
Тесты запускались на хостовой машине в сторону ВМ. Для исключения побочных факторов использовался один рабочий процесс Angie, и во всех сценариях достигалась полная утилизация процессора работой процесса Angie (100%).
Результаты
TLS 1.3 с ECDSA-сертификатами, тест рукопожатий (CPS):
| Библиотека | CPS |
|---|---|
| AWS-LC 5.0.0 | 7201 |
| OpenSSL 3.5.5 | 3888,2 |
| OpenSSL 4.0.1 | 4268,8 |
TLS 1.3, keep-alive, ECDSA-сертификат, шифр TLS_AES_256_GCM_SHA384, тест малых запросов (RPS):
| Библиотека | RPS |
|---|---|
| AWS-LC 5.0.0 | 74368 |
| OpenSSL 3.5.5 | 73769,2 |
| OpenSSL 4.0.1 | 72126,4 |
В рукопожатиях AWS-LC в явных лидерах, а версии OpenSSL различаются в пользу более свежей. В тесте малых запросов AWS-LC практически сравнялся с двумя версиями OpenSSL, сохранив небольшое преимущество.
Тест пропускной способности (TLS 1.3, сертификат ECDSA, шифр TLS_AES_256_GCM_SHA384), ГБ/с:
| Библиотека | ГБ/с |
|---|---|
| AWS-LC 5.0.0 | 1,612 |
| OpenSSL 3.5.5 | 1,508 |
| OpenSSL 4.0.1 | 1,498 |
Основной вклад в результат теста должна вносить реализация симметричного шифрования AES-256. По результатам побеждает AWS-LC, но различие между всеми вариантами укладывается в 10%, и отрыв слишком мал для кастомной сборки Angie.
Отдельно для OpenSSL 3.5.5 измерены три шифра: AES-128 — 1,554; AES-256 — 1,524; CHACHA20 — 1,18 ГБ/сек. Побеждает AES-128, за ним с небольшим отрывом AES-256, последний — CHACHA20, различие около 30%. Встроенный бенчмарк openssl показывает лучшие результаты для AES-128, далее AES-256 и CHACHA20, при этом разница между первым и третьим укладывается в 20%.
Оговорки
Значения некоторых директив изменялись, чтобы обеспечить условия теста, в частности версии ssl_protocols и шифры в ssl_conf_command. Возможности процессора могут значительно влиять на результаты, например наличие инструкций AES-NI или векторных расширений AVX. Выбор конкретного варианта зависит от задачи и требует подробного тестирования в реалистичных условиях нагрузки.
Что из этого следует на практике
- Статический набор key shares — это ставка на то, что сервер выберет одну из угаданных групп. Промах стоит лишнего round-trip: клиент шлёт второй ClientHello, и только потом приходит ServerHello.
- Расширение
supported_groupsпочти бесплатно — это лишь идентификаторы групп.key_shareстоит дорого: для каждой группы генерируется отдельная пара ключей, и для эллиптических кривых это включает вычисление публичной точки. - Динамический выбор позволяет подстроить набор групп под подсказку сервера или явную конфигурацию, но по умолчанию BoringSSL всё равно отправляет до двух групп — одну пост-квантовую и одну классическую, — чтобы снизить вероятность HRR, не раздувая первый полёт.
- Пост-квантовые группы предлагаются только при TLS 1.3 и выше; в списке по умолчанию гибридная пост-квантовая группа стоит первой, а классические оставлены для совместимости и как более дешёвые альтернативы.
- Повторный HRR запрещён: клиент обязан прервать рукопожатие. Cookie в HRR имеет ограниченный срок жизни — 600 секунд, после чего игнорируется.
- При HRR early data не предлагается, а PSK, чей хеш не совпадает с выбранным шифром, удаляются; binder пересчитывается.
- По результатам бенчмарка Angie разница в пропускной способности между AWS-LC и обеими версиями OpenSSL укладывается в 10%, а в тесте рукопожатий AWS-LC опережает OpenSSL более чем в полтора раза — то есть выигрыш от смены TLS-библиотеки проявляется прежде всего на нагрузке, где доминируют рукопожатия.
Где смотреть в коде
- tls13_client.cc: do_send_second_client_hello
- extensions.cc: is_post_quantum_group
- extensions_srvr.c: hrr_sends_key_share
- extensions.cc: ext_early_data_parse_serverhello
- extensions.cc: ext_key_share_add_clienthello
- extensions.cc: ext_supported_groups_add_clienthello
- tls13_client.cc: cookie
- tls13_server.cc: do_read_second_client_hello