Техническое: переходим с ECDH на ML-KEM в проекте на Go

В Go 1.24 появилась типовая библиотека, реализующая ML-KEM – crypto/mlkem. На ML-KEM, если нужно задекларировать постквантовую стойкость, нетрудно перейти, например, с ECDH – “эллиптического” протокола Диффи-Хеллмана. Предположим, что у нас ECDH используется для получения общего секрета при обмене данными между микросервисами по HTTP: один из сервисов выступает HTTP-клиентом и сначала запрашивает у сервера сеансовый открытый ключ ECDH (далее – просто DH), а потом в ответном сообщении передаёт свой открытый ключ, вместе с зашифрованными данными. Данные зашифровываются симметричным шифром, а симметричный ключ вычисляется на основе общего секрета DH.

ECDH – протокол симметричный, если смотреть на уровне привычных операций: и на клиенте, и на сервере требуется две одинаковых операции – умножение на скаляр (M). Ниже приведена схема.

ECDH
Здесь A, B – открытые параметры (ключи) DH. Блоком с буквой M обозначена основная операция протокола (умножение на скаляр).

А вот ML-KEM, на таком же уровне, симметрии не демонстрирует – здесь на сервере используются две операции: первая (K) – получение секрета для декапсуляции, этот секрет выводится вместе с открытым ключом (ключ инкапсуляции); вторая (D) – декапсуляция секрета из полученного от клиента шифротекста. На клиенте – работает одна операция (E), вычисление общего секрета с инкапсуляцией его в виде шифротекста. Посмотрим на схему.

ML-KEM
(Здесь EK – открытый ключ, CT – шифротекст.)
Хорошо видно, что схема перестала быть симметричной. Однако, если немного подрегулировать “абстракцию”, то можно убрать преобразования DH на стороне клиента в блок E (инкапсуляция). Вот так:

ECDH

Если 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/

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



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

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

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

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

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

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