Browse Source

Support 8 KiB member owner tags without signature or serialization truncation

proxy
evgeny 4 days ago
parent
commit
7374d1648c
  1. 37
      lib/json_flat.c
  2. 3
      lib/json_flat.h
  3. 125
      src/chat/chat_core.c
  4. 1
      src/chat/chat_core.h
  5. 77
      src/chat/member_sync.c
  6. 4
      src/chat/member_sync.h
  7. 7
      src/chat/member_sync_doc.md
  8. 71
      src/routing_layer/topo_node_sqlite.c
  9. 3
      src/routing_layer/topo_node_sqlite.h
  10. 5
      src/routing_layer/topo_node_sqlite_doc.md
  11. 16
      tests/test_media_delivery_chat.c
  12. 15
      tests/test_media_delivery_full.c
  13. 102
      tests/test_member_adapter.c

37
lib/json_flat.c

@ -1,4 +1,6 @@
#include "json_flat.h" #include "json_flat.h"
#include "mem.h"
#include "debug_config.h"
#include <string.h> #include <string.h>
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++ #define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
@ -19,6 +21,8 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
case 'n': ch = '\n'; break; case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break; case 'r': ch = '\r'; break;
case 't': ch = '\t'; break; case 't': ch = '\t'; break;
case 'b': ch = '\b'; break;
case 'f': ch = '\f'; break;
default: return -1; default: return -1;
} }
} }
@ -33,20 +37,22 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
return 0; return 0;
} }
static int json_parse_pair(const char** pp, const char* end, static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
json_flat_cb cb, void* arg) { json_flat_cb cb, void* arg) {
const char* p = *pp; const char* p = *pp;
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') { *pp = p; return 0; } if (*p == '}') { *pp = p; return 0; }
char key[128], val[256]; char* key = scratch;
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1; if (json_read_string(&p, end, key, capacity) != 0) return -1;
size_t key_size = strlen(key) + 1;
char* val = scratch + key_size;
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end); CHECK(p, end);
if (*p != ':') return -1; p++; if (*p != ':') return -1; p++;
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end); CHECK(p, end);
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1; if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; } if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end); CHECK(p, end);
if (*p == ',') p++; if (*p == ',') p++;
@ -61,13 +67,23 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end); CHECK(p, end);
if (*p != '{') return -1; p++; if (*p != '{') return -1; p++;
/* Decoded key + value + two NULs fit within the encoded object size. */
size_t capacity = strlen(json) + 1;
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
char local[384];
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
int result = 0;
for (;;) { for (;;) {
int rc = json_parse_pair(&p, end, cb, arg); int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
if (rc < 0) return -1; if (rc < 0) { result = -1; break; }
if (rc > 0) return 0; /* callback stopped */ if (rc > 0) break;
SKIP_WS(p, end); CHECK(p, end); SKIP_WS(p, end);
if (*p == '}') return 0; if (p >= end) { result = -1; break; }
if (*p == '}') break;
} }
if (scratch != local) u_free(scratch);
return result;
} }
struct get_ctx { const char* key; char* out; size_t sz; int found; }; struct get_ctx { const char* key; char* out; size_t sz; int found; };
@ -81,6 +97,7 @@ static int get_cb(const char* k, const char* v, void* arg) {
} }
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) { int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
if (!key || (value_out && !value_sz)) return -1;
struct get_ctx g = { key, value_out, value_sz, 0 }; struct get_ctx g = { key, value_out, value_sz, 0 };
if (json_flat_parse(json, get_cb, &g) != 0) return -1; if (json_flat_parse(json, get_cb, &g) != 0) return -1;
return g.found ? 0 : -1; return g.found ? 0 : -1;

3
lib/json_flat.h

@ -2,7 +2,7 @@
* @file json_flat.h * @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...} * @brief Парсер плоского JSON без вложенности: {"key":"value",...}
* *
* Только строковые значения. Без malloc, без зависимостей. * Только строковые значения. Callback получает полные строки без обрезания.
*/ */
#ifndef JSON_FLAT_H #ifndef JSON_FLAT_H
@ -15,6 +15,7 @@ extern "C" {
#endif #endif
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg); typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
int json_flat_parse(const char* json, json_flat_cb cb, void* arg); int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz); int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);

125
src/chat/chat_core.c

@ -300,7 +300,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg) {
static void json_escape(const char* src, char* dst, size_t dst_sz) { static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src; size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) { while (*s && i < dst_sz - 2) {
if (*s == '"' || *s == '\\') { if (i + 1 < dst_sz - 2) dst[i++] = '\\'; } const char* escape = NULL;
switch (*s) {
case '"': escape = "\\\""; break;
case '\\': escape = "\\\\"; break;
case '\n': escape = "\\n"; break;
case '\r': escape = "\\r"; break;
case '\t': escape = "\\t"; break;
case '\b': escape = "\\b"; break;
case '\f': escape = "\\f"; break;
}
if (escape) {
if (i + 2 >= dst_sz) break;
dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue;
}
dst[i++] = *s++; dst[i++] = *s++;
} }
dst[i] = '\0'; dst[i] = '\0';
@ -403,7 +416,8 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) { int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst); struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1; if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1;
*out_len = 0;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size; char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = ""; *w++ = '['; const char* sep = "";
@ -412,6 +426,12 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags " "SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl); "FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
sqlite3_stmt* as = NULL;
#define MEMBERS_APPEND(...) do { \
int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \
if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \
w += written; \
} while (0)
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) { while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
@ -422,7 +442,7 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
int x25519_len = sqlite3_column_bytes(st, 2); int x25519_len = sqlite3_column_bytes(st, 2);
int ed25519_len = sqlite3_column_bytes(st, 3); int ed25519_len = sqlite3_column_bytes(st, 3);
const char* adm_tags = (const char*)sqlite3_column_text(st, 4); const char* adm_tags = (const char*)sqlite3_column_text(st, 4);
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[384]; char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1];
json_escape(name ? name : "", esc_name, sizeof(esc_name)); json_escape(name ? name : "", esc_name, sizeof(esc_name));
x25519_hex[0] = ed25519_hex[0] = '\0'; x25519_hex[0] = ed25519_hex[0] = '\0';
if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]); if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]);
@ -437,14 +457,13 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags)); json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags));
w += snprintf(w, (size_t)(end - w), MEMBERS_APPEND(
"%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d," "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d,"
"\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", "\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[",
sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex); sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex);
sep = ","; sep = ",";
/* addresses */ /* addresses */
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql), char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol"); "SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
@ -465,20 +484,24 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7], addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7],
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]); addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]);
else { snprintf(ip_str, sizeof(ip_str), "?"); } else { snprintf(ip_str, sizeof(ip_str), "?"); }
w += snprintf(w, (size_t)(end - w), MEMBERS_APPEND(
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}", "%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}",
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt); asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt);
asep2 = ","; asep2 = ",";
} }
sqlite3_finalize(as); sqlite3_finalize(as); as = NULL;
} }
w += snprintf(w, (size_t)(end - w), "]}"); MEMBERS_APPEND("]}");
} }
MEMBERS_APPEND("]");
sqlite3_finalize(st); sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf); *out_len = (size_t)(w - buf);
return 0; return 0;
overflow:
sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size);
return -1;
#undef MEMBERS_APPEND
} }
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) { int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
@ -500,7 +523,7 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
struct chat_member_tags_key { struct chat_member_tags_key {
char key[CHAT_MEMBER_TAGS_KEY_SZ]; char key[CHAT_MEMBER_TAGS_KEY_SZ];
char val[CHAT_MEMBER_TAGS_VAL_SZ]; char* val;
char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */ char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */
}; };
@ -530,20 +553,19 @@ static int tags_is_append_key(const char* key) {
return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0; return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0;
} }
static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) {
uint64_t ts = 0;
const char* eff = tags_append_effective(raw, &ts);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff);
(void)ts;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) { static int tags_json_cb(const char* key, const char* json_val, void* arg) {
struct chat_member_tags* t = (struct chat_member_tags*)arg; struct chat_member_tags* t = (struct chat_member_tags*)arg;
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS) return -1; if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1;
}
struct chat_member_tags_key* k = &t->keys[t->key_count]; struct chat_member_tags_key* k = &t->keys[t->key_count];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val); k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL; k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (!k->val || (tags_is_append_key(key) && !k->raw)) {
u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k));
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1;
}
t->key_count++; t->key_count++;
return 0; return 0;
} }
@ -563,7 +585,9 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) { if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0); const char* tags = (const char*)sqlite3_column_text(st, 0);
if (tags && tags[0]) chat_member_tags_replace(t, tags); if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) {
sqlite3_finalize(st); chat_member_tags_free(t); return NULL;
}
} }
sqlite3_finalize(st); sqlite3_finalize(st);
} }
@ -575,22 +599,24 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) { int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) {
if (!t || !key) return -1; if (!t || !key) return -1;
if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; } if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; }
if (val && strlen(val) >= CHAT_MEMBER_TAGS_VAL_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; } if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
int idx = tags_find(t, key); int idx = tags_find(t, key);
if (!val) { if (!val) {
if (idx >= 0) { u_free(t->keys[idx].raw); t->keys[idx] = t->keys[--t->key_count]; } if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; }
return 0; return 0;
} }
char* copy = u_strdup(val);
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; }
if (idx >= 0) { if (idx >= 0) {
snprintf(t->keys[idx].val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); u_free(t->keys[idx].val); t->keys[idx].val = copy;
} else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) { } else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) {
struct chat_member_tags_key* k = &t->keys[t->key_count++]; struct chat_member_tags_key* k = &t->keys[t->key_count++];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); k->val = copy;
k->raw = NULL; k->raw = NULL;
} else { } else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1; u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
} }
return 0; return 0;
} }
@ -602,11 +628,17 @@ const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) {
} }
int chat_member_tags_replace(struct chat_member_tags* t, const char* json) { int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
if (!t || !json) return -1; if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) {
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1;
t->key_count = 0; }
int rc = json_flat_parse(json, tags_json_cb, t); struct chat_member_tags parsed = {0};
if (rc != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; } int rc = json_flat_parse(json, tags_json_cb, &parsed);
if (rc != 0) {
for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); }
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1;
}
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys));
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d", DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count); CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count);
return 0; return 0;
@ -617,8 +649,14 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) { if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1; DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
} }
char json[4096]; int off = 0, ver = 1; char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1;
#define TAGS_ADVANCE(n) do { if (off < (int)sizeof(json) - 10) off += (int)(n); else off = (int)sizeof(json); } while(0) #define TAGS_ADVANCE(expr) do { \
int written = (expr); \
if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \
} \
off += written; \
} while (0)
int vi = tags_find(t, "ver"); int vi = tags_find(t, "ver");
if (vi >= 0) ver = atoi(t->keys[vi].val) + 1; if (vi >= 0) ver = atoi(t->keys[vi].val) + 1;
TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver)); TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver));
@ -647,7 +685,13 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
} else { } else {
out_val = k->val; out_val = k->val;
} }
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", k->key, out_val)); char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1];
if (strlen(out_val) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1;
}
json_escape(k->key, escaped_key, sizeof(escaped_key));
json_escape(out_val, escaped_value, sizeof(escaped_value));
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value));
} }
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}")); TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}"));
#undef TAGS_ADVANCE #undef TAGS_ADVANCE
@ -656,10 +700,9 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) { if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1; DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
} }
uint8_t sign_msg[8192]; size_t mlen = (size_t)off; uint8_t sign_msg[MS_ADM_TAGS_MAX + 8];
if (mlen + 8 > sizeof(sign_msg)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — json too big", CC_ID); return -1; } int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg));
memcpy(sign_msg, json, mlen); if (mlen < 0) return -1;
memcpy(sign_msg + mlen, &t->node_id, 8); mlen += 8;
uint8_t sig[64]; uint8_t sig[64];
if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) { if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1; DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1;
@ -702,14 +745,14 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
sqlite3_finalize(st); sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; } if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s", DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json); CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off);
return 0; return 0;
} }
void chat_member_tags_free(struct chat_member_tags* t) { void chat_member_tags_free(struct chat_member_tags* t) {
if (!t) return; if (!t) return;
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
u_free(t); u_free(t);
} }

1
src/chat/chat_core.h

@ -214,7 +214,6 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
#define CHAT_MEMBER_TAGS_MAX_KEYS 16 #define CHAT_MEMBER_TAGS_MAX_KEYS 16
#define CHAT_MEMBER_TAGS_KEY_SZ 64 #define CHAT_MEMBER_TAGS_KEY_SZ 64
#define CHAT_MEMBER_TAGS_VAL_SZ 256
struct chat_member_tags; struct chat_member_tags;

77
src/chat/member_sync.c

@ -45,11 +45,23 @@ static uint64_t member_read64(const uint8_t* p) {
return n; return n;
} }
int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity) {
size_t len = tags ? strlen(tags) : 0;
if (!tags || !out || len > MS_ADM_TAGS_MAX || capacity < len + 8) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid owner message node=%016llx bytes=%zu capacity=%zu max=%d",
MS_ID, (unsigned long long)node_id, len, capacity, MS_ADM_TAGS_MAX);
return -1;
}
memcpy(out, tags, len);
memcpy(out + len, &node_id, 8);
return (int)len + 8;
}
static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519,
const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts,
const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig, const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig,
uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) { uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) {
uint8_t data[1024]; uint8_t data[MS_ADM_TAGS_MAX + 1024];
size_t off = 0; size_t off = 0;
member_write64(data + off, node_id); off += 8; member_write64(data + off, node_id); off += 8;
memcpy(data + off, x25519, 32); off += 32; memcpy(data + off, x25519, 32); off += 32;
@ -59,13 +71,13 @@ static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const u
if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64); if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64);
off += 64; member_write64(data + off, update_ts); off += 8; off += 64; member_write64(data + off, update_ts); off += 8;
size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0; size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0;
if (ul > 255 || atl > 255) { if (ul > 255 || atl > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id);
return -1; return -1;
} }
data[off++] = (uint8_t)ul; data[off++] = (uint8_t)ul;
if (ul) { memcpy(data + off, userinfo, ul); off += ul; } if (ul) { memcpy(data + off, userinfo, ul); off += ul; }
data[off++] = (uint8_t)atl; data[off++] = (uint8_t)(atl >> 8); data[off++] = (uint8_t)atl;
if (atl) { memcpy(data + off, adm_tags, atl); off += atl; } if (atl) { memcpy(data + off, adm_tags, atl); off += atl; }
if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64); if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64);
off += 64; member_write64(data + off, signed_by); off += 8; off += 64; member_write64(data + off, signed_by); off += 8;
@ -237,15 +249,15 @@ static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int afte
if (src != 0) continue; if (src != 0) continue;
if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1)
|| !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1) || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1)
|| (nm && strlen(nm) > 255) || (atags && strlen(atags) > 255)) { || (nm && strlen(nm) > 255) || (atags && strlen(atags) > MS_ADM_TAGS_MAX)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid);
sqlite3_finalize(stmt); return -1; sqlite3_finalize(stmt); return -1;
} }
uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0;
uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; size_t atl = atags ? strlen(atags) : 0;
uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 8 + 64; size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 2 + atl + 64 + 8 + 64;
if (off + need > *len) { if (off + need > *len) {
if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; }
*more = 1; *more = 1;
@ -268,7 +280,7 @@ static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int afte
} }
buf[off++] = nl; buf[off++] = nl;
if (nl) { memcpy(buf + off, nm, nl); off += nl; } if (nl) { memcpy(buf + off, nm, nl); off += nl; }
buf[off++] = atl; buf[off++] = (uint8_t)(atl >> 8); buf[off++] = (uint8_t)atl;
if (atl) { memcpy(buf + off, atags, atl); off += atl; } if (atl) { memcpy(buf + off, atags, atl); off += atl; }
if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; } if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; }
else { memset(buf + off, 0, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; }
@ -364,7 +376,7 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
/* REMOVE не удаляет member (удаление — только битая подпись при верификации) */ /* REMOVE не удаляет member (удаление — только битая подпись при верификации) */
char tags[256] = ""; char tags[MS_ADM_TAGS_MAX + 1] = "";
char peers_tbl[128]; _peers_table(group->channel_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[128]; _peers_table(group->channel_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL; sqlite3_stmt* st = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT COALESCE(adm_tags,'') FROM \"%s\" WHERE node_id=?", peers_tbl); char sql[256]; snprintf(sql, sizeof(sql), "SELECT COALESCE(adm_tags,'') FROM \"%s\" WHERE node_id=?", peers_tbl);
@ -377,8 +389,8 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
sqlite3_finalize(st); sqlite3_finalize(st);
} }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags=%s", DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags_bytes=%zu",
MS_ID, event, (unsigned long long)node_id, tags); MS_ID, event, (unsigned long long)node_id, strlen(tags));
_fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id); _fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id);
} }
@ -475,9 +487,9 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — db is NULL", MS_ID); return -1; } sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — db is NULL", MS_ID); return -1; }
int stale = 0, changed = 0; int stale = 0, changed = 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags=%s", DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags_bytes=%zu",
MS_ID, ch_id, (unsigned long long)m->node_id, (unsigned long long)m->update_ts, MS_ID, ch_id, (unsigned long long)m->node_id, (unsigned long long)m->update_ts,
m->adm_tags ? m->adm_tags : ""); m->adm_tags ? strlen(m->adm_tags) : 0);
char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl));
@ -485,7 +497,7 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
int has_local = 0; int has_local = 0;
uint8_t local_join_sig[64] = {0}; uint64_t local_join_ts = 0; uint8_t local_join_sig[64] = {0}; uint64_t local_join_ts = 0;
uint64_t local_update_ts = 0; uint64_t local_update_ts = 0;
char local_tags[256] = ""; char local_tags[MS_ADM_TAGS_MAX + 1] = "";
uint64_t local_signed_by = 0; uint64_t local_signed_by = 0;
uint8_t local_signature[64] = {0}; uint8_t local_signature[64] = {0};
int local_src = 0; int local_src = 0;
@ -601,18 +613,16 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0; adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0;
uint8_t ch_ed_pub[32] = {0}; 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) { if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) {
uint8_t amsg[264]; size_t aoff = 0; uint8_t amsg[MS_ADM_TAGS_MAX + 8];
size_t atl2 = strlen(m->adm_tags); int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg));
if (atl2 > 191) atl2 = 191; if (aoff < 0) return MT_ERR_DATA;
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); EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32);
if (pkey) { if (pkey) {
EVP_MD_CTX* vctx = EVP_MD_CTX_new(); EVP_MD_CTX* vctx = EVP_MD_CTX_new();
if (vctx) { if (vctx) {
int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1)
&& (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1); && (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1);
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s tags=%s", MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags); if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s bytes=%zu", MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags));
else block_b_ok = 1; else block_b_ok = 1;
EVP_MD_CTX_free(vctx); EVP_MD_CTX_free(vctx);
} }
@ -635,7 +645,7 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s", DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s",
MS_ID, local_src, (unsigned long long)m->node_id, ch_id); MS_ID, local_src, (unsigned long long)m->node_id, ch_id);
if (!identity_ok || !block_a_ok || (!is_local && m->adm_tags && m->adm_tags[0] && !block_b_ok)) { if (!identity_ok || !block_a_ok || (m->adm_tags && m->adm_tags[0] && !block_b_ok)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d", DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d",
MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok); MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok);
return MT_ERR_DATA; return MT_ERR_DATA;
@ -762,11 +772,9 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const
uint8_t ch_ed_pub[32] = {0}; 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) if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) != 0)
return 1; /* нет канального ключа — не можем проверить, не удаляем */ return 1; /* нет канального ключа — не можем проверить, не удаляем */
uint8_t amsg[264]; size_t aoff = 0; uint8_t amsg[MS_ADM_TAGS_MAX + 8];
size_t atl2 = strlen(m->adm_tags); int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg));
if (atl2 > 191) atl2 = 191; if (aoff < 0) return 0;
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); EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32);
if (!pkey) return 1; if (!pkey) return 1;
EVP_MD_CTX* vctx = EVP_MD_CTX_new(); EVP_MD_CTX* vctx = EVP_MD_CTX_new();
@ -775,8 +783,8 @@ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const
if (vctx) EVP_MD_CTX_free(vctx); if (vctx) EVP_MD_CTX_free(vctx);
EVP_PKEY_free(pkey); EVP_PKEY_free(pkey);
if (!ok) { if (!ok) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s tags=%s", DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s bytes=%zu",
MS_ID, (unsigned long long)m->node_id, ch_id, m->adm_tags); MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags));
return 0; return 0;
} }
} }
@ -877,7 +885,7 @@ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) {
struct member_wire { struct member_wire {
struct ms_member_rec rec; struct ms_member_rec rec;
char userinfo[256], tags[256]; char userinfo[256], tags[MS_ADM_TAGS_MAX + 1];
uint8_t local_join[64]; uint8_t local_join[64];
}; };
@ -898,11 +906,11 @@ static int member_decode(const uint8_t* p, size_t len, struct member_wire* w, si
if (len - off < 73) return -1; if (len - off < 73) return -1;
m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8; m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8;
uint8_t n = p[off++]; uint8_t n = p[off++];
if (len - off < (size_t)n + 1 || memchr(p + off, 0, n)) return -1; if (len - off < (size_t)n + 2 || memchr(p + off, 0, n)) return -1;
memcpy(w->userinfo, p + off, n); off += n; memcpy(w->userinfo, p + off, n); off += n;
n = p[off++]; size_t tags_len = ((size_t)p[off] << 8) | p[off + 1]; off += 2;
if (len - off < (size_t)n + 136 || memchr(p + off, 0, n)) return -1; if (tags_len > MS_ADM_TAGS_MAX || len - off < tags_len + 136 || memchr(p + off, 0, tags_len)) return -1;
memcpy(w->tags, p + off, n); off += n; memcpy(w->tags, p + off, tags_len); off += tags_len;
m->adm_tags_sig = p + off; off += 64; m->adm_tags_sig = p + off; off += 64;
m->signed_by = member_read64(p + off); off += 8; m->signed_by = member_read64(p + off); off += 8;
m->signature = p + off; off += 64; m->signature = p + off; off += 64;
@ -918,7 +926,7 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, co
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA; DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA;
} }
uint16_t count = ((uint16_t)data[0] << 8) | data[1]; uint16_t count = ((uint16_t)data[0] << 8) | data[1];
if (count > (len - 2) / 283) { if (count > (len - 2) / 284) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA; DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA;
} }
size_t off = 2; size_t off = 2;
@ -1134,7 +1142,8 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage, const char* adm_tags, const uint8_t* adm_tags_sig, int storage,
uint64_t signed_by, const uint8_t* signature) { uint64_t signed_by, const uint8_t* signature) {
if (!inst || !ch_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put — inst=%p ch_id=%s", MS_ID, (void*)inst, ch_id ? ch_id : "(null)"); return -1; } if (!inst || !ch_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put — inst=%p ch_id=%s", MS_ID, (void*)inst, ch_id ? ch_id : "(null)"); return -1; }
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s adm_tags=%s storage=%d signed_by=%016llx", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", adm_tags ? adm_tags : "", storage, (unsigned long long)signed_by); DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx tags_bytes=%zu storage=%d signed_by=%016llx",
MS_ID, ch_id, (unsigned long long)member_id, adm_tags ? strlen(adm_tags) : 0, storage, (unsigned long long)signed_by);
struct ms_member_rec m; struct ms_member_rec m;
memset(&m, 0, sizeof(m)); memset(&m, 0, sizeof(m));

4
src/chat/member_sync.h

@ -55,6 +55,10 @@ struct sqlite3;
#define MS_APPLY_CHANGED 0x01 /* запись и дерево обновлены */ #define MS_APPLY_CHANGED 0x01 /* запись и дерево обновлены */
#define MS_APPLY_STALE 0x02 /* локальная версия новее; обратный проход доставит её пиру */ #define MS_APPLY_STALE 0x02 /* локальная версия новее; обратный проход доставит её пиру */
/* UTF-8 JSON bytes, excluding NUL. A complete record fits in one Merkle page. */
#define MS_ADM_TAGS_MAX 8192
int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity);
/* Распарсенный рекорд мембера (два подписанных блока). */ /* Распарсенный рекорд мембера (два подписанных блока). */
struct ms_member_rec { struct ms_member_rec {
uint64_t node_id; uint64_t node_id;

7
src/chat/member_sync_doc.md

@ -33,10 +33,11 @@
SHA256 по полям: SHA256 по полям:
node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8, node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8,
update_sig:64, update_ts:8, userinfo_len:1, userinfo, update_sig:64, update_ts:8, userinfo_len:1, userinfo,
adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
Целые big-endian; отсутствующие подписи представлены нулями. Целые big-endian; отсутствующие подписи представлены нулями.
Обе строки ограничены 255 байтами. Хеш включает userinfo и дерево приглашений. userinfo ограничен 255 байтами, adm_tags — 8192 байтами UTF-8 без NUL. Длина adm_tags — uint16 big-endian.
Хеш и подпись владельца покрывают весь adm_tags без обрезания. Хеш включает userinfo и дерево приглашений.
Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно. Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно.
## Страница на проводе ## Страница на проводе
@ -45,7 +46,7 @@ adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
node_id:8, x25519:32, ed25519:32, flags:1, node_id:8, x25519:32, ed25519:32, flags:1,
при HAS_JOIN — join_sig:64 и join_ts:8, при HAS_JOIN — join_sig:64 и join_ts:8,
update_sig:64, update_ts:8, userinfo_len:1, userinfo, update_sig:64, update_ts:8, userinfo_len:1, userinfo,
adm_tags_len:1, adm_tags, adm_tags_sig:64, signed_by:8, signature:64. adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
Целые big-endian. Встроенные NUL и хвостовые байты запрещены. Целые big-endian. Встроенные NUL и хвостовые байты запрещены.
get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора. get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора.

71
src/routing_layer/topo_node_sqlite.c

@ -588,7 +588,12 @@ int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint6
sqlite3_bind_int(u, 3, storage); sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags)); sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id); sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u); int rc = sqlite3_step(u); sqlite3_finalize(u);
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member_owner_put: ch=%s node=%016llx rc=%d: %s",
channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db));
return -1;
}
return 0; return 0;
} }
@ -751,70 +756,6 @@ int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
return rc; return rc;
} }
int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
uint8_t* buf, size_t buf_sz, size_t* out_len) {
if (!db || !channel_id || !buf || !out_len) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* stmt = 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"
" FROM \"%s\" ORDER BY node_id ASC", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
size_t off = 0;
if (off + 2 > buf_sz) { sqlite3_finalize(stmt); return -2; }
uint16_t* cnt_ptr = (uint16_t*)(buf + off); off += 2;
*cnt_ptr = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
uint64_t node_id = (uint64_t)sqlite3_column_int64(stmt, 0);
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(stmt, 1);
const uint8_t* ed_pub = (const uint8_t*)sqlite3_column_blob(stmt, 2);
const uint8_t* join_sig = (const uint8_t*)sqlite3_column_blob(stmt, 3);
uint64_t join_ts = (uint64_t)sqlite3_column_int64(stmt, 4);
const uint8_t* update_sig = (const uint8_t*)sqlite3_column_blob(stmt, 5);
uint64_t update_ts = (uint64_t)sqlite3_column_int64(stmt, 6);
const char* puserinfo = (const char*)sqlite3_column_text(stmt, 7);
const char* adm_tags_s = (const char*)sqlite3_column_text(stmt, 8);
const uint8_t* adm_tags_sig_b = (const uint8_t*)sqlite3_column_blob(stmt, 9);
if (!x25519 || !ed_pub) continue;
uint8_t nl = puserinfo ? (uint8_t)strnlen(puserinfo, 255) : 0;
uint8_t atl = adm_tags_s ? (uint8_t)strnlen(adm_tags_s, 255) : 0;
uint8_t flags = (join_sig && join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64;
if (off + peer_needed > buf_sz) { sqlite3_finalize(stmt); return -2; }
memcpy(buf + off, &node_id, 8); off += 8;
memcpy(buf + off, x25519, 32); off += 32;
memcpy(buf + off, ed_pub, 32); off += 32;
buf[off++] = flags;
if (flags & PEERS_FLAG_HAS_JOIN) {
memcpy(buf + off, join_sig, 64); off += 64;
memcpy(buf + off, &join_ts, 8); off += 8;
}
if (update_sig && update_ts) {
memcpy(buf + off, update_sig, 64); off += 64;
memcpy(buf + off, &update_ts, 8); off += 8;
} else {
memset(buf + off, 0, 64); off += 64;
uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8;
}
buf[off++] = nl;
if (nl) { memcpy(buf + off, puserinfo, nl); off += nl; }
buf[off++] = atl;
if (atl) { memcpy(buf + off, adm_tags_s, atl); off += atl; }
if (adm_tags_sig_b && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, adm_tags_sig_b, 64); off += 64; }
else { memset(buf + off, 0, 64); off += 64; }
(*cnt_ptr)++;
}
sqlite3_finalize(stmt);
*out_len = off;
return 0;
}
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) { int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) {
if (!db) return -1; if (!db) return -1;
sqlite3_stmt* stmt = NULL; sqlite3_stmt* stmt = NULL;

3
src/routing_layer/topo_node_sqlite.h

@ -83,9 +83,6 @@ int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id, int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
const uint8_t* x25519_priv, const uint8_t* ed25519_priv); const uint8_t* x25519_priv, const uint8_t* ed25519_priv);
int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
uint8_t* buf, size_t buf_sz, size_t* out_len);
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online); int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online);
int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id); int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id);
int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]); int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32]);

5
src/routing_layer/topo_node_sqlite_doc.md

@ -11,11 +11,11 @@
3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()` 3. Сохранять/обновлять ноды через `topo_node_sqlite_node_put()`
4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_<channel_id>`) 4. Сохранять каналы через `topo_node_sqlite_channel_put()` (автоматически создаёт таблицу `peers_<channel_id>`)
5. Добавлять участников в канал через `topo_node_sqlite_member_put()` 5. Добавлять участников в канал через `topo_node_sqlite_member_put()`
6. Читать участников канала через `topo_node_sqlite_channel_peers_all()` — возвращает бинарный буфер со всеми участниками (node_id, ключи, подписи, адреса) 6. Читать участников из `peers_<channel_id>`; постраничная сетевая сериализация принадлежит `member_sync`.
**Нюансы:** **Нюансы:**
- Имена таблиц `peers_<channel_id>` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_` - Имена таблиц `peers_<channel_id>` санитизируются: все символы кроме `[a-zA-Z0-9_]` заменяются на `_`
- `topo_node_sqlite_channel_peers_all()` формирует компактный бинарный буфер с числом записей в начале (uint16_t) - Сетевой формат участников описан в `src/chat/member_sync_doc.md`; SQLite-слой не дублирует сериализатор.
- Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта - Ключи в BLOB-полях всегда 32 байта (x25519/ed25519), подписи — 64 байта
- Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN - Функции `online` и `get_ed25519_pubkey` — простые SELECT-запросы без JOIN
@ -31,7 +31,6 @@
| `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_<ch_id>` | | `topo_node_sqlite_member_del(db, ch_id, node_id)` | Удаляет участника из `peers_<ch_id>` |
| `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) | | `topo_node_sqlite_node_update_verified(...)` | Обновляет ключи и имя ноды по условию `update_ts < новое_значение` (защита от stale-данных) |
| `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) | | `topo_node_sqlite_channel_get(...)` | Читает метаданные канала (имя, владелец, ключи, подпись) |
| `topo_node_sqlite_channel_peers_all(...)` | Сериализует всех участников канала в бинарный буфер (count + [node_id\|x25519\|ed25519\|flags\|sigs\|name\|addrs]) |
| `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды | | `topo_node_sqlite_node_set_online(db, id, online)` | Устанавливает флаг `online` для ноды |
| `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online | | `topo_node_sqlite_node_get_online(db, id)` | Возвращает 1 если нода online |
| `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) | | `topo_node_sqlite_get_ed25519_pubkey(db, id, out)` | Извлекает ed25519-публичный ключ ноды (32 байта) |

16
tests/test_media_delivery_chat.c

@ -408,7 +408,7 @@ static int author_publish_message(void) {
/* ── фазы ── */ /* ── фазы ── */
static void setup_chat_group(void) { static void setup_chat_group(void) {
uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ch_sig[64] = {0}; uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ed_priv[32] = {0}, ch_sig[64] = {0};
int i; int i;
TEST("generate channel keys"); { TEST("generate channel keys"); {
@ -419,6 +419,7 @@ static void setup_chat_group(void) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); } if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); }
if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l); if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l);
l = 32; EVP_PKEY_get_raw_private_key(epkey, ed_priv, &l);
EVP_MD_CTX* mctx = EVP_MD_CTX_new(); EVP_MD_CTX* mctx = EVP_MD_CTX_new();
if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey); if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey);
size_t sl = 64; EVP_DigestSign(mctx, ch_sig, &sl, (const uint8_t*)CH_ID, strlen(CH_ID)); EVP_MD_CTX_free(mctx); } size_t sl = 64; EVP_DigestSign(mctx, ch_sig, &sl, (const uint8_t*)CH_ID, strlen(CH_ID)); EVP_MD_CTX_free(mctx); }
@ -440,12 +441,19 @@ static void setup_chat_group(void) {
} }
TEST("add 6 members (s1/s2 supernode, st1/st2 storage)"); { TEST("add 6 members (s1/s2 supernode, st1/st2 storage)"); {
const char* adm[N_NODES] = { NULL, NULL, "{\"supernode\":\"yes\"}", "{\"supernode\":\"yes\"}", "{\"storage\":\"yes\"}", "{\"storage\":\"yes\"}" }; const char* adm[N_NODES] = { NULL, NULL, "{\"ver\":\"1\",\"supernode\":\"yes\"}", "{\"ver\":\"1\",\"supernode\":\"yes\"}",
"{\"ver\":\"1\",\"storage\":\"yes\"}", "{\"ver\":\"1\",\"storage\":\"yes\"}" };
for (i = 0; i < N_NODES; i++) { for (i = 0; i < N_NODES; i++) {
for (int m = 0; m < N_NODES; m++) { for (int m = 0; m < N_NODES; m++) {
member_sync_put(g_inst[i], CH_ID, g_nid[m], uint8_t sig[64], msg[MS_ADM_TAGS_MAX + 8];
if (adm[m]) {
int n = member_sync_build_owner_msg(g_nid[m], adm[m], msg, sizeof(msg));
if (n < 0 || sc_ed25519_sign(ed_priv, msg, n, sig) != SC_OK) { FAIL("owner signing failed"); return; }
}
int rc = member_sync_put(g_inst[i], CH_ID, g_nid[m],
g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey,
NULL, 0, NULL, 0, "{\"name\":\"node\"}", adm[m], NULL, 0, 0, NULL); NULL, 0, NULL, 0, "{\"name\":\"node\"}", adm[m], adm[m] ? sig : NULL, 0, 0, NULL);
if (rc < 0) { FAIL("member insertion failed rc=%d", rc); return; }
/* заполняем nodes-таблицу (нужно для верификации Ed25519 подписи блоков/автора) */ /* заполняем nodes-таблицу (нужно для верификации Ed25519 подписи блоков/автора) */
topo_node_sqlite_node_update_verified(g_inst[i]->topo_sqlite_db, g_nid[m], 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, "node", g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey,

15
tests/test_media_delivery_full.c

@ -114,7 +114,7 @@ static int msend(struct UTUN_INSTANCE* inst, uint64_t dst, const uint8_t* data,
/* ── Phase P1: create CHAT group + members on all nodes ── */ /* ── Phase P1: create CHAT group + members on all nodes ── */
static void setup_chat_group(void) { static void setup_chat_group(void) {
uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ch_sig[64] = {0}; uint8_t x25519_pub[32] = {0}, ed_pub[32] = {0}, ed_priv[32] = {0}, ch_sig[64] = {0};
int i; int i;
TEST("generate channel keys"); { TEST("generate channel keys"); {
@ -125,6 +125,7 @@ static void setup_chat_group(void) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); } if (ctx) { EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &epkey); EVP_PKEY_CTX_free(ctx); }
if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l); if (epkey) { size_t l = 32; EVP_PKEY_get_raw_public_key(epkey, ed_pub, &l);
l = 32; EVP_PKEY_get_raw_private_key(epkey, ed_priv, &l);
/* sign channel */ /* sign channel */
EVP_MD_CTX* mctx = EVP_MD_CTX_new(); EVP_MD_CTX* mctx = EVP_MD_CTX_new();
if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey); if (mctx) { EVP_DigestSignInit(mctx, NULL, NULL, NULL, epkey);
@ -147,15 +148,21 @@ static void setup_chat_group(void) {
} }
TEST("add all 4 members (n1, n2, s1 supernode, s2 supernode)"); { TEST("add all 4 members (n1, n2, s1 supernode, s2 supernode)"); {
const char* adm[4] = { NULL, NULL, "supernode=yes", "supernode=yes" }; const char* adm[4] = { NULL, NULL, "{\"ver\":\"1\",\"supernode\":\"yes\"}", "{\"ver\":\"1\",\"supernode\":\"yes\"}" };
for (i = 0; i < N_NODES; i++) { for (i = 0; i < N_NODES; i++) {
sqlite3* db = g_inst[i]->topo_sqlite_db; sqlite3* db = g_inst[i]->topo_sqlite_db;
if (!db) continue; if (!db) continue;
for (int m = 0; m < N_NODES; m++) { for (int m = 0; m < N_NODES; m++) {
member_sync_put(g_inst[i], CH_ID, g_nid[m], uint8_t sig[64], msg[MS_ADM_TAGS_MAX + 8];
if (adm[m]) {
int n = member_sync_build_owner_msg(g_nid[m], adm[m], msg, sizeof(msg));
if (n < 0 || sc_ed25519_sign(ed_priv, msg, n, sig) != SC_OK) { FAIL("owner signing failed"); return; }
}
int rc = member_sync_put(g_inst[i], CH_ID, g_nid[m],
g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey, g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey,
NULL, 0, NULL, 0, "{\"name\":\"node\"}", NULL, 0, NULL, 0, "{\"name\":\"node\"}",
adm[m], NULL, 0, 0, NULL); adm[m], adm[m] ? sig : NULL, 0, 0, NULL);
if (rc < 0) { FAIL("member insertion failed rc=%d", rc); return; }
} }
} }
int ok = 1; int ok = 1;

102
tests/test_member_adapter.c

@ -20,10 +20,112 @@ static void root(struct UTUN_INSTANCE* inst, uint8_t hash[32]) {
assert(merkle_sync_read_hash(inst, "1001", 0, 0, hash) == 0); assert(merkle_sync_read_hash(inst, "1001", 0, 0, hash) == 0);
} }
static void public_key(const uint8_t priv[32], uint8_t pub[32]) {
EVP_PKEY* pk = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32); assert(pk);
size_t len = 32; assert(EVP_PKEY_get_raw_public_key(pk, pub, &len) == 1); EVP_PKEY_free(pk);
}
static void test_owner_sizes(struct UASYNC* ua) {
struct UTUN_INSTANCE source, target; setup(&source, ua); setup(&target, ua);
uint8_t x[32] = {9}, priv[32] = {7}, pub[32], owner_priv[32] = {8}, owner_pub[32], zero[64] = {0};
public_key(priv, pub); public_key(owner_priv, owner_pub);
assert(topo_node_sqlite_channel_put(source.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, owner_priv, zero) == 0);
assert(topo_node_sqlite_channel_put(target.topo_sqlite_db, "1001", "test", 1, x, NULL, owner_pub, NULL, zero) == 0);
uint64_t nid = sc_derive_node_id_from_pubkey(x);
uint8_t join[64], tree[64], owner[64], msg[MS_ADM_TAGS_MAX + 8];
int n = member_sync_build_join_msg(x, owner_pub, nid, x, 1, msg, sizeof(msg)); assert(n > 0);
assert(sc_ed25519_sign(priv, msg, n, join) == SC_OK);
assert(member_sync_sign_pubkey(owner_priv, x, tree) == 0);
char tags[MS_ADM_TAGS_MAX + 2];
struct ms_member_rec rec = { .node_id = nid, .x25519 = x, .ed25519 = pub,
.join_sig = join, .join_ts = 1, .userinfo = "", .adm_tags = tags, .adm_tags_sig = owner, .signature = tree };
const size_t sizes[] = {191, 192, 255, 256, 4096, MS_ADM_TAGS_MAX};
uint8_t page[MT_PAGE_SIZE], expected[32], actual[32];
for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) {
size_t len = sizes[i];
int prefix = snprintf(tags, sizeof(tags), "{\"ver\":\"%zu\",\"pad\":\"", i + 1);
const char* suffix = "\",\"storage\":\"yes\"}";
size_t tail = len - strlen(suffix);
memset(tags + prefix, 'a', tail - (size_t)prefix); memcpy(tags + tail, suffix, strlen(suffix) + 1);
n = member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)); assert(n == (int)len + 8);
assert(sc_ed25519_sign(owner_priv, msg, n, owner) == SC_OK);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) & MS_APPLY_CHANGED);
assert(member_sync_verify_local_record(source.topo_sqlite_db, "1001", &rec) == 1);
size_t bytes = sizeof(page); uint64_t next; int more;
assert(_member_get_page(&source, "1001", merkle_sync_level_prefix(nid, 5), 0, 0, page, &bytes, &next, &more) == 0);
assert(next == nid && !more && bytes < MT_PAGE_SIZE);
struct member_wire w; size_t used;
assert(member_decode(page + 2, bytes - 2, &w, &used) == 0 && used == bytes - 2 && !strcmp(w.rec.adm_tags, tags));
assert(_member_apply_items(&target, "1001", 2, page, bytes) == 0);
root(&source, expected); root(&target, actual); assert(!memcmp(expected, actual, 32));
sqlite3_stmt* st;
assert(sqlite3_prepare_v2(target.topo_sqlite_db, "SELECT adm_tags,storage FROM peers_1001", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == (int)len);
assert(!strcmp((const char*)sqlite3_column_text(st, 0), tags));
char storage[8];
assert(json_flat_get((const char*)sqlite3_column_text(st, 0), "storage", storage, sizeof(storage)) == 0);
assert(!strcmp(storage, "yes"));
sqlite3_finalize(st);
size_t flip = tail - 1; tags[flip] = 'b';
assert(member_sync_apply_record(&target, "1001", 2, &rec) < 0);
assert(member_sync_apply_record(&source, "1001", source.node_id, &rec) < 0);
assert(member_sync_verify_local_record(target.topo_sqlite_db, "1001", &rec) == 0);
root(&target, actual); assert(!memcmp(expected, actual, 32));
assert(sqlite3_exec(target.topo_sqlite_db, "SAVEPOINT hash_tail", NULL, NULL, NULL) == SQLITE_OK);
assert(topo_node_sqlite_member_owner_put(target.topo_sqlite_db, "1001", nid, tags, owner, 0) == 0);
assert(merkle_sync_recompute_path(&target, "1001", nid) == 1);
root(&target, actual); assert(memcmp(expected, actual, 32));
assert(sqlite3_exec(target.topo_sqlite_db, "ROLLBACK TO hash_tail; RELEASE hash_tail", NULL, NULL, NULL) == SQLITE_OK);
tags[flip] = 'a';
assert(_member_apply_items(&target, "1001", 2, page, bytes - 1) < 0);
size_t length_offset = (size_t)(w.rec.adm_tags_sig - page) - len - 2;
page[length_offset] = 0xff; page[length_offset + 1] = 0xff;
assert(_member_apply_items(&target, "1001", 2, page, bytes) < 0);
}
puts("PASS: full owner signatures and wire roundtrip at 191/192/255/256/4096/8192 bytes; suffix tampering rejected");
struct chat_core_ctx cc = { .inst = &source, .db = source.topo_sqlite_db, .initialized = 1 };
source.chat_core = &cc;
char listing[MT_PAGE_SIZE]; size_t listed;
assert(chat_core_get_members_json(&source, "1001", listing, sizeof(listing), &listed) == 0 && listed > MS_ADM_TAGS_MAX);
struct chat_member_tags* edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
const char* pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096);
assert(chat_member_tags_set(edit, "storage", "no") == 0);
assert(chat_member_tags_commit(&source, edit) == 0);
chat_member_tags_free(edit);
edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
pad = chat_member_tags_get(edit, "pad"); assert(pad && strlen(pad) > 4096);
assert(!strcmp(chat_member_tags_get(edit, "storage"), "no"));
root(&source, expected);
assert(sqlite3_exec(source.topo_sqlite_db, "CREATE TRIGGER reject_owner BEFORE UPDATE OF adm_tags ON peers_1001 "
"BEGIN SELECT RAISE(ABORT,'injected owner failure'); END", NULL, NULL, NULL) == SQLITE_OK);
assert(chat_member_tags_set(edit, "storage", "yes") == 0);
assert(chat_member_tags_commit(&source, edit) < 0);
root(&source, actual); assert(!memcmp(expected, actual, 32));
assert(sqlite3_exec(source.topo_sqlite_db, "DROP TRIGGER reject_owner", NULL, NULL, NULL) == SQLITE_OK);
memset(tags, 'x', MS_ADM_TAGS_MAX + 1); tags[MS_ADM_TAGS_MAX + 1] = 0;
assert(member_sync_build_owner_msg(nid, tags, msg, sizeof(msg)) < 0);
assert(chat_member_tags_set(edit, "pad", tags) < 0);
tags[MS_ADM_TAGS_MAX] = 0;
assert(chat_member_tags_set(edit, "pad", tags) == 0 && chat_member_tags_commit(&source, edit) < 0);
root(&source, actual); assert(!memcmp(expected, actual, 32));
assert(chat_member_tags_set(edit, "pad", "quoted\"\\value\n\r\t\b\f") == 0 && chat_member_tags_commit(&source, edit) == 0);
chat_member_tags_free(edit);
edit = chat_member_tags_load(&source, "1001", nid); assert(edit);
assert(!strcmp(chat_member_tags_get(edit, "pad"), "quoted\"\\value\n\r\t\b\f")); chat_member_tags_free(edit);
char small[8]; size_t output;
assert(chat_core_get_members_json(&source, "1001", small, sizeof(small), &output) < 0 && output == 0);
source.chat_core = NULL;
puts("PASS: long JSON edit/reload; escaping; size and SQLite errors preserve data and tree");
member_sync_destroy(&source); member_sync_destroy(&target);
assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK);
}
int main(void) { int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
size_t baseline = u_get_allocated_count(); size_t baseline = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua); struct UASYNC* ua = uasync_create(); assert(ua);
test_owner_sizes(ua);
struct UTUN_INSTANCE source, target; struct UTUN_INSTANCE source, target;
setup(&source, ua); setup(&target, ua); setup(&source, ua); setup(&target, ua);
put(&source, 1); put(&source, 2); put(&source, 1); put(&source, 2);

Loading…
Cancel
Save