diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 90f6c2b8..367fa87d 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -502,6 +502,137 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, return (changed ? MS_APPLY_CHANGED : 0) | (stale ? MS_APPLY_STALE : 0); } +/* ── Верификация локальной записи при чтении из БД ── + Единственный сценарий удаления мембера: подпись присутствует и невалидна, + либо node_id != derive(x25519). Отсутствие подписи = «не верифицировано», не удаляем. */ + +int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const struct ms_member_rec* m) { + if (!db || !ch_id || !m || !m->x25519 || !m->ed25519) return 1; /* нет данных — не битая */ + + /* жёсткая привязка идентичности */ + if (sc_derive_node_id_from_pubkey(m->x25519) != m->node_id) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — node_id/x25519 mismatch nid=0x%016llx ns=%s", + MS_ID, (unsigned long long)m->node_id, ch_id); + return 0; + } + + /* update_sig присутствует — верифицируем */ + if (m->update_sig && !_sig_is_zero64(m->update_sig)) { + uint8_t vmsg[8192]; + int vlen = member_sync_build_update_msg(m->join_sig, m->update_ts, + m->userinfo ? m->userinfo : "", vmsg, (int)sizeof(vmsg)); + if (vlen < 0 || !m->join_sig) return 0; + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, m->ed25519, 32); + if (!pkey) return 0; + EVP_MD_CTX* ver = EVP_MD_CTX_new(); + int ok = ver && (EVP_DigestVerifyInit(ver, NULL, NULL, NULL, pkey) == 1) + && (EVP_DigestVerify(ver, m->update_sig, 64, vmsg, (size_t)vlen) == 1); + if (ver) EVP_MD_CTX_free(ver); + EVP_PKEY_free(pkey); + if (!ok) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid update_sig nid=0x%016llx ns=%s", + MS_ID, (unsigned long long)m->node_id, ch_id); + return 0; + } + } else if (m->join_sig && !_sig_is_zero64(m->join_sig)) { + /* join-only — верифицируем join_sig напрямую */ + if (!_verify_join_sig(db, ch_id, m)) return 0; + } + + /* adm_tags присутствует — верифицируем adm_tags_sig */ + if (m->adm_tags && m->adm_tags[0] && m->adm_tags_sig && !_sig_is_zero64(m->adm_tags_sig)) { + uint8_t ch_ed_pub[32] = {0}; + if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) != 0) + return 1; /* нет канального ключа — не можем проверить, не удаляем */ + uint8_t amsg[264]; size_t aoff = 0; + size_t atl2 = strlen(m->adm_tags); + if (atl2 > 191) atl2 = 191; + memcpy(amsg + aoff, m->adm_tags, atl2); aoff += atl2; + memcpy(amsg + aoff, &m->node_id, 8); aoff += 8; + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); + if (!pkey) return 1; + EVP_MD_CTX* vctx = EVP_MD_CTX_new(); + int ok = vctx && (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) + && (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1); + if (vctx) EVP_MD_CTX_free(vctx); + EVP_PKEY_free(pkey); + if (!ok) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s tags=%s", + MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags); + return 0; + } + } + return 1; +} + +/* Прогоняет все записи канала: битая → удалить + пересчитать хеши. */ +int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id) { + if (!inst || !ch_id) return 0; + sqlite3* db = _db(inst); if (!db) return 0; + char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); + + sqlite3_stmt* st = NULL; + char sql[512]; snprintf(sql, sizeof(sql), + "SELECT node_id, x25519_pubkey, ed25519_pubkey," + " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig, source" + " FROM \"%s\" ORDER BY node_id ASC", peers_tbl); + if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return 0; + + int purged = 0; + while (sqlite3_step(st) == SQLITE_ROW) { + uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); + const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(st, 1); + const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(st, 2); + const uint8_t* jsig = (const uint8_t*)sqlite3_column_blob(st, 3); + uint64_t jts = (uint64_t)sqlite3_column_int64(st, 4); + const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(st, 5); + uint64_t uts = (uint64_t)sqlite3_column_int64(st, 6); + const char* nm = (const char*)sqlite3_column_text(st, 7); + const char* atags = (const char*)sqlite3_column_text(st, 8); + const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(st, 9); + int src = sqlite3_column_int(st, 10); + if (!x25 || !ed) continue; + if (src != 0) continue; /* плейсхолдер topo_group — не верифицируем */ + + struct ms_member_rec m; + memset(&m, 0, sizeof(m)); + m.node_id = nid; + m.x25519 = x25; m.ed25519 = ed; + m.join_sig = jsig; m.join_ts = jts; + m.update_sig = usig; m.update_ts = uts; + m.userinfo = nm; + m.adm_tags = atags; m.adm_tags_sig = atsig; + + if (!member_sync_verify_local_record(db, ch_id, &m)) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_and_purge — PURGE broken record nid=0x%016llx ns=%s", + MS_ID, (unsigned long long)nid, ch_id); + topo_node_sqlite_member_del(db, ch_id, nid); + merkle_sync_recompute_path(inst, ch_id, nid); + purged++; + } + } + sqlite3_finalize(st); + if (purged > 0) + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_and_purge — purged %d broken records ns=%s", + MS_ID, purged, ch_id); + return purged; +} + +/* Прогнать очистку битых записей по всем каналам. */ +void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) { + sqlite3* db = _db(inst); if (!db) return; + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(db, + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", + -1, &st, NULL) != SQLITE_OK) return; + while (sqlite3_step(st) == SQLITE_ROW) { + const char* tbl = (const char*)sqlite3_column_text(st, 0); + if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; + member_sync_verify_and_purge(inst, tbl + 6); + } + sqlite3_finalize(st); +} + static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; @@ -629,6 +760,8 @@ int member_sync_init(struct UTUN_INSTANCE* inst) { if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init — inst is NULL", MS_ID); return -1; } int rc = merkle_sync_init(inst, 0x31, &g_member_ops, inst); if (rc != 0) return rc; + /* очистка битых записей во всех каналах перед построением дерева */ + member_sync_verify_and_purge_all(inst); _rebuild_all_trees(inst); /* подписка на BGP-события существующих chat-групп */ if (inst->topo_groups && inst->topo_groups->group_list) { @@ -660,6 +793,7 @@ void member_sync_destroy(struct UTUN_INSTANCE* inst) { int member_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id, merkle_sync_done_cb done_cb, void* arg) { + member_sync_verify_and_purge(inst, ch_id); return merkle_sync_start(inst, peer, ch_id, done_cb, arg); } diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index bc83ad4d..fb0a0acf 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -51,6 +51,7 @@ extern "C" { struct UTUN_INSTANCE; struct TOPO_GROUP; +struct sqlite3; /* Результат применения рекорда мембера (compare+update по версиям двух блоков). */ #define MS_APPLY_CHANGED 0x01 /* блок стал новее — обновлён, нужен recompute + relay остальным */ @@ -155,6 +156,21 @@ int member_sync_build_join_msg(const uint8_t* ch_x25519, const uint8_t* ch_ed25 int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t from_peer, const struct ms_member_rec* m); +/* + * Верифицировать локальную запись мембера (чтение из БД). Единственный сценарий + * удаления: подпись присутствует и невалидна, либо node_id != derive(x25519). + * Отсутствие подписи = «не верифицировано» (self-add/плейсхолдер) — не битая. + * Возвращает 1 если запись валидна/не подписана, 0 если битая. + */ +int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, + const struct ms_member_rec* m); + +/* Прогнать все записи канала: битую удалить + пересчитать хеши. Возвращает число удалённых. */ +int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id); + +/* Прогнать очистку битых записей по всем каналам (при init). */ +void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst); + /* * Отправить наш полный рекорд мембера конкретному пиру (send-back при stale). * Возвращает 0 при успехе, -1 если нет сессии/соединения.