fix(chat): verify incoming JOIN profiles before local insertion

This commit is contained in:
evgeny committed 2026-10-27 11:48:09 +01:00
1 parent 054ade0dcd
commit ce3287ac94
5 files changed
+89 -37

No files matched your search

+8 -3
View File
@@ -17,7 +17,7 @@
#endif
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler) {
if (!inst) return;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: handler requires instance"); return; }
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-evt", "chat_event_set_handler: handler=%p old=%p",
(void*)handler, (void*)inst->chat_event_handler);
@@ -26,7 +26,11 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha
}
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (inst && inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
if (!inst || type <= 0 || len < 0 || (len && !data)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat_event: invalid event instance=%p type=%d bytes=%d data=%p",
(void*)inst, type, len, (const void*)data); return;
}
if (inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
static const char* names[] = {
[1]="MSG_RECEIVED", [2]="CONNECT_RESULT", [3]="NEW_PEER",
@@ -64,7 +68,8 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : NULL;
if (!n) n = "?";
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
if (data && len > 0 && type != CHAT_EVT_LOCAL_INVITE && type != CHAT_EVT_INVITE_LINK_READY)
log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
}
/* Идентификатор связывает результат с конкретной отправкой, независимо от текущего канала GUI. */
+5 -3
View File
@@ -6,7 +6,8 @@
* post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет.
* data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия.
*
* Без обработчика — данные логируются через DEBUG_DEBUG + log_dump; LOCAL_INVITE с ключом не дампится.
* Без обработчика — DEBUG_DEBUG + log_dump; LOCAL_INVITE/INVITE_LINK_READY с секретами не дампятся.
* Обработчик может удалить канал; stop/destroy сервиса выполняются вне callbacks.
* Все вызовы — из uasync-потока.
*/
@@ -61,7 +62,7 @@ extern "C" {
#define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */
#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1][text:var] */
#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1] */
#define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][generation:8][JSON:utf8] — история указанного поколения */
#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */
#define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */
@@ -101,7 +102,8 @@ void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn ha
// ── Отправка событий из ядра ──
// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*.
// Синхронно передать type>0 и len>=0 байт data; inst обязателен,data может быть NULL только при len=0.
// Неверные аргументы логируются без доставки;формат CHAT_EVT_* проверяет получатель,память остаётся отправителю.
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
// Сообщить этап подготовки: state=0 начало,1 готово,2 ошибка; target — ID без префикса, is_dm добавляет "dm:".
+40 -25
View File
@@ -8,6 +8,7 @@
#include "chat_join.h"
#include "member_sync.h"
#include "chat_event.h"
#include "chat_sync.h"
#include "../utun_instance.h"
#include "../routing_layer/topo_node_sqlite.h"
@@ -59,11 +60,11 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
}
static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
if (!channel_id || !join_key || !inviter_id) {
if (!inst || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID);
return -1;
}
_keys_purge_expired(inst);
for (struct join_key_entry* e = inst->join_keys; e; e = e->next) {
if (e->channel_id != channel_id || e->join_key != join_key) continue;
if (e->inviter_id != inviter_id) {
@@ -103,13 +104,17 @@ static void _keys_free_all(struct UTUN_INSTANCE* inst) {
int chat_join_serialize_member(const struct join_member_data* m,
uint8_t* out, size_t out_sz, size_t* out_len) {
if (!m || !out || !out_len || !m->x25519 || !m->ed25519) return -1;
if (!m || !out || !out_len || !m->x25519 || !m->ed25519) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid member serialization arguments"); return -1;
}
size_t ul = strnlen(m->userinfo, sizeof(m->userinfo));
if (ul > MS_USERINFO_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "chat_join: profile exceeds limit bytes=%zu", ul); return -1;
}
size_t need = 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2 + ul;
if (need > out_sz) return -1;
if (need > out_sz) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: member buffer too small need=%zu capacity=%zu", need, out_sz); return -1;
}
size_t off = 0;
memcpy(out + off, &m->node_id, 8); off += 8;
memcpy(out + off, m->x25519, 32); off += 32;
@@ -125,8 +130,11 @@ int chat_join_serialize_member(const struct join_member_data* m,
return 0;
}
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m) {
if (!data || !m || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) return -1;
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* output) {
if (!data || !output || len < 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "chat_join: invalid/short member block bytes=%zu", len); return -1;
}
struct join_member_data parsed, *m = &parsed;
memset(m, 0, sizeof(*m));
size_t off = 0;
memcpy(&m->node_id, data + off, 8); off += 8;
@@ -142,6 +150,7 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d
}
memcpy(m->userinfo, data + off, ul);
m->userinfo[ul] = '\0';
*output = parsed;
return 0;
}
@@ -150,6 +159,9 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_d
void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t from_node, uint64_t join_key,
const uint8_t* member_data, size_t member_len) {
if (!inst || inst->join_stopping || !group_id || group_id > INT64_MAX || !from_node) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid/stopped process request", CJ_ID); return;
}
struct join_member_data m;
if (chat_join_parse_member(member_data, member_len, &m) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request parse failed ch=%llu from=%016llx", CJ_ID, (unsigned long long)group_id, (unsigned long long)from_node);
@@ -188,6 +200,18 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
/* Локальная запись доверенная: до неё проверяем также профиль и уже известный ключ идентичности. */
struct ms_member_rec record = {.node_id = m.node_id, .x25519 = m.x25519, .ed25519 = m.ed25519,
.join_sig = m.join_sig, .join_ts = m.join_ts, .update_sig = m.update_sig, .update_ts = m.update_ts,
.userinfo = m.userinfo};
uint8_t trusted_ed[32];
int lookup = topo_node_sqlite_get_ed25519_pubkey(db, m.node_id, trusted_ed);
if (lookup == TOPO_SQLITE_LOOKUP_ERROR || (lookup == 0 && memcmp(trusted_ed, m.ed25519, sizeof(trusted_ed))) ||
member_sync_verify_local_record(db, ch_id, &record) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member validation failed ch=%s node=%016llx lookup=%d", CJ_ID,
ch_id, (unsigned long long)m.node_id, lookup); return;
}
/* 3. добавить мембера + подписать его pubkey (дерево приглашений) */
uint8_t tree_sig[64];
if (member_sync_sign_pubkey(inst->my_ed25519_privkey, m.x25519, tree_sig) != 0) {
@@ -204,22 +228,8 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
if (rc & MS_APPLY_CHANGED) {
/* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* jc = instance_find_conn(inst, m.node_id);
if (jc) {
topo_group_new_conn(g, jc);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: joiner 0x%016llx added to group ch=%s for BGP",
CJ_ID, (unsigned long long)m.node_id, ch_id);
}
}
/* уведомить GUI инвайтера об успешном присоединении джойнера */
uint8_t cevt[20]; memcpy(cevt, &m.node_id, 8);
int r = 0; memcpy(cevt + 8, &r, 4);
memcpy(cevt + 12, &group_id, 8);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
/* member_sync уже запускает сессию после commit; GUI получает общий формат результата. */
chat_sync_join_result(inst, 0, group_id, m.node_id, CHAT_JOIN_OK);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me",
@@ -354,7 +364,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* ── Жизненный цикл ── */
int chat_join_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: init without instance", CJ_ID); return -1; }
inst->join_stopping = 0;
if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID);
@@ -382,7 +392,7 @@ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, u
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || channel_id > INT64_MAX || !join_key || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID);
return NULL;
}
@@ -415,6 +425,9 @@ struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
if (!inst || !channel_id || channel_id > INT64_MAX || !join_key) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key lookup", CJ_ID); return 0;
}
struct join_key_entry* e = _keys_find(inst, channel_id, join_key);
return e ? e->inviter_id : 0;
}
@@ -422,7 +435,9 @@ uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_i
int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t inviter_id, uint64_t join_key,
const struct join_member_data* m) {
if (!inst || !m || inviter_id == 0) return -1;
if (!inst || inst->join_stopping || !m || !channel_id || channel_id > INT64_MAX || !join_key || !inviter_id) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid forward arguments", CJ_ID); return -1;
}
uint8_t mbuf[MS_USERINFO_MAX + 256]; size_t mlen = 0;
if (chat_join_serialize_member(m, mbuf, sizeof(mbuf), &mlen) != 0) {
+9 -4
View File
@@ -15,7 +15,7 @@
* │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J)
* │ │ валиден ключ → только pending, БЕЗ BGP new_conn
* │◀── REQUEST_FWD ───────────┤ │
* │ verif join_sig, node_id=derive(x25519) │
* │ verif JOIN,профиля,известного Ed25519,node_id=derive(x25519) │
* │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join"))
* │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │
* │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C
@@ -115,11 +115,14 @@ struct join_member_data {
char userinfo[MS_USERINFO_MAX + 1]; /* JSON, null-terminated */
};
/* Сериализовать member-блок (без join_key). Возвращает 0 и *out_len, -1 если не влез. */
/* Сериализовать member-блок (без join_key). 0 и *out_len,-1 при неверных аргументах/размере.
* На отказе out/out_len сохраняются; подписи NULL кодируются 64 нулями,ключи обязательны.
* Числа native,кроме userinfo_len:u16BE;userinfo<=1024 байт без NUL на проводе. */
int chat_join_serialize_member(const struct join_member_data* m,
uint8_t* out, size_t out_sz, size_t* out_len);
/* Разобрать member-блок. Указатели ссылаются на data (не копируют). 0=ок, -1=ошибка. */
/* Разобрать весь member-блок:проверить длину/отсутствие скрытого NUL. 0/-1,на отказе m сохраняется.
* Ключи/подписи заимствованы у data,userinfo копируется;криптография здесь не проверяется. */
int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_data* m);
/* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */
@@ -147,7 +150,9 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
const struct join_member_data* m);
/* Инвайтер (локально, когда connection==инвайтер, или из recv REQUEST_FWD):
* верификация join_sig + добавление мембера + подпись дерева. */
* Проверка ключа,идентичности,JOIN,подписи профиля при наличии и известного Ed25519;
* затем доверенная локальная запись с подписью приглашения. Join-only допускается.
* Подключение группы инициирует member_sync после commit;при изменении CONNECT_RESULT имеет request_id=0. */
void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t from_node, uint64_t join_key,
const uint8_t* member_data, size_t member_len);
+27 -2
View File
@@ -7,10 +7,11 @@
// Задача 2 (интеграция): два инстанса A(инвайтер) и C(connection) — регистрация join-ключа
// через etcp_router, после сходимости BGP lookup_inviter(C) == A.node_id.
//
// chat_core/chat_sync (глобальные синглтоны) НЕ используются: конфиг без секции [chatserver],
// Сервис chat_core/chat_sync не запускается: конфиг без секции [chatserver],
// member_sync/chat_join инициализируются вручную.
#include "chat_join.h"
#include "chat_event.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/route_crypto.h"
#include "../transport_layer/etcp_api.h"
@@ -31,6 +32,7 @@
#include "test_net.h"
#include <sqlite3.h>
#include <assert.h>
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <stdio.h>
@@ -58,6 +60,14 @@ 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 uint64_t g_group_id = 0;
static unsigned join_results;
static void check_join_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
(void)inst;
if (type != CHAT_EVT_CONNECT_RESULT) return;
uint64_t request; int32_t result;
assert(len == 28); memcpy(&request, data + 20, 8); memcpy(&result, data + 8, 4);
assert(request == 0 && result == 0); join_results++;
}
/* ── крипто-хелперы ── */
@@ -415,6 +425,7 @@ static void t_process_request(void) {
/* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */
TEST("process_request: valid join_sig accepted"); {
chat_event_set_handler(inst, check_join_event);
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);
@@ -427,8 +438,22 @@ static void t_process_request(void) {
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();
if (in_ch == 1 && gs == 0 && sb == inst->node_id && vs == 1 && join_results == 1) OK();
else FAIL("in_ch=%d gs=%d sb=%016llx vs=%d", in_ch, gs, (unsigned long long)sb, vs);
chat_event_set_handler(inst, NULL);
}
TEST("process_request: signed profile accepted"); {
struct joiner j; make_joiner(&j, ch_x_pub, ch_ed_pub);
uint8_t update[64], message[MS_USERINFO_MAX + 73], buf[MS_USERINFO_MAX + 256]; size_t len;
struct join_member_data m = {.node_id = j.node_id, .x25519 = j.x_pub, .ed25519 = j.ed_pub,
.join_sig = j.join_sig, .join_ts = j.join_ts, .update_sig = update, .update_ts = 1};
strcpy(m.userinfo, "{\"name\":\"signed profile\"}");
int bytes = member_sync_build_update_msg(j.join_sig, m.update_ts, m.userinfo, message, sizeof(message));
assert(bytes > 0 && sc_ed25519_sign(j.ed_priv, message, (size_t)bytes, update) == SC_OK);
assert(!chat_join_serialize_member(&m, 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();
}
/* 2. невалидный join_sig → отклонён */