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.
752 lines
34 KiB
752 lines
34 KiB
// test_chat_join.c — компонентные + интеграционные тесты join-протокола. |
|
// |
|
// Задача 1 (компонентные): криптография дерева приглашений (build_sign_msg / sign_pubkey / |
|
// verify_signature), сериализация мембера, chat_join_process_request (инвайтер), хранилище |
|
// ключей (register_key self / lookup_inviter), member_sync_validate_pubkey. |
|
// |
|
// Задача 2 (интеграция): два инстанса A(инвайтер) и C(connection) — регистрация join-ключа |
|
// через etcp_router, после сходимости BGP lookup_inviter(C) == A.node_id. |
|
// |
|
// chat_core/chat_sync (глобальные синглтоны) НЕ используются: конфиг без секции [chatserver], |
|
// member_sync/chat_join инициализируются вручную. |
|
|
|
#include "chat_join.h" |
|
#include "../routing_layer/etcp_router.h" |
|
#include "../routing_layer/route_crypto.h" |
|
#include "../transport_layer/etcp_api.h" |
|
#include "member_sync.h" |
|
#include "../routing_layer/topo_node_sqlite.h" |
|
#include "../utun_instance.h" |
|
#include "../transport_layer/etcp.h" |
|
#include "../transport_layer/secure_channel.h" |
|
#include "../config_parser.h" |
|
#include "../config_updater.h" |
|
#include "../routing_layer/topo_group.h" |
|
#include "../routing_layer/topo_node.h" |
|
#include "../lib/u_async.h" |
|
#include "../lib/debug_config.h" |
|
#include "../lib/mem.h" |
|
#include "test_utils.h" |
|
|
|
#include <sqlite3.h> |
|
#define OPENSSL_API_COMPAT 0x10100000L |
|
#include <openssl/evp.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdarg.h> |
|
#include <time.h> |
|
#include <unistd.h> |
|
|
|
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; |
|
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0) |
|
#define OK() do { G_PASSED++; printf("OK\n"); } while(0) |
|
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) |
|
|
|
#define POLL_MS 5 |
|
#define CH_COMP "42424242424" |
|
#define CH_INT "53535353535" |
|
#define JOIN_KEY 0x0BADCAFEF00DF00DULL |
|
#define MAX_NODES 2 |
|
|
|
static struct UASYNC* g_ua = NULL; |
|
static struct UTUN_INSTANCE* g_inst[MAX_NODES]; |
|
static uint64_t g_nid[MAX_NODES]; |
|
static char g_tdir[256] = "/tmp/utun_cj_XXXXXX"; |
|
static char g_cfg[MAX_NODES][256]; |
|
static char g_db_dir[MAX_NODES][320]; |
|
static int g_port[MAX_NODES]; |
|
static uint64_t g_group_id = 0; |
|
|
|
/* ── крипто-хелперы ── */ |
|
|
|
static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) { |
|
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); |
|
EVP_PKEY* pkey = NULL; |
|
EVP_PKEY_keygen_init(ctx); |
|
EVP_PKEY_keygen(ctx, &pkey); |
|
EVP_PKEY_CTX_free(ctx); |
|
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); |
|
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); |
|
EVP_PKEY_free(pkey); |
|
} |
|
|
|
static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) { |
|
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); |
|
EVP_PKEY* pkey = NULL; |
|
EVP_PKEY_keygen_init(ctx); |
|
EVP_PKEY_keygen(ctx, &pkey); |
|
EVP_PKEY_CTX_free(ctx); |
|
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); |
|
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); |
|
EVP_PKEY_free(pkey); |
|
} |
|
|
|
/* ed25519 из x25519: priv = SHA512(x_priv)[0:32], pub = sc_derive_ed25519_pubkey(x_priv) */ |
|
static void x25519_to_ed25519(const uint8_t* x_priv, uint8_t ed_priv[32], uint8_t ed_pub[32]) { |
|
EVP_MD_CTX* c = EVP_MD_CTX_new(); |
|
uint8_t h[64]; unsigned int hl = 0; |
|
EVP_DigestInit_ex(c, EVP_sha512(), NULL); |
|
EVP_DigestUpdate(c, x_priv, 32); |
|
EVP_DigestFinal_ex(c, h, &hl); |
|
EVP_MD_CTX_free(c); |
|
memcpy(ed_priv, h, 32); |
|
sc_derive_ed25519_pubkey(x_priv, ed_pub); |
|
} |
|
|
|
/* ── DB-хелперы ── */ |
|
|
|
static sqlite3* open_db(void) { |
|
sqlite3* db = NULL; |
|
sqlite3_open(":memory:", &db); |
|
topo_node_sqlite_init(db); |
|
return db; |
|
} |
|
|
|
static void make_channel(sqlite3* db, const char* ch_id, |
|
uint8_t ch_x_pub[32], uint8_t ch_x_priv[32], |
|
uint8_t ch_ed_pub[32], uint8_t ch_ed_priv[32]) { |
|
gen_x25519(ch_x_pub, ch_x_priv); |
|
gen_ed25519(ch_ed_pub, ch_ed_priv); |
|
uint8_t ch_sig[64]; |
|
sc_ed25519_sign(ch_ed_priv, (const uint8_t*)ch_id, strlen(ch_id), ch_sig); |
|
topo_node_sqlite_channel_put(db, ch_id, "test", 0, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv, ch_sig); |
|
} |
|
|
|
/* ── джойнер: ключи + join_sig + сериализованный member-блок ── */ |
|
|
|
struct joiner { |
|
uint8_t x_pub[32], x_priv[32], ed_pub[32], ed_priv[32]; |
|
uint64_t node_id; |
|
uint8_t join_sig[64]; |
|
uint64_t join_ts; |
|
}; |
|
|
|
static void make_joiner(struct joiner* j, const uint8_t ch_x_pub[32], const uint8_t ch_ed_pub[32]) { |
|
gen_x25519(j->x_pub, j->x_priv); |
|
x25519_to_ed25519(j->x_priv, j->ed_priv, j->ed_pub); |
|
j->node_id = sc_derive_node_id_from_pubkey(j->x_pub); |
|
j->join_ts = (uint64_t)time(NULL); |
|
uint8_t jm[128]; |
|
int jl = member_sync_build_join_msg(ch_x_pub, ch_ed_pub, j->node_id, j->x_pub, j->join_ts, |
|
jm, (int)sizeof(jm)); |
|
sc_ed25519_sign(j->ed_priv, jm, (size_t)jl, j->join_sig); |
|
} |
|
|
|
static int serialize_member(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, |
|
const uint8_t* join_sig, uint64_t join_ts, |
|
uint8_t* buf, size_t sz, size_t* len) { |
|
struct join_member_data m; |
|
memset(&m, 0, sizeof(m)); |
|
m.node_id = node_id; m.x25519 = x25519; m.ed25519 = ed25519; |
|
m.join_sig = join_sig; m.join_ts = join_ts; |
|
snprintf(m.userinfo, sizeof(m.userinfo), "{\"name\":\"J\"}"); |
|
return chat_join_serialize_member(&m, buf, sz, len); |
|
} |
|
|
|
/* ── 5.1.1 build_sign_msg ── */ |
|
|
|
static void t_build_sign_msg(void) { |
|
TEST("member_sync_build_sign_msg: 42 bytes"); { |
|
uint8_t x[32], priv[32], out[64]; gen_x25519(x, priv); |
|
int n = member_sync_build_sign_msg(x, out, (int)sizeof(out)); |
|
if (n == MEMBER_SIGN_MSG_SIZE && memcmp(out, x, 32) == 0 |
|
&& memcmp(out + 32, MEMBER_SIGN_SUFFIX, MEMBER_SIGN_SUFFIX_LEN) == 0) OK(); |
|
else FAIL("n=%d", n); |
|
} |
|
TEST("member_sync_build_sign_msg: short buffer -> -1"); { |
|
uint8_t x[32], priv[32], out[41]; gen_x25519(x, priv); |
|
if (member_sync_build_sign_msg(x, out, (int)sizeof(out)) == -1) OK(); else FAIL(); |
|
} |
|
} |
|
|
|
/* ── 5.1.2 sign_pubkey / verify_signature ── */ |
|
|
|
static void t_sign_verify(void) { |
|
sqlite3* db = open_db(); |
|
uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; |
|
make_channel(db, CH_COMP, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); |
|
(void)ch_x_priv; |
|
|
|
/* joiner (подписываемый pubkey) и «другой» pubkey для подмены */ |
|
uint8_t jx[32], jpriv[32]; gen_x25519(jx, jpriv); |
|
uint8_t other_x[32], other_priv[32]; gen_x25519(other_x, other_priv); |
|
(void)jpriv; (void)other_priv; |
|
|
|
/* родитель (для ребёнка) */ |
|
uint8_t p_x_pub[32], p_x_priv[32], p_ed_pub[32], p_ed_priv[32]; |
|
gen_x25519(p_x_pub, p_x_priv); |
|
x25519_to_ed25519(p_x_priv, p_ed_priv, p_ed_pub); |
|
uint64_t parent_id = sc_derive_node_id_from_pubkey(p_x_pub); |
|
topo_node_sqlite_node_update_verified(db, parent_id, "parent", p_x_pub, p_ed_pub, |
|
(uint64_t)time(NULL), time(NULL)); |
|
|
|
struct ms_member_rec rec; |
|
uint8_t sig[64]; |
|
|
|
TEST("verify_signature: root (signed_by=0, channel ed25519) == 1"); { |
|
member_sync_sign_pubkey(ch_ed_priv, jx, sig); |
|
memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0; |
|
if (member_sync_verify_signature(db, CH_COMP, &rec) == 1) OK(); else FAIL(); |
|
} |
|
|
|
TEST("verify_signature: child (signed_by=parent) == 1"); { |
|
member_sync_sign_pubkey(p_ed_priv, jx, sig); |
|
memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = parent_id; |
|
if (member_sync_verify_signature(db, CH_COMP, &rec) == 1) OK(); else FAIL(); |
|
} |
|
|
|
TEST("verify_signature: tampered x25519 == 0"); { |
|
member_sync_sign_pubkey(ch_ed_priv, jx, sig); |
|
memset(&rec, 0, sizeof(rec)); rec.x25519 = other_x; rec.signature = sig; rec.signed_by = 0; |
|
if (member_sync_verify_signature(db, CH_COMP, &rec) == 0) OK(); else FAIL(); |
|
} |
|
|
|
TEST("verify_signature: parent not in nodes == -1"); { |
|
member_sync_sign_pubkey(ch_ed_priv, jx, sig); |
|
memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0x7777ULL; |
|
if (member_sync_verify_signature(db, CH_COMP, &rec) == -1) OK(); else FAIL(); |
|
} |
|
|
|
TEST("verify_signature: no channel == -1"); { |
|
member_sync_sign_pubkey(ch_ed_priv, jx, sig); |
|
memset(&rec, 0, sizeof(rec)); rec.x25519 = jx; rec.signature = sig; rec.signed_by = 0; |
|
if (member_sync_verify_signature(db, "99999999999", &rec) == -1) OK(); else FAIL(); |
|
} |
|
|
|
sqlite3_close(db); |
|
} |
|
|
|
/* ── 5.1.3 serialize_member / parse_member ── */ |
|
|
|
static void t_serialize_parse(void) { |
|
uint8_t x[32], xp[32], ed[32], edp[32], jsig[64], usig[64]; |
|
gen_x25519(x, xp); gen_ed25519(ed, edp); |
|
memset(jsig, 0x11, 64); memset(usig, 0x22, 64); |
|
|
|
struct join_member_data m; |
|
memset(&m, 0, sizeof(m)); |
|
m.node_id = 0x1234567890abcdefULL; m.x25519 = x; m.ed25519 = ed; |
|
m.join_sig = jsig; m.join_ts = 1234567ULL; |
|
m.update_sig = usig; m.update_ts = 7654321ULL; |
|
strcpy(m.userinfo, "{\"name\":\"J\"}"); |
|
|
|
uint8_t buf[512]; size_t len = 0; |
|
|
|
TEST("serialize_member + parse_member roundtrip"); { |
|
struct join_member_data p; memset(&p, 0, sizeof(p)); |
|
if (chat_join_serialize_member(&m, buf, sizeof(buf), &len) != 0 |
|
|| chat_join_parse_member(buf, len, &p) != 0 |
|
|| p.node_id != m.node_id |
|
|| memcmp(p.x25519, x, 32) || memcmp(p.ed25519, ed, 32) |
|
|| memcmp(p.join_sig, jsig, 64) || p.join_ts != m.join_ts |
|
|| memcmp(p.update_sig, usig, 64) || p.update_ts != m.update_ts |
|
|| strcmp(p.userinfo, m.userinfo) != 0) FAIL(); |
|
else OK(); |
|
} |
|
|
|
TEST("serialize_member: small buffer -> -1"); { |
|
if (chat_join_serialize_member(&m, buf, 10, &len) == -1) OK(); else FAIL(); |
|
} |
|
|
|
TEST("parse_member: short input -> -1"); { |
|
struct join_member_data p; |
|
if (chat_join_parse_member(buf, 10, &p) == -1) OK(); else FAIL(); |
|
} |
|
|
|
TEST("serialize_member: userinfo up to 255"); { |
|
char ui[256]; memset(ui, 'a', 255); ui[255] = '\0'; |
|
struct join_member_data ml = m; ml.userinfo[0] = '\0'; |
|
memcpy(ml.userinfo, ui, 256); |
|
uint8_t b2[512]; size_t l2 = 0; |
|
struct join_member_data p; memset(&p, 0, sizeof(p)); |
|
if (chat_join_serialize_member(&ml, b2, sizeof(b2), &l2) != 0 |
|
|| chat_join_parse_member(b2, l2, &p) != 0 |
|
|| strlen(p.userinfo) != 255 || memcmp(p.userinfo, ui, 255) != 0) FAIL(); |
|
else OK(); |
|
} |
|
} |
|
|
|
/* ── config/instance хелперы ── */ |
|
|
|
static int wf(const char* p, const char* f, ...) { |
|
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; |
|
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; |
|
} |
|
static char* gv(const char* p, const char* k) { |
|
struct utun_config* c = parse_config(p); if (!c) return NULL; |
|
char* r = (strcmp(k, "pub") == 0) ? u_strdup(c->global.my_public_key_hex) : u_strdup(c->global.my_private_key_hex); |
|
free_config(c); return r; |
|
} |
|
static void t_rmdir(const char* p) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", p); system(cmd); } |
|
|
|
/* пишет конфиг n0 (component) — без клиентов */ |
|
static void write_comp_config(void) { |
|
snprintf(g_cfg[0], sizeof(g_cfg[0]), "%s/comp.conf", g_tdir); |
|
snprintf(g_db_dir[0], sizeof(g_db_dir[0]), "%s/dbcomp", g_tdir); |
|
utun_mkdir(g_db_dir[0], 0755); |
|
g_port[0] = 54000 + (getpid() % 4000); |
|
wf(g_cfg[0], "[global]\ntun_ip=10.98.70.1/24\ntun_ifname=tun70\ndb_path=%s\n" |
|
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", |
|
g_db_dir[0], g_port[0]); |
|
config_ensure_keys_and_node_id(g_cfg[0]); |
|
struct utun_config* c = parse_config(g_cfg[0]); g_nid[0] = c->global.my_node_id; free_config(c); |
|
} |
|
|
|
/* пишет конфиги a.conf (инвайтер, сервер) и c.conf (connection, клиент → a) */ |
|
static void write_int_configs(void) { |
|
for (int i = 0; i < 2; i++) { |
|
snprintf(g_cfg[i], sizeof(g_cfg[i]), "%s/%s.conf", g_tdir, i ? "c" : "a"); |
|
snprintf(g_db_dir[i], sizeof(g_db_dir[i]), "%s/db%s", g_tdir, i ? "c" : "a"); |
|
utun_mkdir(g_db_dir[i], 0755); |
|
g_port[i] = 55000 + (getpid() % 3000) + i * 1000; |
|
} |
|
for (int i = 0; i < 2; i++) { |
|
wf(g_cfg[i], "[global]\ntun_ip=10.98.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s\n" |
|
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", |
|
i, i, g_db_dir[i], g_port[i]); |
|
config_ensure_keys_and_node_id(g_cfg[i]); |
|
struct utun_config* c = parse_config(g_cfg[i]); g_nid[i] = c->global.my_node_id; free_config(c); |
|
} |
|
/* c (i=1) — клиент к a (i=0) */ |
|
{ |
|
char* pr = gv(g_cfg[1], "priv"); char* pu = gv(g_cfg[1], "pub"); |
|
char* p0_pu = gv(g_cfg[0], "pub"); |
|
wf(g_cfg[1], "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.98.1.1/24\ntun_ifname=tun10\n" |
|
"db_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n" |
|
"[client: to_a]\nkeepalive=1\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n[allowed_keys]\nallow_all=1\n", |
|
pr, pu, g_db_dir[1], g_port[1], p0_pu, g_port[0]); |
|
u_free(pr); u_free(pu); u_free(p0_pu); |
|
} |
|
} |
|
|
|
static int create_instances(int n) { |
|
for (int i = 0; i < n; i++) { |
|
g_inst[i] = utun_instance_create(g_ua, g_cfg[i]); |
|
if (!g_inst[i]) { printf("FAIL: create n%d\n", i); return -1; } |
|
utun_instance_init(g_inst[i]); |
|
member_sync_init(g_inst[i]); |
|
} |
|
return 0; |
|
} |
|
|
|
static void destroy_instances(int n) { |
|
for (int i = 0; i < n; i++) { |
|
if (g_inst[i]) { |
|
chat_join_destroy(g_inst[i]); |
|
member_sync_destroy(g_inst[i]); |
|
g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; |
|
} |
|
} |
|
} |
|
|
|
static int wait_connected(int n) { |
|
if (n <= 1) return 1; |
|
int need = n * (n - 1); |
|
for (int a = 0; a < 2000; a++) { |
|
int ok = 0; |
|
for (int i = 0; i < n; i++) { |
|
struct ll_entry* e = g_inst[i]->connections->head; |
|
while (e) { |
|
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; |
|
if (ce->conn && ce->conn->links) { |
|
struct ETCP_LINK* l; |
|
for (l = ce->conn->links; l; l = l->next) |
|
if (l->initialized && ce->conn->crypto_ctx.initialized) ok++; |
|
} |
|
e = e->next; |
|
} |
|
} |
|
if (ok >= need) return 1; |
|
uasync_poll(g_ua, POLL_MS); |
|
} |
|
return 0; |
|
} |
|
|
|
static int wait_chat_bgp(int n) { |
|
for (int a = 0; a < 8000; a++) { |
|
int ok = 1; |
|
for (int i = 0; i < n && ok; i++) { |
|
struct TOPO_GROUP* g = topo_groups_find(g_inst[i]->topo_groups, g_group_id); |
|
if (!g) { ok = 0; break; } |
|
for (int m = 0; m < n; m++) { |
|
if (m == i) continue; |
|
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, g_nid[m]); |
|
if (!nq || !nq->paths || !nq->paths->head) { ok = 0; break; } |
|
} |
|
} |
|
if (ok) return 1; |
|
uasync_poll(g_ua, POLL_MS); |
|
} |
|
return 0; |
|
} |
|
|
|
/* ── 5.1.4 chat_join_process_request (инвайтер) ── */ |
|
|
|
static void t_process_request(void) { |
|
struct UTUN_INSTANCE* inst = g_inst[0]; |
|
sqlite3* db = inst->topo_sqlite_db; |
|
|
|
uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; |
|
make_channel(db, CH_COMP, ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); |
|
(void)ch_x_priv; (void)ch_ed_priv; |
|
/* инвайтер (self) в nodes — чтобы verify_signature резолвил signed_by */ |
|
topo_node_sqlite_node_update_verified(db, inst->node_id, "inviter", |
|
inst->my_keys.public_key, inst->my_ed25519_pubkey, (uint64_t)time(NULL), time(NULL)); |
|
|
|
uint64_t group_id = strtoull(CH_COMP, NULL, 10); |
|
|
|
/* 0. неизвестный join-ключ → отклонён (ключ ещё не зарегистрирован) */ |
|
TEST("process_request: unknown join_key rejected"); { |
|
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); |
|
uint8_t buf[512]; size_t len = 0; |
|
serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); |
|
chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); |
|
if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); |
|
} |
|
|
|
/* регистрируем ключ локально — дальше process_request его принимает */ |
|
chat_join_store_local_key(inst, group_id, JOIN_KEY); |
|
|
|
/* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */ |
|
TEST("process_request: valid join_sig accepted"); { |
|
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); |
|
uint8_t buf[512]; size_t len = 0; |
|
serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); |
|
chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); |
|
|
|
uint64_t sb = 0; uint8_t sig[64]; memset(sig, 0, sizeof(sig)); |
|
int in_ch = topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id); |
|
int gs = topo_node_sqlite_member_get_sign(db, CH_COMP, j.node_id, &sb, sig); |
|
struct ms_member_rec rec; memset(&rec, 0, sizeof(rec)); |
|
rec.x25519 = j.x_pub; rec.signature = sig; rec.signed_by = sb; |
|
int vs = (gs == 0) ? member_sync_verify_signature(db, CH_COMP, &rec) : -99; |
|
|
|
if (in_ch == 1 && gs == 0 && sb == inst->node_id && vs == 1) OK(); |
|
else FAIL("in_ch=%d gs=%d sb=%016llx vs=%d", in_ch, gs, (unsigned long long)sb, vs); |
|
} |
|
|
|
/* 2. невалидный join_sig → отклонён */ |
|
TEST("process_request: invalid join_sig rejected"); { |
|
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); |
|
uint8_t bad_sig[64]; memcpy(bad_sig, j.join_sig, 64); bad_sig[0] ^= 0xFF; |
|
uint8_t buf[512]; size_t len = 0; |
|
serialize_member(j.node_id, j.x_pub, j.ed_pub, bad_sig, j.join_ts, buf, sizeof(buf), &len); |
|
chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); |
|
if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); |
|
} |
|
|
|
/* 3. node_id != derive(x25519) → отклонён */ |
|
TEST("process_request: node_id mismatch rejected"); { |
|
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); |
|
uint8_t buf[512]; size_t len = 0; |
|
serialize_member(j.node_id ^ 0x12345ULL, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); |
|
chat_join_process_request(inst, group_id, j.node_id, JOIN_KEY, buf, len); |
|
if (!topo_node_sqlite_member_in_channel(db, CH_COMP, j.node_id)) OK(); else FAIL(); |
|
} |
|
} |
|
|
|
/* ── 5.1.5 chat_join_register_key(self) / lookup_inviter ── */ |
|
|
|
static void t_register_key_self(void) { |
|
struct UTUN_INSTANCE* inst = g_inst[0]; |
|
uint64_t group_id = strtoull(CH_COMP, NULL, 10); |
|
|
|
TEST("register_key(target=self) -> lookup_inviter == self"); { |
|
chat_join_store_local_key(inst, group_id, JOIN_KEY); |
|
uint64_t inv = chat_join_lookup_inviter(inst, group_id, JOIN_KEY); |
|
if (inv == inst->node_id) OK(); else FAIL("inv=%016llx", (unsigned long long)inv); |
|
} |
|
TEST("lookup_inviter: unknown key -> 0"); { |
|
if (chat_join_lookup_inviter(inst, group_id, JOIN_KEY + 1) == 0) OK(); else FAIL(); |
|
} |
|
} |
|
|
|
/* ── 5.1.6 member_sync_validate_pubkey ── */ |
|
|
|
static void t_validate_pubkey(void) { |
|
struct UTUN_INSTANCE* inst = g_inst[0]; |
|
sqlite3* db = inst->topo_sqlite_db; |
|
|
|
uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; |
|
make_channel(db, "61616161616", ch_x_pub, ch_x_priv, ch_ed_pub, ch_ed_priv); |
|
(void)ch_x_priv; (void)ch_ed_priv; |
|
|
|
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub); |
|
uint64_t gid = strtoull("61616161616", NULL, 10); |
|
uint8_t buf[512]; size_t len = 0; |
|
serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); |
|
chat_join_store_local_key(inst, gid, JOIN_KEY); |
|
chat_join_process_request(inst, gid, j.node_id, JOIN_KEY, buf, len); |
|
|
|
TEST("validate_pubkey: member -> 1"); { |
|
if (member_sync_validate_pubkey(inst, "61616161616", j.x_pub) == 1) OK(); else FAIL(); |
|
} |
|
TEST("validate_pubkey: non-member -> 0"); { |
|
uint8_t ox[32], op[32]; gen_x25519(ox, op); (void)op; |
|
if (member_sync_validate_pubkey(inst, "61616161616", ox) == 0) OK(); else FAIL(); |
|
} |
|
} |
|
|
|
struct registration_result { int calls, result; }; |
|
static void registered(void* arg, int result) { |
|
struct registration_result* r = arg; |
|
r->calls++; r->result = result; |
|
} |
|
|
|
static void poll_ms(int ms) { |
|
uint64_t until = get_time_tb() + (uint64_t)ms * 10; |
|
while (get_time_tb() < until) uasync_poll(g_ua, POLL_MS); |
|
} |
|
|
|
static int wait_registered(struct registration_result* r) { |
|
uint64_t until = get_time_tb() + JOIN_REGISTER_TIMEOUT_TB + 10000; |
|
while (!r->calls && get_time_tb() < until) uasync_poll(g_ua, POLL_MS); |
|
return r->calls == 1 && r->result == CHAT_JOIN_OK; |
|
} |
|
|
|
static etcp_recv_fn original_join_recv; |
|
static struct ll_entry* held_ack; |
|
static struct ETCP_CONN* held_conn; |
|
static void hold_ack(struct ETCP_CONN* conn, struct ll_entry* entry) { |
|
if (entry->len > ROUTER_SVC_PAYLOAD_OFF && entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == JOIN_SUBCMD_KEY_REGISTER_ACK) { |
|
if (held_ack) { queue_dgram_free(held_ack); queue_entry_free(held_ack); etcp_conn_ref_free(held_conn); } |
|
if (etcp_conn_ref_take(conn) != 0) abort(); |
|
held_ack = entry; held_conn = conn; |
|
} else original_join_recv(conn, entry); |
|
} |
|
|
|
static void release_ack(void) { |
|
struct ll_entry* entry = held_ack; |
|
held_ack = NULL; |
|
if (entry) { original_join_recv(held_conn, entry); etcp_conn_ref_free(held_conn); } |
|
} |
|
|
|
/* Инъекция после transport decode: проверяем корреляцию и malformed, без подделки криптографии. */ |
|
static void inject_ack(uint64_t peer, uint64_t group, uint64_t key, int status, int size, int flags) { |
|
struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + 10); |
|
e->len = ROUTER_SVC_PAYLOAD_OFF + size; |
|
memset(e->dgram, 0, e->memlen); |
|
e->dgram[ROUTER_SVC_FLAGS_OFF] = flags; |
|
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &peer, 8); |
|
memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8); |
|
e->dgram[ROUTER_SVC_PAYLOAD_OFF] = JOIN_SUBCMD_KEY_REGISTER_ACK; |
|
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, &key, 8); |
|
e->dgram[ROUTER_SVC_PAYLOAD_OFF + 9] = status; |
|
struct ETCP_CONN conn = {0}; conn.instance = g_inst[0]; |
|
original_join_recv(&conn, e); |
|
} |
|
|
|
static void t_registration_edges(void) { |
|
original_join_recv = g_inst[0]->router_bindings.callbacks[ETCP_RT_ID_JOIN]; |
|
etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, hold_ack); |
|
TEST("registration: delayed ACK, wrong peer/channel/key, duplicate"); { |
|
struct registration_result r = {0}; |
|
uint64_t key = JOIN_KEY + 10; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); |
|
poll_ms(200); |
|
int ok = req && held_ack && !r.calls && chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[0]; |
|
inject_ack(g_nid[1] ^ 1, g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
inject_ack(g_nid[1], g_group_id + 1, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
inject_ack(g_nid[1], g_group_id, key + 1, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
inject_ack(g_nid[1], g_group_id, key, 0, 9, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
inject_ack(g_nid[1], g_group_id, key, 2, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
inject_ack(g_nid[1], g_group_id, key, 0, 10, 0); |
|
inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_ENCRYPTED); |
|
ok = ok && !r.calls && !chat_join_lookup_inviter(g_inst[0], g_group_id, key); |
|
release_ack(); |
|
inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED); |
|
if (ok && r.calls == 1 && r.result == CHAT_JOIN_OK && !g_inst[0]->join_registrations) OK(); else FAIL(); |
|
if (!r.calls && req) chat_join_cancel_registration(req); |
|
} |
|
TEST("registration: cancelled request ignores late ACK"); { |
|
struct registration_result r = {0}; |
|
uint64_t key = JOIN_KEY + 11; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); |
|
poll_ms(100); |
|
int ok = req && held_ack && !r.calls; |
|
if (req) chat_join_cancel_registration(req); |
|
release_ack(); poll_ms(30); |
|
if (ok && r.calls == 1 && r.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations |
|
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL(); |
|
} |
|
TEST("registration: missing ACK times out once, late ACK ignored"); { |
|
struct registration_result r = {0}; |
|
uint64_t key = JOIN_KEY + 12; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); |
|
wait_registered(&r); |
|
int had_ack = held_ack != NULL; |
|
release_ack(); poll_ms(20); |
|
if (req && had_ack && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations |
|
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL(); |
|
if (!r.calls && req) chat_join_cancel_registration(req); |
|
} |
|
etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, original_join_recv); |
|
TEST("registration: concurrent keys and duplicate pending key"); { |
|
struct registration_result r[2] = {{0}, {0}}, duplicate = {0}; |
|
struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &r[0]); |
|
struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 21, registered, &r[1]); |
|
struct chat_join_registration* d = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &duplicate); |
|
int first = wait_registered(&r[0]), second = wait_registered(&r[1]); |
|
if (a && b && !d && !duplicate.calls && first && second && !g_inst[0]->join_registrations) OK(); else FAIL(); |
|
if (!r[0].calls && a) chat_join_cancel_registration(a); |
|
if (!r[1].calls && b) chat_join_cancel_registration(b); |
|
if (d) chat_join_cancel_registration(d); |
|
} |
|
TEST("registration: conflict is rejected without replacing inviter"); { |
|
uint64_t key = JOIN_KEY + 22; |
|
chat_join_store_local_key(g_inst[1], g_group_id, key); |
|
struct registration_result r = {0}; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r); |
|
wait_registered(&r); |
|
if (req && r.calls == 1 && r.result == CHAT_JOIN_ERROR |
|
&& chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[1]) OK(); else FAIL(); |
|
if (!r.calls && req) chat_join_cancel_registration(req); |
|
} |
|
TEST("registration: send failure leaves no pending timer or callback"); { |
|
struct registration_result r = {0}; |
|
struct ll_queue* saved = g_inst[0]->router_conns; |
|
g_inst[0]->router_conns = NULL; /* inject router allocation/unavailability failure */ |
|
TimeoutEntry timer; timeout_heap_peek(g_ua->timeout_heap, &timer); |
|
size_t before = g_ua->timer_alloc_count - g_ua->timer_free_count; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 26, registered, &r); |
|
uint8_t key[32] = {0}; struct join_member_data member = {0}; |
|
member.node_id = g_nid[0]; member.x25519 = member.ed25519 = key; |
|
int forwarded = chat_join_forward_request(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, &member); |
|
g_inst[0]->router_conns = saved; |
|
timeout_heap_peek(g_ua->timeout_heap, &timer); /* освобождает отменённые таймеры без вызова callback */ |
|
if (!req && !r.calls && forwarded == -1 && !g_inst[0]->join_registrations |
|
&& before == g_ua->timer_alloc_count - g_ua->timer_free_count) OK(); |
|
else FAIL("req=%p calls=%d forward=%d timers=%zu/%zu", (void*)req, r.calls, forwarded, |
|
before, g_ua->timer_alloc_count - g_ua->timer_free_count); |
|
if (req) chat_join_cancel_registration(req); |
|
} |
|
TEST("registration: self callback deferred, cancellation, shutdown"); { |
|
struct registration_result r = {0}, cancelled = {0}, shutdown = {0}; |
|
struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 23, registered, &r); |
|
int deferred = !r.calls; |
|
int ok = wait_registered(&r); |
|
struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 24, registered, &cancelled); |
|
if (b) chat_join_cancel_registration(b); |
|
struct chat_join_registration* c = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 25, registered, &shutdown); |
|
chat_join_destroy(g_inst[0]); |
|
chat_join_init(g_inst[0]); |
|
poll_ms(100); |
|
if (a && b && c && deferred && ok && cancelled.calls == 1 && cancelled.result == CHAT_JOIN_CANCELLED |
|
&& shutdown.calls == 1 && shutdown.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations) OK(); else FAIL(); |
|
} |
|
TEST("registration: connection node shutdown times out without success"); { |
|
struct registration_result r = {0}; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 27, registered, &r); |
|
chat_join_destroy(g_inst[1]); member_sync_destroy(g_inst[1]); |
|
g_inst[1]->running = 0; utun_instance_destroy(g_inst[1]); g_inst[1] = NULL; |
|
wait_registered(&r); |
|
if (req && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations |
|
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, JOIN_KEY + 27)) OK(); else FAIL(); |
|
if (!r.calls && req) chat_join_cancel_registration(req); |
|
} |
|
|
|
} |
|
|
|
/* ── 5.2 интеграция: register_key через etcp_router ── */ |
|
|
|
static void t_integration(void) { |
|
g_group_id = strtoull(CH_INT, NULL, 10); |
|
|
|
write_int_configs(); |
|
if (create_instances(2) != 0) return; |
|
|
|
TEST("integration: A<->C connected"); { |
|
if (wait_connected(2)) OK(); else FAIL(); |
|
} |
|
|
|
/* CHAT-группа + канал + мемберы (после connect — чтобы сработал passive topo_group_new_conn) */ |
|
{ |
|
uint8_t ch_x_pub[32], ch_x_priv[32], ch_ed_pub[32], ch_ed_priv[32]; |
|
gen_x25519(ch_x_pub, ch_x_priv); gen_ed25519(ch_ed_pub, ch_ed_priv); |
|
uint8_t ch_sig[64]; |
|
sc_ed25519_sign(ch_ed_priv, (const uint8_t*)CH_INT, strlen(CH_INT), ch_sig); |
|
for (int i = 0; i < 2; i++) { |
|
sqlite3* db = g_inst[i]->topo_sqlite_db; |
|
topo_node_sqlite_channel_put(db, CH_INT, "test", g_nid[0], ch_x_pub, NULL, ch_ed_pub, NULL, ch_sig); |
|
if (!topo_groups_find(g_inst[i]->topo_groups, g_group_id)) |
|
topo_groups_create_group(g_inst[i]->topo_groups, g_group_id, TOPO_GROUP_TYPE_CHAT, CH_INT); |
|
for (int m = 0; m < 2; m++) { |
|
member_sync_put(g_inst[i], CH_INT, g_nid[m], |
|
g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, |
|
NULL, 0, NULL, 0, "{\"name\":\"node\"}", NULL, NULL, 0, 0, NULL); |
|
topo_node_sqlite_node_update_verified(g_inst[i]->topo_sqlite_db, g_nid[m], |
|
"node", g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, |
|
(uint64_t)time(NULL), time(NULL)); |
|
} |
|
chat_join_init(g_inst[i]); |
|
} |
|
} |
|
|
|
TEST("integration: CHAT group BGP converged"); { |
|
if (wait_chat_bgp(2)) OK(); else FAIL(); |
|
} |
|
|
|
for (int repeat = 0; repeat < 2; repeat++) { |
|
TEST(repeat ? "integration: repeated registration confirmed" : "integration: registration confirmed after C stores key"); |
|
struct registration_result r = {0}; |
|
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, registered, &r); |
|
int deferred = !r.calls, ok = wait_registered(&r); |
|
if (req && deferred && ok && chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) OK(); else FAIL(); |
|
if (!r.calls && req) chat_join_cancel_registration(req); |
|
} |
|
t_registration_edges(); |
|
|
|
destroy_instances(2); |
|
} |
|
|
|
/* ── main ── */ |
|
|
|
int main(void) { |
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); |
|
if (getenv("UTUN_TEST_DEBUG")) { |
|
debug_set_category_level_by_name("chat_sync", "info"); |
|
debug_set_category_level_by_name("member_sync", "info"); |
|
debug_set_category_level_by_name("etcp_route", "info"); |
|
debug_set_category_level_by_name("bgp", "trace"); |
|
} |
|
utun_instance_set_tun_init_enabled(0); |
|
srand((unsigned)time(NULL)); |
|
printf("=== test_chat_join ===\n"); |
|
{ char t[256] = "/tmp/utun_cj_XXXXXX"; |
|
(void)test_mkdtemp(t); snprintf(g_tdir, sizeof(g_tdir), "%s", t); } |
|
|
|
g_ua = uasync_create(); |
|
if (!g_ua) { printf("FAIL: uasync_create\n"); return 1; } |
|
|
|
/* ── компонентные (без инстанса) ── */ |
|
t_build_sign_msg(); |
|
t_sign_verify(); |
|
t_serialize_parse(); |
|
|
|
/* ── компонентные (один инстанс, без [chatserver]) ── */ |
|
write_comp_config(); |
|
if (create_instances(1) == 0) { |
|
t_process_request(); |
|
t_register_key_self(); |
|
t_validate_pubkey(); |
|
destroy_instances(1); |
|
} |
|
|
|
/* ── интеграция (2 инстанса через etcp_router) ── */ |
|
t_integration(); |
|
|
|
fflush(stdout); fflush(stderr); |
|
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); fflush(stdout); |
|
|
|
if (g_ua) { |
|
uasync_poll(g_ua, 0); |
|
if (g_ua->timer_alloc_count != g_ua->timer_free_count) { |
|
fprintf(stderr, "timer leak: allocated=%zu freed=%zu\n", g_ua->timer_alloc_count, g_ua->timer_free_count); |
|
G_FAILED++; |
|
} |
|
uasync_destroy(g_ua, 0); g_ua = NULL; |
|
} |
|
t_rmdir(g_tdir); |
|
return G_FAILED > 0 ? 1 : 0; |
|
}
|
|
|