083c441e4c
- docs/protocol.md, pki.md, split-tunnel.md, sing-box.md переведены на русский и сверены с текущим кодом (транспорт v2: свой UDP + TCP/443 + QUIC fallback, handover; PKI; split-tunnel; sing-box-план). - docs/deployment.md (новый, 369 строк): пошаговое руководство для удалённого сервера — сборка, PKI init/issue-server/issue-client (проверено бинарём), server.toml/client.toml на основе фактических config/*.example, firewall + NAT/IP-форвардинг, sudo-запуск, бандл клиента (ca.crt + client.crt + client.key + server addr/sni), на каком транспорте идёт трафик, ограничения v1. - README.md (новый, корень): краткий обзор + таблица крейтов + быстрый старт. Всё на русском (проза); команды/идентификаторы/конфиги — как есть. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
512 lines
37 KiB
Markdown
512 lines
37 KiB
Markdown
# Протокол Aura
|
||
|
||
Протокол Aura обеспечивает взаимно аутентифицированный, пост-квантово стойкий туннель между
|
||
клиентом и сервером. Он реализован в крейте `aura-proto` поверх `aura-crypto` (гибридный KEM, HKDF,
|
||
AEAD) и `aura-pki` (взаимная проверка X.509).
|
||
|
||
Этот документ предназначен для инженера, который проводит аудит протокола или реализует его заново.
|
||
Всё, что описано ниже, отражает **фактическую реализацию**, а не идеализированную спецификацию. Там,
|
||
где исходная спецификация была неоднозначной (особенно порядок сообщений рукопожатия), реализация
|
||
фиксирует конкретный выбор — и именно этот зафиксированный выбор здесь задокументирован.
|
||
|
||
---
|
||
|
||
## Транспорт v2: свой UDP-канал, TCP/443 и QUIC как fallback
|
||
|
||
Это ключевое изменение по сравнению с ранней версией: основной канал данных теперь — это **собственный
|
||
транспорт Aura поверх обычного UDP**, без QUIC и без внешнего TLS на основном пути. Единственная
|
||
граница безопасности — внутреннее рукопожатие Aura (гибридное X25519 + ML-KEM-768 со взаимной X.509),
|
||
которое само по себе уже пост-квантовое. QUIC сохраняется как fallback и средство камуфляжа.
|
||
|
||
Доступны три транспорта, все они выдают единый `Arc<dyn aura_proto::PacketConnection>`, поэтому
|
||
маршрутизатору туннеля безразлично, какой транспорт перенёс соединение
|
||
(`crates/aura-transport/src/dial.rs`):
|
||
|
||
| Режим (`TransportMode`) | Реализация | Роль |
|
||
|-------------------------|------------|------|
|
||
| `Udp` | `crates/aura-transport/src/udp.rs` | Свой протокол Aura поверх обычного UDP — **основной путь** |
|
||
| `Tcp` | `crates/aura-transport/src/tcp.rs` | Aura поверх TCP/443 (fallback для сетей, блокирующих UDP; опц. HTTP-маскировка) |
|
||
| `Quic` | `crates/aura-transport/src/lib.rs`, `conn.rs`, `quic.rs` | Aura внутри QUIC/HTTP3-мимикрии (fallback / сильный камуфляж) |
|
||
|
||
### Порядок переключения (handover)
|
||
|
||
Клиентский `dial` (`dial.rs`) пробует транспорты по списку `order` слева направо; первый, который
|
||
дозвонился, побеждает. По умолчанию `order = [Udp, Tcp, Quic]`. Транспорт без сконфигурированного
|
||
адреса пропускается; транспорт, который ошибся или вышел по таймауту (`attempt_timeout`, по умолчанию
|
||
8 с), уступает место следующему.
|
||
|
||
```
|
||
dial(order = [Udp, Tcp, Quic])
|
||
└─ Udp → UdpClient::connect ── ок? → соединение по UDP
|
||
│ ошибка/таймаут
|
||
└─ Tcp → TcpClient::connect ── ок? → соединение по TCP
|
||
│ ошибка/таймаут
|
||
└─ Quic → AuraClient::connect ── ок? → соединение по QUIC
|
||
│ все упали
|
||
Err(последняя ошибка)
|
||
```
|
||
|
||
Серверная сторона (`MultiServer`, `dial.rs`) поступает иначе: она привязывается и слушает **все**
|
||
включённые в `order` транспорты одновременно и выдаёт принятые соединения из любого из них через один
|
||
`MultiServer::accept`.
|
||
|
||
> **Важно про порты.** Свой UDP-транспорт и QUIC оба используют UDP, поэтому им нужны **разные**
|
||
> порты (`udp_port` ≠ `quic_port`). TCP может занимать тот же номер порта, что и UDP-транспорт (это
|
||
> другой протокол). См. `docs/deployment.md`.
|
||
|
||
### Свой транспорт поверх UDP (основной путь)
|
||
|
||
Один сокет `tokio::net::UdpSocket` несёт обе фазы, различаемые по первому байту-типу
|
||
(`crates/aura-transport/src/udp.rs`):
|
||
|
||
```text
|
||
0x01 HS : 0x01 || hs_seq(u16 BE) || ack_upto(u16 BE) || msg_bytes
|
||
0x02 DATA : 0x02 || rec_len(u16 BE) || sealed_record [|| random_padding]
|
||
```
|
||
|
||
* **Фаза рукопожатия (`0x01` HS).** Рукопожатие Aura написано поверх `AsyncRead` + `AsyncWrite` и
|
||
обменивается целыми кадрами. UDP теряет и переупорядочивает датаграммы, поэтому рукопожатие
|
||
выполняется через `ReliableHsAdapter` — небольшой слой надёжности в стиле «полётов» DTLS:
|
||
- **границы сообщений.** Записанные байты буферизуются; адаптер парсит 5-байтный заголовок кадра
|
||
Aura, чтобы узнать полный размер сообщения (`5 + len`), и эмитит ровно одну HS-датаграмму на
|
||
каждое целое сообщение (границы берутся из заголовка, а не из `flush`).
|
||
- **надёжность отправки.** Каждая отправленная HS-датаграмма хранится в упорядоченной карте по
|
||
`hs_seq`. Каждые `hs_rto` (по умолчанию 250 мс) все ещё не подтверждённые датаграммы
|
||
перепосылаются, пока их не подтвердят или пока не истечёт общий дедлайн `hs_timeout` (по
|
||
умолчанию 10 с → ошибка).
|
||
- **подтверждения (ack).** Каждая HS-датаграмма несёт `ack_upto` = наибольший **непрерывный**
|
||
полученный `hs_seq` (кумулятивный ack); значение-сентинел `ACK_NONE = 0xFFFF` означает «ничего не
|
||
получено». При приёме адаптер отбрасывает свои неподтверждённые записи с `hs_seq <= ack_upto`.
|
||
Если подтверждать есть что, а посылать нечего, эмитится «голая» HS-датаграмма с пустым телом.
|
||
- **упорядочивание приёма.** Полученные HS-полезные нагрузки буферизуются в карте по `hs_seq` и
|
||
выдаются читателю строго в непрерывном порядке; дубликаты отбрасываются.
|
||
- **linger.** После завершения рукопожатия короткая фаза дослки (`hs_linger`, по умолчанию 2 с)
|
||
повторно отправляет последний «полёт», чтобы потеря финального сообщения не сорвала установление.
|
||
* **Фаза данных (`0x02` DATA).** После рукопожатия берутся
|
||
`Session::into_datagram_parts`, и каждый прикладной пакет уходит как одна explicit-nonce
|
||
AEAD-запись в одной UDP-датаграмме — ненадёжно, ровно как сама сеть. Потери/переупорядочивание —
|
||
забота вызывающего (Aura туннелирует IP-пакеты, которые это терпят), а датаграммный кодек уже
|
||
проверяет replay. Для DATA `sealed_record` — это ровно `rec_len` байт (один вывод
|
||
`DatagramSender::seal`); любые хвостовые байты — это обфускационный паддинг, и приёмник их
|
||
игнорирует (читает ровно `rec_len`).
|
||
|
||
**Обфускация (`UdpOpts::obfuscate`).** Когда включена, каждая исходящая DATA-датаграмма дополняется
|
||
случайными байтами до следующей «корзины» размера из `padding::HTTPS_SIZE_BUCKETS`
|
||
(`[64, 128, 256, 512, 1024, 1280, 1460]`), чтобы размыть распределение размеров на проводе под
|
||
HTTPS-подобное. Приёмник читает ровно `rec_len` запечатанной записи и игнорирует паддинг.
|
||
|
||
**Один пир на принятое соединение (v1).** `UdpServer::accept` обслуживает **одного** клиента за
|
||
вызов: ждёт первую HS-датаграмму клиента, фиксирует адрес источника, проводит рукопожатие, привязанное
|
||
к этому адресу, и возвращает выделенное `UdpConnection`. Для обслуживания многих клиентов на одном
|
||
порту понадобился бы слой демультиплексирования по адресу источника — это вне рамок v1; пока для
|
||
нескольких клиентов предпочтительны TCP/QUIC.
|
||
|
||
### TCP/443 (fallback)
|
||
|
||
`crates/aura-transport/src/tcp.rs` гоняет **то же** рукопожатие Aura и `aura_proto::Session`
|
||
напрямую поверх `TcpStream` (он уже реализует `AsyncRead` + `AsyncWrite`). Никакой дополнительной
|
||
криптографии и никакого QUIC — граница безопасности по-прежнему внутреннее рукопожатие Aura.
|
||
|
||
**Опциональная HTTP-маскировка (`TcpOpts::masquerade`).** Перед рукопожатием стороны обмениваются
|
||
минимальной преамбулой HTTP/1.1 (клиент шлёт `GET / HTTP/1.1` с заголовком `Host`, сервер отвечает
|
||
`HTTP/1.1 200 OK`), так что начало соединения для поверхностного наблюдателя похоже на обычный HTTP.
|
||
Это **лёгкая маскировка, а не TLS** — полноценная HTTPS/TLS-443-мимикрия (переиспользование внешнего
|
||
слоя rustls из QUIC-бэкенда) запланирована; сейчас задача TCP — пропихнуть байты там, где UDP
|
||
заблокирован. Преамбула читается побайтно до терминатора `\r\n\r\n`, чтобы не залезть в поток
|
||
рукопожатия.
|
||
|
||
### QUIC (fallback / камуфляж)
|
||
|
||
QUIC-путь (`crates/aura-transport/`) переносит протокол Aura поверх настоящего QUIC и выдаёт
|
||
установленное соединение как `aura_proto::PacketConnection`. У него два слоя, и какой из них является
|
||
границей безопасности — ключевой момент дизайна (подробности — в разделе
|
||
[Слой мимикрии](#слой-мимикрии)):
|
||
|
||
- **Внешний QUIC/TLS** — это камуфляж под трафик браузера HTTP/3. Он **не** выполняет осмысленной
|
||
аутентификации.
|
||
- **Внутреннее рукопожатие/сессия Aura** — настоящая граница безопасности.
|
||
|
||
```
|
||
+-------------------------------------------------------------+
|
||
| Прикладные IP-пакеты (TUN) |
|
||
+-------------------------------------------------------------+
|
||
| Внутренняя сессия Aura: Frame -> AEAD-запись Data | <- настоящая граница безопасности
|
||
| Внутреннее рукопожатие Aura: гибридный KEM + взаимный X.509 |
|
||
+-------------------------------------------------------------+
|
||
| Несущий транспорт: |
|
||
| • свой UDP (без TLS) — основной |
|
||
| • TCP/443 (опц. HTTP-маскировка) — fallback |
|
||
| • QUIC/TLS (мимикрия под HTTP/3) — fallback / камуфляж |
|
||
+-------------------------------------------------------------+
|
||
```
|
||
|
||
Внутренний протокол транспортно-независим: `client_handshake` / `server_handshake` обобщены по
|
||
отдельным `tokio::io::AsyncRead`-читателю и `AsyncWrite`-писателю, поэтому один и тот же код одинаково
|
||
управляет надёжным UDP-адаптером, TCP-потоком, разрезанными `RecvStream` / `SendStream` от quinn
|
||
(QUIC) и in-memory дуплекс-каналом (тесты).
|
||
|
||
---
|
||
|
||
## Формат кадра
|
||
|
||
Каждое сообщение протокола Aura — это **5-байтный заголовок**, за которым следует полезная нагрузка
|
||
(`crates/aura-proto/src/frame.rs`):
|
||
|
||
```
|
||
байт 0 : msg_type (u8)
|
||
байты 1..4 : length (u24, big-endian) = длина полезной нагрузки в байтах
|
||
байт 4 : version = 0x01
|
||
байты 5.. : payload (length байт)
|
||
```
|
||
|
||
- `length` — это 24-битное целое big-endian, так что максимальная полезная нагрузка —
|
||
`0x00FF_FFFF` (16 МиБ − 1). Слишком большая нагрузка отвергается с `FrameTooLarge`.
|
||
- `version` равна `0x01`. Заголовок, у которого байт 4 не `0x01`, отвергается с `BadVersion`.
|
||
|
||
### Типы сообщений
|
||
|
||
| Байт | `MsgType` | Направление | Шифрование | Роль |
|
||
|--------|---------------|-------------|------------|--------------------------------------------|
|
||
| `0x01` | `ClientHello` | C→S | нет | Рукопожатие 1: гибридный публичный ключ + nonce |
|
||
| `0x02` | `ServerHello` | S→C | нет | Рукопожатие 2: гибридный шифртекст + nonce |
|
||
| `0x03` | `ClientAuth` | C→S | да | Рукопожатие 4: сертификат клиента + подпись |
|
||
| `0x04` | `ServerAuth` | S→C | да | Рукопожатие 3: сертификат сервера + подпись |
|
||
| `0x05` | `Finished` | оба | да | Рукопожатие 5/6: HMAC по transcript |
|
||
| `0x06` | `Data` | оба | да | Прикладная запись (AEAD-запечатанный `Frame`) |
|
||
| `0xFF` | `Alert` | оба | нет | Фатальный alert; байт 0 нагрузки — код |
|
||
|
||
> Замечание: числовые значения байтов **не** соответствуют порядку отправки. `ServerAuth` (`0x04`)
|
||
> отправляется *перед* `ClientAuth` (`0x03`). Порядок отправки задаётся конечным автоматом (ниже), а
|
||
> не байтом-типом.
|
||
|
||
### Прикладные кадры
|
||
|
||
Когда сессия установлена, прикладная нагрузка внутри каждой зашифрованной записи `Data` — это `Frame`
|
||
(`crates/aura-proto/src/frame.rs`). Все многобайтные целые — big-endian:
|
||
|
||
| Кадр | Тег | Кодирование |
|
||
|---------|--------|------------------------------------------------------|
|
||
| `Data` | `0x01` | `0x01 \|\| stream_id(u32) \|\| payload` |
|
||
| `Ping` | `0x02` | `0x02 \|\| seq(u32)` |
|
||
| `Pong` | `0x03` | `0x03 \|\| seq(u32)` |
|
||
| `Close` | `0x04` | `0x04 \|\| code(u8) \|\| reason_len(u32) \|\| reason_utf8` |
|
||
|
||
На всех трёх транспортах прикладные IP-пакеты упаковываются как `Frame::Data` на `stream_id 0`;
|
||
входящий `Ping` отвечается `Pong`, лишний `Pong` игнорируется, а `Close` всплывает как ошибка.
|
||
|
||
---
|
||
|
||
## Рукопожатие
|
||
|
||
### Зафиксированный порядок сообщений
|
||
|
||
Исходная диаграмма спецификации была неоднозначна насчёт порядка зашифрованных сообщений
|
||
auth/Finished. Реализация фиксирует ровно такой порядок, и обе стороны следуют ему шаг в шаг
|
||
(`crates/aura-proto/src/handshake.rs`):
|
||
|
||
```
|
||
1. C -> S ClientHello (открытый текст): x25519_pub[32] || mlkem_ek[1184] || client_nonce[32]
|
||
2. S -> C ServerHello (открытый текст): x25519_ephemeral[32] || mlkem_ct[1088] || server_nonce[32]
|
||
-- обе стороны выводят гибридный общий секрет и два направленных SessionKeys --
|
||
3. S -> C ServerAuth (зашифровано под s2c): u16(cert_der_len) || server_leaf_cert_der || sig(transcript)
|
||
4. C -> S ClientAuth (зашифровано под c2s): u16(cert_der_len) || client_leaf_cert_der || sig(transcript)
|
||
5. C -> S Finished (зашифровано под c2s): HMAC-SHA256(key_c2s, transcript)
|
||
6. S -> C Finished (зашифровано под s2c): HMAC-SHA256(key_s2c, transcript)
|
||
-- зашифрованный канал Data теперь открыт в обе стороны --
|
||
```
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Клиент
|
||
participant S as Сервер
|
||
Note over C,S: открытый текст
|
||
C->>S: 1. ClientHello (x25519_pub, mlkem_ek, client_nonce)
|
||
S->>C: 2. ServerHello (x25519_eph, mlkem_ct, server_nonce)
|
||
Note over C,S: обе стороны выводят общий секрет + SessionKeys<br/>transcript = SHA-256(CH_frame || SH_frame)
|
||
Note over C,S: зашифровано (AEAD под направленными ключами)
|
||
S->>C: 3. ServerAuth (сертификат сервера + подпись по transcript)
|
||
C->>S: 4. ClientAuth (сертификат клиента + подпись по transcript)
|
||
C->>S: 5. Finished (HMAC_c2s по transcript)
|
||
S->>C: 6. Finished (HMAC_s2c по transcript)
|
||
Note over C,S: сессия установлена; записи Data идут в обе стороны
|
||
```
|
||
|
||
### Полезные нагрузки Hello (точные размеры)
|
||
|
||
| Поле | ClientHello | ServerHello | Байт |
|
||
|-------------------|:-----------:|:-----------:|-----:|
|
||
| X25519 pub / eph | ✔ | ✔ | 32 |
|
||
| ML-KEM-768 ek | ✔ | | 1184 |
|
||
| ML-KEM-768 ct | | ✔ | 1088 |
|
||
| nonce | ✔ | ✔ | 32 |
|
||
| **Итого нагрузка**| **1248** | **1152** | |
|
||
|
||
Hello отправляются в открытом виде и при приёме проверяются на точную длину; неверная длина
|
||
отвергается с `MalformedHandshake`.
|
||
|
||
### Transcript-хеш
|
||
|
||
```
|
||
transcript = SHA-256( ClientHello_frame_bytes || ServerHello_frame_bytes )
|
||
```
|
||
|
||
Хеш покрывает **полные сериализованные кадры** (5-байтный заголовок + полезная нагрузка) ClientHello
|
||
и ServerHello, ровно как они передаются на проводе. Это привязывает согласованный ключевой материал и
|
||
версию протокола одновременно к подписям и к MAC-ам Finished.
|
||
|
||
### Аутентификация (ServerAuth / ClientAuth)
|
||
|
||
Каждая нагрузка Auth:
|
||
|
||
```
|
||
u16_be(cert_der_len) || leaf_cert_der || signature
|
||
```
|
||
|
||
- `leaf_cert_der` — это **листовой сертификат** отправителя в DER (передаётся встроенным, без цепочки —
|
||
CA является якорем доверия на принимающей стороне).
|
||
- `signature` — это подпись **ECDSA P-256 / SHA-256**, в кодировке ASN.1 DER
|
||
(`ECDSA_P256_SHA256_ASN1`), вычисленная по 32-байтному `transcript` (через `ring`).
|
||
|
||
Проверка (`crates/aura-proto/src/handshake.rs`):
|
||
|
||
1. Принимающая сторона строит `AuraCertVerifier` из настроенного PEM своего CA и проверяет листовой
|
||
сертификат пира против CA (цепочка + назначение ключа + срок действия; см. `pki.md`).
|
||
- **Клиент** дополнительно требует, чтобы листовой сертификат сервера был валиден для ожидаемого
|
||
`server_name` (совпадение DNS SAN).
|
||
- **Сервер** захватывает проверенный **id клиента** (Common Name листа) и сохраняет его как
|
||
`peer_id` сессии.
|
||
2. Принимающая сторона извлекает точку EC-публичного ключа из листа и проверяет `signature` по
|
||
`transcript`. Неуспех — это `Signature(...)`.
|
||
|
||
Таким образом, владение приватным ключом сертификата доказывается подписью по transcript, а личность
|
||
сертификата — проверкой цепочки против CA.
|
||
|
||
### Finished
|
||
|
||
Каждая сторона отправляет, затем проверяет, MAC Finished, привязанный к transcript и ключу
|
||
направления:
|
||
|
||
```
|
||
Finished_c2s = HMAC-SHA256(key_c2s, transcript) // отправляет клиент (сообщение 5), проверяет сервер
|
||
Finished_s2c = HMAC-SHA256(key_s2c, transcript) // отправляет сервер (сообщение 6), проверяет клиент
|
||
```
|
||
|
||
Проверка выполняется за постоянное время (`Hmac::verify_slice`); несовпадение — это
|
||
`FinishedMismatch`. Обмен Finished подтверждает, что обе стороны вывели одинаковые ключи и согласны по
|
||
всему transcript.
|
||
|
||
### Зашифрованные сообщения рукопожатия и непрерывность счётчиков
|
||
|
||
Сообщения 3–6 запечатываются AEAD под **теми же** двумя направленными `AeadSession`, что защищают
|
||
прикладные Data; их счётчики nonce непрерывны через границу рукопожатие/данные.
|
||
|
||
- AAD каждого зашифрованного сообщения рукопожатия — это его 5-байтный заголовок кадра (привязка типа
|
||
+ длины), как и в записях Data.
|
||
- Каждое направление запечатывает **ровно два** зашифрованных сообщения рукопожатия до начала Data:
|
||
- c2s запечатывает `ClientAuth` (счётчик 0) и `Finished` (счётчик 1)
|
||
- s2c запечатывает `ServerAuth` (счётчик 0) и `Finished` (счётчик 1)
|
||
- Поэтому оба направления достигают AEAD-счётчика **2** в конце рукопожатия, и первая прикладная
|
||
запись Data ставит `seq == 2` (`POST_HANDSHAKE_COUNTER`). Это задаёт начальное состояние
|
||
replay-окна (ниже). На датаграммном (UDP) пути это же значение счётчика переносится в
|
||
explicit-nonce кодеки через `into_datagram_parts`, так что nonce, уже использованные в рукопожатии,
|
||
не переиспользуются.
|
||
|
||
---
|
||
|
||
## Гибридный KEM
|
||
|
||
Обмен ключами — гибрид классического X25519 ECDH и пост-квантового ML-KEM-768
|
||
(`crates/aura-crypto/src/kem/`). Атакующему нужно сломать **оба** примитива, чтобы восстановить ключ
|
||
сессии.
|
||
|
||
> **ML-KEM-768 (FIPS 203)** через крейт RustCrypto `ml-kem` (v0.3) — это стандартизованная схема
|
||
> FIPS 203, а **не** Kyber раунда 3.
|
||
|
||
### Роли
|
||
|
||
- **Клиент** владеет долговременным `HybridPrivateKey` и публикует свой `HybridPublicKey` в
|
||
ClientHello.
|
||
- **Сервер** вызывает `encapsulate()` против этого публичного ключа: он генерирует **эфемерную** пару
|
||
X25519 и ML-KEM-инкапсуляцию, возвращает `HybridCiphertext` в ServerHello и выводит общий секрет.
|
||
- **Клиент** восстанавливает тот же секрет через `decapsulate()`.
|
||
|
||
То есть X25519 здесь **эфемерно–статический** (эфемерный сервера против статического публичного
|
||
клиента), а ML-KEM — это стандартный KEM против инкапсуляционного ключа клиента.
|
||
|
||
### Размеры
|
||
|
||
| Величина | Байт | Константа |
|
||
|-----------------------------------|------:|---------------------|
|
||
| X25519 публичный / эфемерный / секрет | 32 | `X25519_LEN` |
|
||
| ML-KEM-768 инкапсуляционный ключ (ek) | 1184 | `EK_LEN` |
|
||
| ML-KEM-768 шифртекст (ct) | 1088 | `CT_LEN` |
|
||
| ML-KEM-768 общий секрет | 32 | `SS_LEN` |
|
||
| ML-KEM-768 деинкапсуляционный ключ (dk) | 2400 | `DK_LEN` |
|
||
|
||
> **Деталь реализации — кодирование dk.** Деинкапсуляционный (секретный) ключ хранится в
|
||
> **развёрнутой 2400-байтной** форме FIPS 203 (`ExpandedKeyEncoding`), а не в 64-байтном seed,
|
||
> который предпочитает `ml-kem` 0.3. Именно с этим кодированием работают KAT-векторы ACVP / FIPS 203
|
||
> проекта, поэтому оно используется для совместимости/interop. dk никогда не путешествует по проводу —
|
||
> по нему идут только `ek` (1184 Б) и `ct` (1088 Б).
|
||
|
||
### Объединённый общий секрет
|
||
|
||
```
|
||
shared = x25519_ss (32 Б) || mlkem_ss (32 Б) // всего 64 байта
|
||
```
|
||
|
||
Деинкапсуляция ML-KEM не может завершиться ошибкой на корректно размерном шифртексте: подделанный
|
||
шифртекст даёт псевдослучайный секрет (неявное отклонение), а не ошибку, что позже всплывает как сбой
|
||
AEAD/Finished.
|
||
|
||
---
|
||
|
||
## Вывод ключей (HKDF)
|
||
|
||
Направленные ключи сессии выводятся с помощью **HKDF-SHA256** (RFC 5869)
|
||
(`crates/aura-crypto/src/kdf.rs`):
|
||
|
||
```
|
||
salt = client_nonce || server_nonce (64 байта)
|
||
IKM = x25519_ss || mlkem_ss (64 байта)
|
||
info = "aura-v1-session"
|
||
OKM = HKDF-Expand(HKDF-Extract(salt, IKM), info, 64) (64 байта)
|
||
|
||
key_client_to_server = OKM[0..32]
|
||
key_server_to_client = OKM[32..64]
|
||
```
|
||
|
||
Вывод полностью детерминирован по своим входам. Строка `info` обеспечивает доменное разделение.
|
||
Промежуточный секретный материал (`salt`, `IKM`, `OKM`) обнуляется после использования, а
|
||
`SessionKeys` обнуляет свои ключи при drop.
|
||
|
||
---
|
||
|
||
## AEAD
|
||
|
||
Шифр записей — **ChaCha20-Poly1305** (`crates/aura-crypto/src/aead.rs`). Есть два режима:
|
||
|
||
- `AeadSession` — для **потоковых** транспортов (TCP, QUIC, поток рукопожатия): держит 256-битный ключ
|
||
и 64-битный счётчик сообщений; nonce выводится из счётчика, который продвигается шаг в шаг на каждом
|
||
`seal` и `open`, поэтому стороны остаются синхронными без передачи nonce.
|
||
- `AeadKey` — для **датаграммного** (UDP) пути: nonce-счётчик передаётся аргументом на каждый вызов,
|
||
потому что датаграммы могут теряться или переупорядочиваться, так что счётчик каждой записи несётся
|
||
на проводе. Схема nonce идентична `AeadSession`, поэтому оба совместимы на одном ключе, пока их
|
||
диапазоны счётчиков не пересекаются.
|
||
|
||
### Схема nonce
|
||
|
||
96-битный (12-байтный) nonce выводится из счётчика:
|
||
|
||
```
|
||
nonce[0..8] = counter как little-endian u64
|
||
nonce[8..12] = 0x00 00 00 00
|
||
```
|
||
|
||
В потоковом режиме nonce никогда не переиспользуется в рамках сессии (переполнение счётчика 2^64
|
||
недостижимо; при переполнении происходит паника, а не повторное использование nonce). В датаграммном
|
||
режиме за уникальность счётчика отвечает отправитель (`DatagramSender` монотонно увеличивает `seq`).
|
||
Ключ обнуляется при drop.
|
||
|
||
---
|
||
|
||
## Записи данных и защита от повтора (replay)
|
||
|
||
После рукопожатия прикладные `Frame` передаются как записи `Data`
|
||
(`crates/aura-proto/src/session.rs`). Кодирование зависит от того, потоковый транспорт или
|
||
датаграммный — но логика replay-окна общая.
|
||
|
||
### Потоковый путь (TCP / QUIC)
|
||
|
||
**Полезная нагрузка** каждой записи `Data`:
|
||
|
||
```
|
||
seq (u64, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = header || seq )
|
||
```
|
||
|
||
- `seq` — 8-байтный big-endian счётчик записи. На «счастливом пути» он равен счётчику запечатывающего
|
||
AEAD (и ожидаемому счётчику AEAD приёмника).
|
||
- **AAD** AEAD — это 5-байтный заголовок кадра `header`, сцепленный с 8-байтным `seq`, так что запись
|
||
криптографически привязана и к своей объявленной длине/типу, и к заявленной позиции.
|
||
- Шифртекст включает 16-байтный тег Poly1305.
|
||
|
||
Полная запись на проводе:
|
||
|
||
```
|
||
[ header(5) ][ seq(8) ][ ciphertext + tag ]
|
||
\_____________________________________________/
|
||
header.length = 8 + len(ciphertext+tag)
|
||
```
|
||
|
||
### Датаграммный путь (свой UDP)
|
||
|
||
Здесь нет 5-байтного потокового заголовка внутри записи. Датаграммная запись
|
||
(`DatagramSender::seal`):
|
||
|
||
```
|
||
seq(8, big-endian) || ChaCha20Poly1305_seal( frame_bytes, aad = seq )
|
||
```
|
||
|
||
То есть AAD — это **только** `seq` (а не `header || seq`). На проводе эта запись несётся внутри
|
||
DATA-датаграммы UDP-транспорта как `0x02 || rec_len(u16 BE) || запись [|| паддинг]` (см. раздел про
|
||
транспорт v2 выше).
|
||
|
||
### Скользящее replay-окно
|
||
|
||
Приёмник запускает проверку повтора **скользящим окном шириной 64** (`REPLAY_WINDOW = 64`) *до*
|
||
обращения к AEAD, так что дубликат или слишком старая запись отвергаются с `Replay(seq)`, не трогая
|
||
счётчик AEAD (сессия остаётся работоспособной). Окно:
|
||
|
||
- отслеживает наибольший принятый `seq` плюс 64-битную битовую карту принятых позиций ниже него;
|
||
- принимает `seq`, только если он строго новее всего виденного, либо попадает в окно и ранее не был
|
||
виден;
|
||
- отвергает `seq`, который равен текущему наибольшему, уже отмечен в битовой карте или находится более
|
||
чем на `REPLAY_WINDOW` ниже наибольшего.
|
||
|
||
Окно инициализируется на пост-рукопожатном счётчике (`start = 2`): всё строго ниже `start` считается
|
||
уже потреблённым, поэтому первая легитимная запись Data (`seq == 2`) принимается как «новейшая». Этот
|
||
механизм работает одинаково на потоковом и датаграммном путях.
|
||
|
||
### Полнодуплексное разделение
|
||
|
||
`Session` можно `split()` на независимые половины `SessionSender` (писатель + исходящий AEAD +
|
||
счётчик отправки) и `SessionReceiver` (читатель + входящий AEAD + replay-окно), которыми можно
|
||
управлять из разных задач для конкурентного чтения/записи (например, в VPN-туннеле). `recv_frame`
|
||
**не** безопасен к отмене (cancellation-safe) и должен выполняться из одной владеющей задачи.
|
||
Для датаграммного пути аналогично есть `into_datagram_parts`, выдающий `DatagramSender` /
|
||
`DatagramReceiver`.
|
||
|
||
---
|
||
|
||
## Слой мимикрии
|
||
|
||
Внешний слой QUIC/TLS (`crates/aura-transport/`) существует исключительно для маскировки соединения
|
||
под трафик браузера HTTP/3. Он явно **не** является границей аутентификации.
|
||
|
||
- **ALPN** объявляет `h3` и `h3-29` (`ALPN_H3`) — ровно то, что предлагает Chrome для HTTP/3 —
|
||
поэтому расширение ALPN неотличимо от реального браузерного.
|
||
- **Параметры транспорта** зеркалят соединение Chromium HTTP/3: ~30 с idle-таймаут, ~15 с
|
||
keep-alive, 100 конкурентных bidi/uni-потоков, ~10 МБ окна управления потоком на приём
|
||
(`chrome_quic_transport_config`).
|
||
- **SNI** по умолчанию — обобщённое CDN-подобное имя (`cdn.example.com`), если вызывающий его не задал;
|
||
развёртывания передают своё камуфляжное имя.
|
||
- QUIC-**клиент принимает любой серверный сертификат** (`AcceptAnyServerCert` — все методы
|
||
верификатора возвращают успех). Это безопасно *только* потому, что внешний TLS не является
|
||
аутентификацией: настоящая взаимная аутентификация — это внутреннее рукопожатие Aura. Внешний TLS
|
||
сервера также отключает клиентскую аутентификацию (`with_no_client_auth`).
|
||
|
||
> Не переиспользуйте `AcceptAnyServerCert` нигде, где слой TLS *является* границей аутентификации.
|
||
|
||
Лёгкая HTTP-маскировка TCP-транспорта (`TcpOpts::masquerade`) преследует ту же цель, но гораздо
|
||
скромнее (см. раздел про транспорт v2): это преамбула HTTP/1.1, а не TLS.
|
||
|
||
---
|
||
|
||
## Модель ошибок
|
||
|
||
Слой протокола выдаёт `ProtoError` (`crates/aura-proto/src/lib.rs`), в том числе:
|
||
`Io`, `Crypto`, `Pki`, `UnknownMsgType`, `BadVersion`, `FrameTooLarge`, `UnexpectedMsg`,
|
||
`MalformedHandshake`, `MalformedFrame`, `Signature`, `FinishedMismatch`, `Replay` и `Alert`. Пир может
|
||
отправить фатальный кадр `Alert` (тип `0xFF`); первый байт полезной нагрузки — код alert, который
|
||
всплывает на локальной стороне как `ProtoError::Alert(code)`.
|