Files
utun2/tests/test_reality_hello.c
T
evgeny 1340ab123b reality/stcp: fix OOB read in key_share parse, padding clamp, shbuf leak
- reality_parse_client_hello: bound shares_len by extension data length
  (len-2) to prevent stack-buffer-overflow from malformed ClientHello
- stcp handshake: reject padding_size > STCP_HS_PADDING_MAX (512)
- reality_ch_body_cb: free shbuf on stcp_try_send error (leak)
- tests: fuzz key_share parse; raw-socket handshake control/clamp/garbage

Signed-off-by: uTun <dev@utun>
2026-09-18 17:20:19 +03:00

339 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test_reality_hello.c — тесты модуля reality (REALITY-style ClientHello/ServerHello)
//
// Сценарии:
// 1. Round-trip: клиент собирает ClientHello → сервер проверяет авторизацию и
// собирает ServerHello; проверка структуры обоих (TLS record + handshake,
// echo SessionId).
// 2. Неверный short_id → REALITY_ERR_AUTH.
// 3. Неверный static privkey сервера → REALITY_ERR_AUTH (AES-GCM не сходится).
// 4. Порча байта ClientHello → REALITY_ERR_AUTH (AAD не совпадает).
// 5. Несовпадение версии протокола → REALITY_ERR_AUTH.
// 6. Timestamp вне окна (антиреплей) → REALITY_ERR_AUTH; в окне → OK.
// 7. Ошибки формата (мусор/пусто/неверный тип) → REALITY_ERR_FORMAT.
// 8. Fingerprint-геттер и keygen/pubkey_from_priv.
#include "reality.h"
#include "reality_fingerprint.h"
#include "config_parser.h"
#include "../lib/debug_config.h"
#include <stdio.h>
#include <string.h>
static int test_failed = 0;
#define CHECK(expr, msg) do { \
if (!(expr)) { \
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "FAIL: %s", msg); \
test_failed = 1; \
} else { \
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "PASS: %s", msg); \
} \
} while (0)
static uint8_t g_short_id[REALITY_SHORT_ID_SIZE] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
static uint8_t g_short_id_wrong[REALITY_SHORT_ID_SIZE] = { 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8 };
static uint8_t g_version[REALITY_VERSION_SIZE] = { 1, 2, 3 };
static uint8_t g_version_wrong[REALITY_VERSION_SIZE] = { 9, 9, 9 };
static void init_client_cfg(struct reality_client_config *cc,
const uint8_t *server_pub,
const uint8_t *short_id,
const uint8_t *version) {
memset(cc, 0, sizeof(*cc));
memcpy(cc->server_static_pubkey, server_pub, REALITY_AUTH_KEY_SIZE);
memcpy(cc->short_id, short_id, REALITY_SHORT_ID_SIZE);
memcpy(cc->version, version, REALITY_VERSION_SIZE);
snprintf(cc->server_name, sizeof(cc->server_name), "www.microsoft.com");
cc->fingerprint = REALITY_FP_CHROME;
}
static void init_server_cfg(struct reality_server_config *sc,
const uint8_t *server_priv,
const uint8_t *short_id,
const uint8_t *version,
int64_t window) {
memset(sc, 0, sizeof(*sc));
memcpy(sc->static_privkey, server_priv, REALITY_AUTH_KEY_SIZE);
memcpy(sc->short_ids[0], short_id, REALITY_SHORT_ID_SIZE);
sc->short_id_count = 1;
memcpy(sc->version, version, REALITY_VERSION_SIZE);
sc->time_window_sec = window;
sc->fingerprint = REALITY_FP_CHROME;
}
// Собирает структурно-валидный ClientHello (TLS record + handshake), тело которого
// содержит ровно одну extension-запись ext_block/ext_block_len. Для fuzz-проверок
// парсера (malformed key_share и т.п.).
static size_t build_ch_with_ext(uint8_t *out, size_t out_cap,
const uint8_t *ext_block, size_t ext_block_len) {
(void)out_cap;
uint8_t body[REALITY_MAX_CH_SIZE];
size_t b = 0;
body[b++] = 0x03; body[b++] = 0x03; // legacy_version
memset(body + b, 0x11, 32); b += 32; // random
body[b++] = 32; // session_id len
memset(body + b, 0x00, 32); b += 32; // session_id (нули)
body[b++] = 0x00; body[b++] = 0x02; // cipher_suites: 1 суит
body[b++] = 0x13; body[b++] = 0x01; // TLS_AES_128_GCM_SHA256
body[b++] = 0x01; body[b++] = 0x00; // compression (null)
body[b++] = (uint8_t)(ext_block_len >> 8); // extensions len
body[b++] = (uint8_t)(ext_block_len & 0xff);
memcpy(body + b, ext_block, ext_block_len); b += ext_block_len;
size_t body_len = b;
size_t hs_len = 4 + body_len;
out[0] = 0x16; out[1] = 0x03; out[2] = 0x01; // TLS record header
out[3] = (uint8_t)(hs_len >> 8); out[4] = (uint8_t)(hs_len & 0xff);
out[5] = 0x01; // handshake type = ClientHello
out[6] = (uint8_t)(body_len >> 16);
out[7] = (uint8_t)(body_len >> 8);
out[8] = (uint8_t)(body_len & 0xff);
memcpy(out + 9, body, body_len);
return 5 + hs_len;
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_REALITY);
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test ===");
uint8_t srv_priv[REALITY_AUTH_KEY_SIZE], srv_pub[REALITY_AUTH_KEY_SIZE];
CHECK(reality_generate_keypair(srv_priv, srv_pub) == REALITY_OK, "generate server keypair");
{
uint8_t pub2[REALITY_AUTH_KEY_SIZE];
CHECK(reality_pubkey_from_priv(srv_priv, pub2) == REALITY_OK, "pubkey_from_priv");
CHECK(memcmp(srv_pub, pub2, REALITY_AUTH_KEY_SIZE) == 0, "pubkey_from_priv matches");
}
uint8_t other_priv[REALITY_AUTH_KEY_SIZE], other_pub[REALITY_AUTH_KEY_SIZE];
CHECK(reality_generate_keypair(other_priv, other_pub) == REALITY_OK, "generate wrong keypair");
struct reality_client_config cc;
struct reality_server_config sc;
uint8_t ch[REALITY_MAX_CH_SIZE], sh[REALITY_MAX_SH_SIZE];
size_t ch_len = 0, sh_len = 0;
// ── Сценарий 1: round-trip ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build");
CHECK(ch_len > 5, "client hello non-trivial size");
CHECK(ch[0] == 0x16 && ch[1] == 0x03, "client hello TLS record header");
CHECK(ch[5] == 0x01, "client hello handshake type");
{
// проверка согласованности длины: record length == handshake length + 4
size_t rec_len = ((size_t)ch[3] << 8) | ch[4];
CHECK(rec_len + 5 == ch_len, "client hello record length matches");
}
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "server hello build (auth OK)");
CHECK(sh[0] == 0x16 && sh[5] == 0x02, "server hello record + handshake type");
{
// echo SessionId: client SessionId в ch[44..76], server echo в sh[44..76]
CHECK(memcmp(ch + 44, sh + 44, REALITY_SESSION_ID_SIZE) == 0, "server echoes client SessionId");
}
// ── Сценарий 2: неверный short_id ──
init_client_cfg(&cc, srv_pub, g_short_id_wrong, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong sid)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong short_id rejected");
// ── Сценарий 3: неверный static privkey сервера ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, other_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (wrong key)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "wrong server key rejected");
// ── Сценарий 4: порча байта ClientHello ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (tamper)");
ch[50] ^= 0x01; // портим байт внутри handshake-сообщения
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "tampered hello rejected");
// ── Сценарий 5: несовпадение версии ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version_wrong);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(&cc, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ver mismatch)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "version mismatch rejected");
// ── Сценарий 6: timestamp вне окна ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
{
uint32_t now = (uint32_t)time(NULL);
CHECK(reality_client_hello_build_at(&cc, now, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=now)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_OK, "ts=now accepted");
CHECK(reality_client_hello_build_at(&cc, now - 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=past)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=past rejected (anti-replay)");
CHECK(reality_client_hello_build_at(&cc, now + 3600, ch, sizeof(ch), &ch_len) == REALITY_OK, "client hello build (ts=future)");
CHECK(reality_server_hello_build(&sc, ch, ch_len, sh, sizeof(sh), &sh_len) == REALITY_ERR_AUTH, "ts=future rejected");
}
// ── Сценарий 7: ошибки формата ──
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
{
uint8_t garbage[REALITY_MAX_CH_SIZE];
memset(garbage, 0, sizeof(garbage));
CHECK(reality_server_hello_build(&sc, garbage, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "empty input rejected");
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "all-zero input rejected");
memset(garbage, 0, sizeof(garbage));
garbage[0] = 0x17; // не handshake-запись
CHECK(reality_server_hello_build(&sc, garbage, 100, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "wrong record type rejected");
// валидная запись, но не ClientHello
memset(garbage, 0, sizeof(garbage));
garbage[0] = 0x16; garbage[1] = 0x03; garbage[2] = 0x01;
garbage[3] = 0x00; garbage[4] = 0x04; // record length 4
garbage[5] = 0x02; // handshake type = server_hello
CHECK(reality_server_hello_build(&sc, garbage, 9, sh, sizeof(sh), &sh_len) == REALITY_ERR_FORMAT, "non-client-hello rejected");
}
// ── Сценарий 8: fingerprint-геттер ──
CHECK(reality_fingerprint_get(REALITY_FP_CHROME) != NULL, "fingerprint chrome found");
CHECK(reality_fingerprint_get(999) == NULL, "unknown fingerprint = NULL");
// ── Сценарий 9: невалидные аргументы ──
init_client_cfg(&cc, srv_pub, g_short_id, g_version);
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
CHECK(reality_client_hello_build(NULL, ch, sizeof(ch), &ch_len) == REALITY_ERR_ARG, "client build NULL cfg rejected");
CHECK(reality_client_hello_build(&cc, ch, 16, &ch_len) == REALITY_ERR_ARG, "client build small buffer rejected");
CHECK(reality_server_hello_build(&sc, NULL, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_ARG, "server build NULL input rejected");
// ── Сценарий 9b: malformed key_share (fuzz парсера, без OOB-read) ──
init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60);
{
uint8_t ch2[REALITY_MAX_CH_SIZE], sh2[REALITY_MAX_SH_SIZE];
size_t ch2_len = 0, sh2_len = 0;
uint8_t ks[10];
// shares_len (0xFFFF) не влезает в extension-данные (len=4) → FORMAT, без OOB
memset(ks, 0, sizeof(ks));
ks[0] = 0x00; ks[1] = 0x33; // type = key_share
ks[2] = 0x00; ks[3] = 0x04; // len = 4
ks[4] = 0xFF; ks[5] = 0xFF; // shares_len = 0xFFFF (завышен)
ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, 8);
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_FORMAT,
"key_share shares_len > ext len rejected");
// запись X25519 с klen=32, но без тела ключа → AUTH, без OOB
memset(ks, 0, sizeof(ks));
ks[0] = 0x00; ks[1] = 0x33; // type = key_share
ks[2] = 0x00; ks[3] = 0x06; // len = 6
ks[4] = 0x00; ks[5] = 0x04; // shares_len = 4 (= len-2)
ks[6] = 0x00; ks[7] = 0x1d; // group = X25519
ks[8] = 0x00; ks[9] = 0x20; // klen = 32 (тела нет)
ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks));
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH,
"key_share truncated X25519 entry → AUTH");
// цепочка записей не-X25519 с klen=0 (в границах) → AUTH, без OOB
memset(ks, 0, sizeof(ks));
ks[0] = 0x00; ks[1] = 0x33; // type = key_share
ks[2] = 0x00; ks[3] = 0x06; // len = 6
ks[4] = 0x00; ks[5] = 0x04; // shares_len = 4
ks[6] = 0x00; ks[7] = 0x17; // group = secp256r1
ks[8] = 0x00; ks[9] = 0x00; // klen = 0
ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks));
CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH,
"key_share without X25519 → AUTH");
}
// ── Сценарий 10: парсинг секции [reality] из конфига ──
{
const char *cfg_text =
"[global]\nname=test\n"
"[reality]\n"
"enabled=1\n"
"server_name=www.microsoft.com\n"
"dest=www.microsoft.com:443\n"
"short_id=0102030405060708\n"
"short_ids=0102030405060708,aabbccddeeff0011\n"
"public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"private_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"time_window=45\n"
"fingerprint=chrome\n";
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem");
CHECK(uc != NULL, "parse config with [reality]");
if (uc) {
CHECK(uc->global.reality.enabled == 1, "reality enabled parsed");
CHECK(strcmp(uc->global.reality.server_name, "www.microsoft.com") == 0, "server_name parsed");
CHECK(strcmp(uc->global.reality.dest, "www.microsoft.com:443") == 0, "dest parsed");
CHECK(uc->global.reality.has_public_key == 1 && uc->global.reality.has_private_key == 1, "pub/priv keys parsed");
CHECK(uc->global.reality.short_id_count == 2, "short_ids parsed");
CHECK(uc->global.reality.version[0] == 2 && uc->global.reality.version[1] == 3 && uc->global.reality.version[2] == 4, "version parsed");
CHECK(uc->global.reality.time_window_sec == 45, "time_window parsed");
CHECK(uc->global.reality.fingerprint == REALITY_FP_CHROME, "fingerprint parsed");
free_config(uc);
}
}
// ── Сценарий 11: парсинг reality-ключей в секции [client] ──
{
const char *cfg_text =
"[global]\nname=test\n"
"[server: bind_srv]\naddr=127.0.0.1:2000\ntransport=tcp\n"
"[client: c1]\n"
"peer_public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"link=1.2.3.4:1443\n"
"reality=yes\n"
"server_name=www.microsoft.com\n"
"short_id=0102030405060708\n"
"public_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"fingerprint=chrome\n"
"[client: c2]\n"
"peer_public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"link=bind_srv:5.6.7.8:1444\n"
"reality=yes\n"
"short_id=aabbccddeeff0011\n";
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem");
CHECK(uc != NULL, "parse config with [client] reality");
if (uc) {
/* parser prepends: c2 первая, c1 вторая */
struct CFG_CLIENT *c2 = uc->clients;
struct CFG_CLIENT *c1 = c2 ? c2->next : NULL;
CHECK(c2 && strcmp(c2->name, "c2") == 0, "client c2 present");
CHECK(c1 && strcmp(c1->name, "c1") == 0, "client c1 present");
if (c1) {
CHECK(c1->reality_enabled == 1, "c1 reality enabled");
CHECK(strcmp(c1->reality.server_name, "www.microsoft.com") == 0, "c1 server_name");
CHECK(c1->reality.fingerprint == REALITY_FP_CHROME, "c1 fingerprint");
CHECK(c1->reality.version[0] == 2 && c1->reality.version[1] == 3 && c1->reality.version[2] == 4, "c1 version parsed");
{ uint8_t sid[8] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
CHECK(memcmp(c1->reality.short_id, sid, 8) == 0, "c1 short_id"); }
{ uint8_t pk[32]; memset(pk, 0x22, 32);
CHECK(memcmp(c1->reality.server_static_pubkey, pk, 32) == 0, "c1 public_key"); }
CHECK(c1->links != NULL && c1->links->local_srv == NULL, "c1 link без bind (local_srv NULL)");
if (c1->links) {
struct sockaddr_in* sin = (struct sockaddr_in*)&c1->links->remote_addr;
CHECK(c1->links->remote_addr.ss_family == AF_INET && ntohs(sin->sin_port) == 1443, "c1 remote 1.2.3.4:1443");
}
}
if (c2) {
CHECK(c2->reality_enabled == 1, "c2 reality enabled");
CHECK(c2->reality.version[0] == 1 && c2->reality.version[1] == 0 && c2->reality.version[2] == 0, "c2 version default 1.0.0");
CHECK(c2->links != NULL && c2->links->local_srv != NULL, "c2 link с bind (local_srv set)");
if (c2->links && c2->links->local_srv)
CHECK(strcmp(c2->links->local_srv->name, "bind_srv") == 0, "c2 bind socket name");
}
free_config(uc);
}
}
if (test_failed) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: FAILED ===");
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: PASSED ===");
return 0;
}