Align crypto documentation with random CCM nonces and public-key node IDs

This commit is contained in:
evgeny committed 2026-10-24 00:35:13 +02:00
1 parent 2715faffff
commit 3afd31e561
3 files changed
+42 -24

No files matched your search

+8 -3
View File
@@ -21,11 +21,15 @@
etcp->normalizer = pn_init(etcp);
etcp->send_input_q = etcp->normalizer->input; // единая точка входа для отправки
// Отправка пакета (пример из тестов):
// Отправка кодограммы cmd || payload через публичный API:
struct ll_entry* entry = ll_alloc_lldgram(len);
if (!entry) return -1;
memcpy(entry->dgram, data, len);
entry->len = len;
queue_data_put(pn->input, entry);
if (etcp_send(etcp, entry) < 0) {
queue_dgram_free(entry); queue_entry_free(entry);
return -1;
}
// Приём (через callback на pn->output):
// queue_set_callback(pn->output, etcp_int_recv, etcp);
@@ -75,7 +79,8 @@ Normalizer не удерживает отдельную OOM-очередь ил
**`pn_unpacker_reset_state(pn)`** — сбрасывает только unpacker (recvpart).
**`pn_packer_send(pn, data, len)`** — копирует данные в `ll_alloc_lldgram` и кладёт в `pn->input`. Используется только в юнит-тестах.
`pn_packer_send` — вспомогательная функция тестов из .c, в API заголовка не объявлена.
Сервисы отправляют кодограммы через `etcp_send`, учитывая UP, backpressure и владение entry.
### Внутренние callback-и
+5 -3
View File
@@ -53,7 +53,7 @@ struct SC_MYKEYS {
// Контекст защищенного канала
struct secure_channel {
struct SC_MYKEYS* pk;
struct SC_MYKEYS* pk; // заимствованная локальная пара до конца использования ctx
/* Ключ пира (после key exchange) */
uint8_t peer_public_key[SC_PUBKEY_SIZE];
uint8_t session_key[SC_SESSION_KEY_SIZE]; /* Derived session key */
@@ -61,8 +61,8 @@ struct secure_channel {
uint8_t initialized;
uint8_t peer_key_set;
uint8_t session_ready;
uint64_t tx_counter;
uint64_t rx_counter;
uint64_t tx_counter; // успешные encrypt; nonce создаётся независимо через CSPRNG
uint64_t rx_counter; // успешные decrypt после проверки CCM tag и CRC
};
// Функции инициализации
@@ -126,6 +126,8 @@ sc_status_t sc_ed25519_verify(const uint8_t pubkey[32], const uint8_t* msg, size
sc_status_t sc_derive_ed25519_pubkey(const uint8_t *x25519_privkey, uint8_t *ed25519_pubkey_out);
/* SHA256(public_key[32]): первые 8 байт memcpy в uint64_t хоста, старший бит обнуляется.
* public_key — бинарный X25519-ключ; при NULL логирует ошибку и возвращает 0. */
uint64_t sc_derive_node_id_from_pubkey(const uint8_t *public_key);
int sc_hex_to_binary(const char *hex_str, uint8_t *binary, size_t binary_len);
+29 -18
View File
@@ -4,7 +4,8 @@
Модуль реализует криптографический слой uTun: защищённый канал между двумя узлами на основе **X25519** (Elliptic Curve Diffie-Hellman) для обмена сессионным ключом и **AES-128-CCM** (Counter with CBC-MAC) для аутентифицированного шифрования трафика. Дополнительно предоставляет Ed25519 для цифровых подписей (аутентификация данных, происходящих от конкретного узла) и стриминговое шифрование (AES-128-CTR) для больших потоков данных, где нужна только конфиденциальность без аутентификации.
Это **один из важнейших модулей** проекта — каждый ETCP-пакет, проходящий между узлами, шифруется и аутентифицируется через этот канал.
UDP ETCP использует пакетное AES-128-CCM; STCP использует отдельные AES-128-CTR API
этого модуля и CRC кадров. Сетевые handshake и подтверждение эпох выполняются выше, в transport_layer.
**Ключевые криптографические операции:**
1. **X25519 ECDH** — получение общего секрета из своего приватного и чужого публичного ключа
@@ -43,18 +44,25 @@ sc_set_peer_public_key(&ctx, peer_pubkey, SC_PEER_PUBKEY_BIN); // или SC_PEE
// Теперь ctx.session_ready == 1
// ── Шаг 4: Шифрование / дешифрование ──
uint8_t ciphertext[1500];
size_t ct_len = sizeof(ciphertext);
if (!plaintext_len) return SC_ERR_INVALID_ARG;
uint8_t ciphertext[plaintext_len + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE];
size_t ct_len = 0;
// Шифрование: nonce(13) || AES-CCM(plaintext+crc32) || tag(16)
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ct_len);
int status = sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ct_len);
if (status != SC_OK) return status;
// Дешифрование: проверка тега (аутентификация) → проверка CRC32 → возврат plaintext
uint8_t decrypted[1500];
size_t pt_len = sizeof(decrypted);
sc_decrypt(&ctx, ciphertext, ct_len, decrypted, &pt_len);
uint8_t decrypted[plaintext_len];
size_t pt_len = 0;
status = sc_decrypt(&ctx, ciphertext, ct_len, decrypted, &pt_len);
if (status != SC_OK) return status;
```
ct_len/pt_len — выходные длины, не capacity буферов. Для encrypt требуется
plaintext_len + 33 байта, для decrypt — ciphertext_len - 33. ctx заимствует my_keys;
на каждый передаваемый CCM-пакет выбирается новый 13-байтный nonce через OpenSSL RAND_bytes.
### 2.2. Обфускация pubkey (INIT-пакеты)
При отправке INIT-пакета публичный ключ не передаётся в открытом виде. Вместо этого он обфусцируется через XOR с SHA256-хэшем от соли и публичного ключа пира:
@@ -80,7 +88,8 @@ sc_obfuscate_pubkey(salt, my_pubkey, received_obfuscated, decrypted_pubkey);
// XOR обратим: obfuscated XOR SHA256(salt||my_pubkey) = исходный pubkey отправителя
```
**Важный нюанс реализации:** в текущей версии `sc_obfuscate_pubkey()` вычисляет два SHA256-хэша (`SHA256(salt||peer_pubkey)` и `SHA256(peer_pubkey||salt)`), но XOR'ит публичный ключ только с первым. Второй хэш вычисляется, но не используется (возможно, задел на будущее).
В реализации `sc_obfuscate_pubkey()` вычисляются два SHA256-хэша, но 32-байтная
маска использует только первый, `SHA256(salt||peer_pubkey)`. Второй хэш на результат не влияет.
### 2.3. Стриминговое шифрование (AES-128-CTR)
@@ -155,7 +164,7 @@ sc_sha_transcode(key, key_len, data, data_len);
| `SC_PRIVKEY_SIZE` | 32 | Размер приватного ключа X25519 (байт) |
| `SC_PUBKEY_SIZE` | 32 | Размер публичного ключа X25519 (байт) |
| `SC_HASH_SIZE` | 32 | Размер SHA256-хэша (байт) |
| `SC_NONCE_SIZE` | **13** | Размер nonce для AES-128-CCM. **Ровно 13 байт — требование CCM!** |
| `SC_NONCE_SIZE` | **13** | Фиксированный размер nonce пакетного протокола этого модуля |
| `SC_SHARED_SECRET_SIZE` | 32 | Размер общего секрета X25519 (= SC_HASH_SIZE) |
| `SC_SESSION_KEY_SIZE` | 16 | Размер сессионного ключа AES-128 (байт) |
| `SC_TAG_SIZE` | 16 | Размер аутентификационного тега CCM (байт) |
@@ -180,14 +189,14 @@ sc_sha_transcode(key, key_len, data, data_len);
- `uint8_t public_key[32]` — публичный ключ X25519
**`struct secure_channel` (sc_context_t)** — контекст защищённого канала между двумя конкретными узлами:
- `pk` — указатель на `SC_MYKEYS` (локальные ключи)
- `pk` — заимствованный указатель на `SC_MYKEYS` до конца использования контекста
- `peer_public_key[32]` — публичный ключ пира (заполняется после ECDH)
- `session_key[16]` — производный сессионный ключ AES-128
- `initialized` — флаг: `sc_init_ctx()` вызван
- `peer_key_set` — флаг: `sc_set_peer_public_key()` вызван успешно
- `session_ready` — флаг: можно шифровать/дешифровать
- `tx_counter` — счётчик отправленных пакетов (используется в nonce)
- `rx_counter` — счётчик принятых пакетов
- `tx_counter` — счётчик успешных encrypt; генерация nonce от него не зависит
- `rx_counter` — счётчик успешных decrypt после проверки CCM tag и CRC
**`struct sc_stream_state`** — состояние стримингового шифра (AES-128-CTR):
- `ectx` — `EVP_CIPHER_CTX*` (opaque)
@@ -205,7 +214,7 @@ sc_sha_transcode(key, key_len, data, data_len);
| Функция | Описание |
|---|---|
| `sc_init_ctx(ctx, mykeys)` | Инициализирует контекст канала, привязывает локальные ключи. **Не делает ECDH** — только обнуляет состояние. |
| `sc_init_ctx(ctx, mykeys)` | Заимствует локальные ключи, инициализирует флаги и счётчики; ECDH выполняется при установке ключа пира. |
| `sc_generate_keypair(keys)` | Генерирует новую пару X25519 через `EVP_PKEY_keygen`. Случайный приватный ключ. |
| `sc_init_local_keys(mykeys, pub_hex, priv_hex)` | Загружает ключи из hex-строк (обычно из конфига). Валидирует публичный ключ (не all-zero). |
| `sc_set_peer_public_key(ctx, peer_pubkey, mode)` | **Ключевая операция:** выполняет X25519 ECDH, выводит session_key, устанавливает `session_ready=1`. mode: `SC_PEER_PUBKEY_BIN` или `SC_PEER_PUBKEY_HEX`. |
@@ -220,7 +229,9 @@ sc_sha_transcode(key, key_len, data, data_len);
| `sc_encrypt(ctx, plaintext, pt_len, ciphertext, &ct_len)` | AES-128-CCM шифрование. Формат вывода: `nonce(13) \|\| AES-CCM(plaintext + CRC32(plaintext)) \|\| tag(16)`. Инкрементирует `tx_counter`. |
| `sc_decrypt(ctx, ciphertext, ct_len, plaintext, &pt_len)` | AES-128-CCM дешифрование. Проверяет CCM-тег → проверяет CRC32. Инкрементирует `rx_counter` при успехе. |
**Формат nonce:** `SHA256(random_seed(8) || tx_counter(8) || tv_sec(4) || tv_usec(4))` — truncated to 13 bytes. Использует OpenSSL SHA256 (не sha256.h).
**Nonce:** 13 случайных байт из OpenSSL `RAND_bytes` на каждый encrypt.
При отказе RNG возвращается `SC_ERR_CRYPTO`, выходная длина остаётся нулевой.
Nonce не зависит от часов, счётчиков контекста или параллельных экземпляров ядра.
**Проверки при дешифровании:**
1. `ciphertext_len >= SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE` (минимум 13+16+4 = 33 байта)
@@ -255,7 +266,7 @@ sc_sha_transcode(key, key_len, data, data_len);
| `sc_stream_sign_init(ctx, state)` | Инициализирует подпись. Извлекает Ed25519-ключ из X25519 через `SHA512(x25519_privkey)`. |
| `sc_stream_sign_verify_init(state, ed25519_pubkey)` | Инициализирует верификацию с переданным Ed25519 pubkey. |
| `sc_stream_sign_update(state, data, len)` | SHA-512 update (инкрементальный). Нулевая длина — no-op. |
| `sc_stream_sign_final(state, sig_out, &sig_len)` | Завершает SHA-512 → one-shot Ed25519 подпись хэша → **автоматический cleanup**. `sig_len` всегда 64. |
| `sc_stream_sign_final(state, sig_out, &sig_len)` | Завершает SHA-512 → one-shot Ed25519 подпись хэша → **автоматический cleanup**. При успехе `sig_len=64`. |
| `sc_stream_sign_verify(state, sig, sig_len)` | Завершает SHA-512 → one-shot Ed25519 проверка → **автоматический cleanup**. Возвращает `SC_OK` или `SC_ERR_AUTH_FAILED`. |
| `sc_stream_sign_cleanup(state)` | Ручная очистка (если не вызван final/verify). |
@@ -266,7 +277,7 @@ sc_sha_transcode(key, key_len, data, data_len);
| Функция | Описание |
|---|---|
| `sc_derive_ed25519_pubkey(x25519_privkey, ed25519_pubkey_out)` | Из X25519-приватного ключа (32 байта) выводит Ed25519-публичный ключ (32 байта) через `SHA512(x25519_privkey)`. |
| `sc_derive_node_id(private_key)` | `SHA256(private_key)` → первые 8 байт как uint64_t, старший бит обнулён. Уникальный идентификатор узла. |
| `sc_derive_node_id_from_pubkey(public_key)` | SHA256 бинарного X25519 pubkey[32] → первые 8 байт memcpy в uint64_t хоста, старший бит обнулён. При NULL возвращает 0 и логирует ошибку. |
#### Утилиты
@@ -282,7 +293,7 @@ sc_sha_transcode(key, key_len, data, data_len);
| **OpenSSL SHA** (`openssl/sha.h`) | `SHA256_Init/Update/Final` для построения nonce в `sc_build_nonce()` |
| **`../lib/sha256.h`** (`sc_sha256_*`) | SHA256 для вывода session_key, stream nonce, obfuscate pubkey, sha_transcode, node_id |
| **`../lib/debug_config.h`** | `DEBUG_ERROR`, `DEBUG_WARN`, `DEBUG_INFO`, `DEBUG_DEBUG` (категория `DEBUG_CATEGORY_CRYPTO`) |
| **`../lib/platform_compat.h`** | `random_bytes()` (для соли и random seed), `utun_gettimeofday()` (для nonce) |
| **`../lib/platform_compat.h`** | Общие платформенные определения; CCM nonce генерируется OpenSSL RAND_bytes |
| **`crc32.h`** (`crc32_calc`) | Контроль целостности в шифрованных пакетах |
### 3.5. Отсутствующие функции
@@ -300,7 +311,7 @@ sc_sha_transcode(key, key_len, data, data_len);
└──────────────┴─────────────────────────────────────┴──────────────┘
```
- **nonce:** 13 байт, уникальный для каждого пакета (SHA256 от seed + counter + времени)
- **nonce:** 13 случайных байт из OpenSSL RAND_bytes для каждого пакета
- **ciphertext:** AES-128-CCM in-place шифрование `plaintext || CRC32(plaintext)`
- **tag:** 16 байт, аутентификационный тег CCM (обеспечивает integrity + authenticity)