Browse Source

chat: verify_and_purge — единственное удаление мембера (битая подпись при чтении из БД)

topo_upd
evgeny 2 months ago
parent
commit
1899f3a00f
  1. 134
      src/chat/member_sync.c
  2. 16
      src/chat/member_sync.h

134
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);
}

16
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 если нет сессии/соединения.

Loading…
Cancel
Save