753 lines
34 KiB
C
753 lines
34 KiB
C
// 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;
|
|
}
|