Техническое: переходим с ECDH на ML-KEM в проекте на Go
В Go 1.24 появилась типовая библиотека, реализующая ML-KEM – crypto/mlkem. На ML-KEM, если нужно задекларировать постквантовую стойкость, нетрудно перейти, например, с ECDH – “эллиптического” протокола Диффи-Хеллмана. Предположим, что у нас ECDH используется для получения общего секрета при обмене данными между микросервисами по HTTP: один из сервисов выступает HTTP-клиентом и сначала запрашивает у сервера сеансовый открытый ключ ECDH (далее – просто DH), а потом в ответном сообщении передаёт свой открытый ключ, вместе с зашифрованными данными. Данные зашифровываются симметричным шифром, а симметричный ключ вычисляется на основе общего секрета DH.
ECDH – протокол симметричный, если смотреть на уровне привычных операций: и на клиенте, и на сервере требуется две одинаковых операции – умножение на скаляр (M). Ниже приведена схема.
Здесь A, B – открытые параметры (ключи) DH. Блоком с буквой M обозначена основная операция протокола (умножение на скаляр).
А вот ML-KEM, на таком же уровне, симметрии не демонстрирует – здесь на сервере используются две операции: первая (K) – получение секрета для декапсуляции, этот секрет выводится вместе с открытым ключом (ключ инкапсуляции); вторая (D) – декапсуляция секрета из полученного от клиента шифротекста. На клиенте – работает одна операция (E), вычисление общего секрета с инкапсуляцией его в виде шифротекста. Посмотрим на схему.
(Здесь EK – открытый ключ, CT – шифротекст.)
Хорошо видно, что схема перестала быть симметричной. Однако, если немного подрегулировать “абстракцию”, то можно убрать преобразования DH на стороне клиента в блок E (инкапсуляция). Вот так:
Если A и B справа поменяются местами, то получится в точности схема ML-KEM. Собственно, часто так и делают: алгоритм Диффи-Хеллмана можно строго переписать в терминах KEM, это известно. Следовательно, нетрудно заменить в проекте, реализованном на Go, ECDH на ML-KEM: доработки оказываются минимальными. Посмотрим чуть детальнее.
Во-первых, речь вот про что. В начале сессии клиент отправляет HTTP-запрос GET на адрес API “коннектора”, где получает, кроме прочего, сессионный ключ. Ответ приходит в JSON. Ключ сервера находится в “блобе” SessionKey (Base64). Выглядит это примерно так, как в распечатке ниже. Кроме ключа в ответе есть токен, таймстемп и подпись ECDSA (на все поля), но это здесь “для антуража”, так что в данной записке не рассматривается.
type serverMessage struct { Id uint64 `json:"Id"` Timestamp uint64 `json:"Timestamp"` SessionKey []byte `json:"SessionKey"` ECDSASig []byte `json:"Sig"` }
Во-вторых, мы всего лишь хотим получать вместо DH – ключ ML-KEM. Передавать “блоб” ключа можно в том же поле – в SessionKey.
Чтобы сгенерировать на сервере ключ, нужно сделать пару вызовов crypto/mlkem:
var sMsg serverMessage var err error [...] // В sMsg записывается принятый JSON-объект. dk, err := mlkem.GenerateKey768() if err != nil { fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err) return } sMsg.SessionKey = append(sMsg.SessionKey, dk.EncapsulationKey().Bytes()[0:]...) [...]
dk – это секретный ключ, он временно сохраняется на сервере в привязке к сессии.
Клиент получает общий секрет и шифротекст, записывает шифротекст в запрос, который отправляет по адресу API для обработки сессии. Формат данных клиента, предположим, такой:
type clientMessage struct { Id uint64 `json:"Id"` Timestamp uint64 `json:"Timestamp"` MLKEMCT []byte `json:"MLKEMCT"` ECDSASig []byte `json:"Sig"` }
Здесь уже придётся поменять имя поля на MLKEMCT – раньше тут был бы открытый параметр DH клиента (SessionKey).
В коде, обработка на стороне клиента выглядит так:
ek, err := mlkem.NewEncapsulationKey768(sMsg.MLKEMKey) if err != nil { fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err) return } sharedSecret, ct := ek.Encapsulate() var cMsg clientMessage cMsg.MLKEMCT = append(cMsg.MLKEMCT, ct[0:]...) [...]
Когда клиент пришлёт шифротекст, серверу нужно извлечь из него общий секрет, это делается так (используется сохранённый в обработчике “коннектора” dk):
var cMsg clientMessage sharedSecret, err := dk.Decapsulate(cMsg.MLKEMCiphertext) if err != nil { fmt.Fprintf(os.Stderr, "ML-KEM error: %s\n", err) return } [...]
Алгоритмически, от ECDH, реализуемого типовой библиотекой crypto/ecdh, отличается мало. Но замена даёт постквантовую стойкость и ML-KEM работает даже несколько быстрее, чем ECDH.
Может показаться, что в новой схеме сервер генерирует какой-то ключ, а клиент этот ключ использует, а в варианте с ECDH – ключи генерировали обе стороны. Однако, во-первых, секрет здесь генерируется и на стороне клиента; во-вторых, при использовании DH, секрет каждой из сторон раскрывает общий сессионный секрет – то есть, в DH достаточно знать секретный параметр сервера или клиента, чтобы из открытого параметра вычислить общий секрет, использованный для защиты трафика. Так что схема ML-KEM в этой части, на практике, не отличается (хоть и устроена иначе). При желании, можно сделать “гибрид”, как в TLS, но тогда придётся и передавать параметры DH в дополнение к ML-KEM, и хранить дополнительные ключи, пока не установлена сессия. Формально, спецификация ML-KEM не требует “гибридизации” – эта криптосистема заявлена как стойкая, поэтому, если соответствия спецификации достаточно, то – достаточно.
Адрес записки: https://dxdt.ru/2025/03/08/15129/
Похожие записки:
- Кубиты от IBM
- Техническое описание TLS: обновление 2022
- RCE через ssh-agent
- Подстановки и определение понятия бита
- Техническое: TLS-ALPN Control Validation
- Python, "численный" j-инвариант и десятичные цифры
- Gitea и омоглифы не в ту сторону
- Про цепочки, RSA и ECDSA
- Техническое: имена в TLS и Nginx
- Следы звуков в кодах для LLM
- Целевая подмена приложений и "прокси" для утечек
Написать комментарий