ESPN DeportesFalcons amenazan invicto de 49ersThe Jerusalem PostInfamous Nazi-era alpine retreat mostly destroyed in deliberate firePunchLagos begins online verification for pensionersESPNBaylor fires Dave Aranda: Who could be next and what are roster concerns?BBC SportWatch: Sportscene - Sunday's Scottish Premiership highlightsDaily MaverickBUSINESS REFLECTION: Crossed Wires: Multiple Gutenberg moments and the defining question of our ageZDF heuteAktuelle Pressemitteilungen des ZDFBBC News BrasilVotos do Nordeste demoram mais para chegar ao TSE? Tudo para acompanhar a apuração7sur7Des lycéens annoncent une rencontre lundi avec le Premier ministreSportstarBarcelona vs Real Madrid, El Clasico LIVE — Mbappe, Yamal start in derby before Ballon d'Or nighttazSymbol der Anti-AKW-Bewegung: Immer umkämpftBusiness AMBill Gates waarschuwt voor grote economische gevolgen van artificiële intelligentie
The Daily Newsstand · Free, Always
Sunday, October 25, 2026

Гибридная постквантовая печать сообщений: X25519 + ML-KEM-1024 и тесты, которые запускает каждый

Translate

Почти каждый мессенджер пишет «мы не можем прочитать ваши сообщения», но проверить это обычно нельзя. В мессенджере, который я разрабатываю (Umbera), я пошёл от обратного: опубликовал криптографическое ядро — ту часть кода, которая определяет, что может узнать сервер, — и написал тесты, которые воспроизводят схему и проверяют её свойства одной командой.

Ниже — гибридная постквантовая печать сообщения (X25519 + ML-KEM-1024), почему взлом одного из алгоритмов ничего не даёт, какие метаданные сервер всё-таки видит, как защитить DTLS-SRTP от подмены отпечатков при установке звонка, и как проверить каждое утверждение самому.

Коротко

  • Ключ печати выводится из двух общих секретов — классического (X25519) и постквантового (ML-KEM-1024). Атакующему придётся решить обе задачи.

  • В хранилище релея попадает только пара «идентификатор ящика + непрозрачный блоб»; полей отправителя и получателя в записи нет.

  • Медиа звонка защищено DTLS-SRTP по схеме end-to-end, TURN видит лишь зашифрованные датаграммы.

  • Свойства схемы проверяются семью тестами на Rust; для звонков есть простая проверка снифером.

Как запечатывается сообщение

Схема запечатывания сообщения

Схема запечатывания сообщения

Путь сообщения:

  1. Double Ratchet. Текст шифруется сессией переписки. У каждого диалога свои ключи, и они меняются с каждым сообщением — это даёт прямую секретность и восстановление после компрометации.

  2. Внутренний пакет. Зашифрованное сообщение упаковывается вместе с данными отправителя. Так отправитель оказывается внутри шифра (sealed sender), а не в заголовке.

  3. Печать получателю. Отправитель генерирует эфемерную пару X25519 (своя для каждого сообщения), считает DH с долговременным X25519-ключом получателя и параллельно выполняет Encaps ML-KEM-1024 на его Kyber-ключ. Конкатенация ss1 || ss2 идёт в HKDF-SHA256 с меткой домена, первые 32 байта выхода — ключ AES-256-GCM.

  4. Выравнивание размера. Результат дополняется до фиксированной «корзины», чтобы длина не выдавала содержимое.

  5. Доставка. Конверт кладётся в ящик, адрес которого вычисляют только двое собеседников из своего общего секрета. Адреса меняются каждую неделю.

Для вскрытия нужны оба приватных ключа получателя — X25519 и Kyber. Они генерируются на устройстве и наружу не передаются, так что дамп базы релея или доступ к хостингу открыть конверт не помогают.

Печать из теста-доказательства (та же конструкция, что в createSealedSenderHybridPQ протокольного слоя):

fn seal(to: &Recipient, plaintext: &[u8]) -> Sealed {
    let ephemeral_private = random::<32>();
    let ephemeral_public = x25519_public_key(ephemeral_private.clone()).unwrap();
    let ss1 = x25519_shared_secret(ephemeral_private, to.x25519_public.clone()).unwrap();
    let enc = mlkem1024_encapsulate(to.kyber_public.clone()).unwrap();
    let iv = random::<12>();
    let box_ = aes256_gcm_encrypt(key(ss1, enc.shared_secret), iv.clone(), plaintext.to_vec(), vec![]).unwrap();
    Sealed { ephemeral_public, kyber_ciphertext: enc.ciphertext, iv, box_ }
}

Почему гибрид, а не только Kyber? ML-KEM стандартизирован недавно (FIPS 203), и классический X25519 страхует от возможной ошибки в новой схеме. А Kyber страхует X25519 от квантового компьютера. Ключ получается из обоих секретов, поэтому печать держится, пока жив хотя бы один из алгоритмов.

Что видит сервер

Здесь я специально показываю и неудобное. Доказательство, которое скрывает то, что сервер всё-таки видит, — это реклама.

Данные

Видит ли сервер

Содержимое (текст, медиа, голос)

нет: AEAD-шифротекст

Отправитель

нет: идентичность лежит внутри печати

Получатель

нет: виден лишь rendezvous-идентификатор, выводимый из общего секрета пары и ротируемый по эпохам (неделя)

Длина

частично: только номер размерного бакета

Время

да: момент записи и выборки

Сетевой адрес клиента

да при прямом подключении; при включённом Tor — адрес выходного узла

Хранимое сообщение — это адрес ящика, непрозрачный шифротекст, тип и отметки времени. Поля отправителя, получателя или идентификатора аккаунта у него нет.

Тесты-доказательства

В репозитории лежит core/tests/proof.rs. Он воспроизводит печать и проверяет каждое свойство отдельным тестом. Главный — постквантовая гарантия:

#[test]
fn breaking_only_x25519_is_not_enough() {
    let bob = new_recipient();
    let eve = new_recipient();
    let sealed = seal(&bob, MESSAGE);
    assert_eq!(open(&bob.x25519_private, &eve.kyber_private, &sealed), None);
}

Тест моделирует атакующего, у которого есть ключ X25519 получателя (например, после взлома квантовым компьютером), но нет ключа Kyber. Сообщение не открывается. Остальные тесты проверяют, что получатель читает сообщение, шифротекст не содержит текста, чужие ключи не подходят, взлома одного Kyber тоже недостаточно, изменение хоть одного бита отклоняется, а одинаковый текст дважды даёт несвязанные шифротексты.

Проверить можно за минуту, нужен только Rust:

git clone https://github.com/Vahe327/UMBERA
cd UMBERA/core
cargo test --test proof
running 7 tests
test the_recipient_can_read_the_message ... ok
test the_ciphertext_does_not_contain_the_message ... ok
test anyone_with_other_keys_cannot_read_it ... ok
test breaking_only_x25519_is_not_enough ... ok
test breaking_only_kyber_is_not_enough ... ok
test a_tampered_message_is_rejected ... ok
test every_message_is_sealed_with_fresh_keys ... ok

test result: ok. 7 passed; 0 failed

Кроме них, в ядре 23 теста с эталонными векторами для каждого примитива и сквозные проверки ML-KEM-1024 и ML-DSA-65. Свои шифры мы не пишем: примитивы взяты из RustCrypto, dalek-cryptography и OpenMLS.

Звонки и защита DTLS-отпечатков

Схема шифрования звонка

Схема шифрования звонка

Медиа идёт по WebRTC: ключи SRTP выводятся из DTLS-рукопожатия, которое выполняется между двумя конечными устройствами. При симметричном NAT трафик проксирует TURN, но он работает на транспортном уровне и в DTLS не участвует, поэтому мастер-секрета SRTP у него нет.

Слабое место у такой схемы одно: подмена отпечатков DTLS при установке звонка, то есть атака «человек посередине». Сервер сигнализации мог бы подставить свои отпечатки и стоять между собеседниками. Поэтому описание звонка (SDP с отпечатками) шифруется той же схемой, что и сообщения, и подписывается звонящим:

val result = messageEncryption.encryptMessage(
    content = data,
    recipientId = peerUserId,
    recipientPublicKey = peerKeys.first,
    recipientX25519PublicKey = peerKeys.second,
    type = "signaling",
    sequenceNumber = 0
)

А на приёме чужая или изменённая подпись обрывает установку звонка:

if (!Secp256k1.verify(ciphertext, sig, senderPublicKey)) {
    throw SecurityException("Invalid message signature")
}

Групповые звонки построены как full mesh: каждый участник держит отдельную DTLS-SRTP-сессию с каждым. SFU или MCU, который мог бы декодировать поток, в схеме нет.

Проверка в Wireshark

  1. Раздайте интернет с ноутбука и снимайте трафик телефона на этом интерфейсе.

  2. Позвоните.

  3. Отфильтруйте захват выражением dtls || rtp || srtp.

  4. Ожидаемая картина: ClientHello/ServerHello DTLS, затем только SRTP; декодировать полезную нагрузку Wireshark не сможет, незашифрованного RTP в дампе нет.

Честные допущения

  • Конечная точка доверенная. Компрометация ОС устройства (малварь, доступ к экрану) выходит за рамки любой E2E-схемы.

  • Собеседник честен. Криптография не мешает получателю переслать расшифрованный текст.

  • Примитивы стойкие. Опираемся на стандарты NIST и IETF: FIPS 203, RFC 7748, RFC 5869, AES-GCM, RFC 5764.

  • Ключи важного контакта сверены. Прозрачность ключей (доказательства включения в дерево Меркла) и закрепление ключей предупредят, если ключи собеседника сменились.

Что опубликовано, а что нет

В репозитории: Rust-крейт с примитивами и MLS-движком (экспортируется в мобильные клиенты через UniFFI), протокольный код на Kotlin Multiplatform, общий для Android и iOS, и спецификация форматов. Сервер и интерфейс приложения в репозиторий не входят: сервер получает только то, что этот код уже зашифровал, поэтому для проверки утверждений он не нужен.

Лицензия — PolyForm Strict 1.0.0: код можно читать, собирать, запускать тесты и проверять, но это не open source, а source-available.

Попробуйте сломать

Если вы сможете опровергнуть хотя бы одно утверждение, я хочу узнать об этом первым — порядок описан в SECURITY.md репозитория. Буду рад разбору схемы в комментариях: особенно интересны замечания по комбинированию секретов в HKDF и по защите сигнализации звонка.

Код ядра, тесты и спецификация протокола: https://github.com/Vahe327/UMBERA

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.