You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
19 KiB
338 lines
19 KiB
// 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; |
|
}
|
|
|