#include "devgroup.h" #include "topo_node.h" #include "../utun_instance.h" #include "../transport_layer/secure_channel.h" #include "../../lib/json_flat.h" #include "../../lib/strbuf.h" #include "../../lib/debug_config.h" #include #include #include #include #include #include #include static int dg_error(const char* stage) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "devgroup: rejected stage=%s", stage); return -1; } static int dg_sql(sqlite3* db, const char* sql) { int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "devgroup: SQL failed rc=%d: %s", rc, sqlite3_errmsg(db)); return rc == SQLITE_OK ? 0 : -1; } static int dg_finish(sqlite3* db, sqlite3_stmt* st, int rc, int expected) { int final = sqlite3_finalize(st); if (rc == expected && final == SQLITE_OK) return 0; DEBUG_ERROR(DEBUG_CATEGORY_BGP, "devgroup: SQL rc=%d expected=%d finalize=%d: %s", rc, expected, final, sqlite3_errmsg(db)); return -1; } static int dg_utf8(const char* text) { const unsigned char* p = (const unsigned char*)text; while (*p) { uint32_t c = *p++; unsigned n; uint32_t min; if (c < 128) { if (c < 32) return 0; continue; } if (c >= 0xc2 && c <= 0xdf) { n = 1; min = 0x80; c &= 31; } else if (c >= 0xe0 && c <= 0xef) { n = 2; min = 0x800; c &= 15; } else if (c >= 0xf0 && c <= 0xf4) { n = 3; min = 0x10000; c &= 7; } else return 0; while (n--) { if ((*p & 0xc0) != 0x80) return 0; c = (c << 6) | (*p++ & 63); } if (c < min || c > 0x10ffff || (c >= 0xd800 && c <= 0xdfff)) return 0; } return 1; } /* Строгий каркас плоского JSON; escapes и Unicode проверяет json_flat_parse. */ static void dg_ws(const char** p) { while (**p && strchr(" \t\r\n", **p)) ++*p; } static int dg_string(const char** p) { if (*(*p)++ != '"') return -1; while (**p && **p != '"') { if (**p == '\\') { ++*p; if (!**p) return -1; } ++*p; } if (!**p) return -1; ++*p; return 0; } static int dg_syntax(const char* p) { dg_ws(&p); if (*p++ != '{') return -1; dg_ws(&p); if (*p != '}') while (1) { if (dg_string(&p)) return -1; dg_ws(&p); if (*p++ != ':') return -1; dg_ws(&p); if (dg_string(&p)) return -1; dg_ws(&p); if (*p == '}') break; if (*p++ != ',') return -1; dg_ws(&p); } p++; dg_ws(&p); return *p ? -1 : 0; } static int dg_number(const char* text, uint64_t* out) { if (*text < '1' || *text > '9') return -1; uint64_t value = 0; for (const char* p = text; *p; p++) { if (*p < '0' || *p > '9' || value > ((uint64_t)INT64_MAX - (*p - '0')) / 10) return -1; value = value * 10 + (*p - '0'); } *out = value; return 0; } struct dg_parser { struct devgroup_profile p; uint8_t x[32]; unsigned fields, count; char keys[32][64]; }; static int dg_field(const char* key, const char* value, void* arg) { struct dg_parser* p = arg; if (!*key || strlen(key) > 63 || p->count == 32) return -1; for (unsigned i = 0; i < p->count; i++) if (!strcmp(p->keys[i], key)) return -1; strcpy(p->keys[p->count++], key); if (!strcmp(key, "name")) { if (!*value || strlen(value) > 63 || !dg_utf8(value)) return -1; strcpy(p->p.name, value); p->fields |= 1; } else if (!strcmp(key, "ver")) { if (dg_number(value, &p->p.version)) return -1; p->fields |= 2; } else if (!strcmp(key, "timestamp")) { if (dg_number(value, &p->p.timestamp)) return -1; p->fields |= 4; } else if (!strcmp(key, "x25519")) { if (strlen(value) != 64 || strspn(value, "0123456789abcdef") != 64 || sc_hex_to_binary(value, p->x, 32)) return -1; p->fields |= 8; } return 0; } static size_t dg_profile_message(const char* json, uint8_t* out) { const char domain[] = "utun-devgroup-profile-v1"; memcpy(out, domain, sizeof(domain)); size_t n = strlen(json); memcpy(out + sizeof(domain), json, n); return sizeof(domain) + n; } static size_t dg_member_message(const struct devgroup_record* r, uint64_t node, const uint8_t x[32], const uint8_t ed[32], uint8_t* out) { const char domain[] = "utun-devgroup-member-v1"; size_t n = sizeof(domain); memcpy(out, domain, n); memcpy(out + n, r->ed, 32); n += 32; memcpy(out + n, r->x, 32); n += 32; for (unsigned i = 0; i < 8; i++) out[n++] = (uint8_t)(node >> (8 * i)); memcpy(out + n, x, 32); n += 32; memcpy(out + n, ed, 32); return n + 32; } int devgroup_profile_read(const struct devgroup_record* r, struct devgroup_profile* out) { if (!r || !out || !r->json[0] || strnlen(r->json, sizeof(r->json)) > DEVGROUP_PROFILE_MAX) return dg_error("profile length"); struct dg_parser p = {0}; uint8_t body[DEVGROUP_PROFILE_MAX + 32]; if (dg_syntax(r->json) || json_flat_parse(r->json, dg_field, &p) || p.fields != 15 || memcmp(p.x, r->x, 32)) return dg_error("profile JSON"); size_t n = dg_profile_message(r->json, body); if (sc_ed25519_verify(r->ed, body, n, r->profile_sig) != SC_OK) return dg_error("profile signature"); SHA256((const uint8_t*)r->json, strlen(r->json), p.p.hash); *out = p.p; return 0; } int devgroup_verify(const struct devgroup_record* r, uint64_t node, const uint8_t x[32], const uint8_t ed[32]) { if (!r || !x || !ed) return dg_error("membership arguments"); if (!r->json[0]) { const struct devgroup_record zero = {0}; return memcmp(r, &zero, sizeof(zero)) ? dg_error("partial membership") : 0; } struct devgroup_profile p; uint8_t body[192]; if (devgroup_profile_read(r, &p)) return -1; size_t n = dg_member_message(r, node, x, ed, body); if (sc_ed25519_verify(r->ed, body, n, r->member_sig) != SC_OK) return dg_error("membership signature"); return 0; } int devgroup_encode(const struct devgroup_record* r, uint8_t* out, size_t capacity) { if (!r || !out) return dg_error("encode arguments"); size_t n = strnlen(r->json, sizeof(r->json)); if (!n) return 0; if (n > DEVGROUP_PROFILE_MAX || capacity < 192 + n) return dg_error("encode capacity"); memcpy(out, r->ed, 32); memcpy(out + 32, r->x, 32); memcpy(out + 64, r->member_sig, 64); memcpy(out + 128, r->profile_sig, 64); memcpy(out + 192, r->json, n); return (int)(192 + n); } int devgroup_decode(struct devgroup_record* out, const uint8_t* data, size_t size) { if (!out || (size && (!data || size <= 192 || size > DEVGROUP_WIRE_MAX || memchr(data + 192, 0, size - 192)))) return dg_error("decode length"); struct devgroup_record r = {0}; if (size) { memcpy(r.ed, data, 32); memcpy(r.x, data + 32, 32); memcpy(r.member_sig, data + 64, 64); memcpy(r.profile_sig, data + 128, 64); memcpy(r.json, data + 192, size - 192); } *out = r; return 0; } int devgroup_profile_compare(const struct devgroup_profile* a, const struct devgroup_profile* b) { if (a->version != b->version) return a->version > b->version ? 1 : -1; if (a->timestamp != b->timestamp) return a->timestamp > b->timestamp ? 1 : -1; return memcmp(a->hash, b->hash, 32); } int devgroup_bind(struct devgroup_record* r, const uint8_t priv[32], uint64_t node, const uint8_t x[32], const uint8_t ed[32]) { if (!r || !priv || !node || !x || !ed) return dg_error("bind arguments"); uint8_t body[192]; size_t n = dg_member_message(r, node, x, ed, body); if (sc_ed25519_sign(priv, body, n, r->member_sig) != SC_OK) return dg_error("bind signature"); return devgroup_verify(r, node, x, ed); } /* При локальном переименовании сохраняем неизвестные строковые поля расширений. */ static int dg_copy_extension(const char* key, const char* value, void* arg) { if (!strcmp(key, "name") || !strcmp(key, "ver") || !strcmp(key, "timestamp") || !strcmp(key, "x25519")) return 0; struct strbuf* json = arg; return strbuf_addf(json, ",") < 0 || strbuf_add_json_string(json, key) || strbuf_addf(json, ":") < 0 || strbuf_add_json_string(json, value) ? -1 : 0; } int devgroup_sign(struct devgroup_record* r, const uint8_t priv[32], const char* name, uint64_t version, uint64_t timestamp, uint64_t node, const uint8_t x[32], const uint8_t ed[32]) { if (!r || !priv || !name || !*name || strlen(name) > 63 || !dg_utf8(name) || !version || version > INT64_MAX || !timestamp || timestamp > INT64_MAX || !node || !x || !ed) return dg_error("sign arguments"); struct devgroup_record next = *r; struct strbuf json = {0}; char hex[65]; for (unsigned i = 0; i < 32; i++) snprintf(hex + i * 2, 3, "%02x", r->x[i]); int rc = strbuf_addf(&json, "{\"ver\":\"%llu\",\"timestamp\":\"%llu\",\"x25519\":\"%s\",\"name\":", (unsigned long long)version, (unsigned long long)timestamp, hex); if (rc < 0 || strbuf_add_json_string(&json, name) || (r->json[0] && json_flat_parse(r->json, dg_copy_extension, &json)) || strbuf_addf(&json, "}") < 0 || json.len > DEVGROUP_PROFILE_MAX) { strbuf_free(&json); return dg_error("JSON allocation/size"); } memset(next.json, 0, sizeof(next.json)); memcpy(next.json, json.buf, json.len); strbuf_free(&json); uint8_t body[DEVGROUP_PROFILE_MAX + 32]; size_t n = dg_profile_message(next.json, body); if (sc_ed25519_sign(priv, body, n, next.profile_sig) != SC_OK) return dg_error("profile signing"); n = dg_member_message(&next, node, x, ed, body); if (sc_ed25519_sign(priv, body, n, next.member_sig) != SC_OK || devgroup_verify(&next, node, x, ed)) return dg_error("membership signing"); *r = next; return 0; } int devgroup_public_keys(const uint8_t ep[32], const uint8_t xp[32], uint8_t ed[32], uint8_t x[32]) { EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ep, 32); size_t n = 32; int ok = key && EVP_PKEY_get_raw_public_key(key, ed, &n) == 1 && n == 32; EVP_PKEY_free(key); if (!ok || sc_compute_public_key_from_private(xp, x) != SC_OK) return dg_error("key derivation"); return 0; } int devgroup_schema(sqlite3* db) { if (!db) return dg_error("schema database"); const char* names[] = {"devgroup_ed", "devgroup_x", "devgroup_member_sig", "devgroup_json", "devgroup_profile_sig"}; for (unsigned i = 0; i < 5; i++) { sqlite3_stmt* st = NULL; int rc = sqlite3_prepare_v2(db, "SELECT count(*) FROM pragma_table_info('nodes') WHERE name=?", -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, names[i], -1, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_step(st); int exists = rc == SQLITE_ROW && sqlite3_column_int(st, 0); if (dg_finish(db, st, rc, SQLITE_ROW)) return -1; if (!exists) { char sql[128]; snprintf(sql, sizeof(sql), "ALTER TABLE nodes ADD COLUMN %s %s", names[i], i == 3 ? "TEXT" : "BLOB"); if (dg_sql(db, sql)) return -1; } } return dg_sql(db, "CREATE INDEX IF NOT EXISTS nodes_devgroup ON nodes(devgroup_ed);" "CREATE TABLE IF NOT EXISTS devgroup_local(node_id INTEGER PRIMARY KEY,ed_priv BLOB NOT NULL,x_priv BLOB NOT NULL,record BLOB NOT NULL);" "CREATE TABLE IF NOT EXISTS devgroup_receipts(node_id INTEGER NOT NULL,id BLOB NOT NULL,peer INTEGER NOT NULL,PRIMARY KEY(node_id,id));"); } int devgroup_project(sqlite3* db, const struct TOPO_NODE* node) { const struct devgroup_record* r = &node->devgroup; sqlite3_stmt* st = NULL; const char* sql = "INSERT INTO nodes(node_id,x25519_pubkey,ed25519_pubkey,devgroup_ed,devgroup_x,devgroup_member_sig,devgroup_json,devgroup_profile_sig)" " VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(node_id) DO UPDATE SET devgroup_ed=excluded.devgroup_ed,devgroup_x=excluded.devgroup_x," "devgroup_member_sig=excluded.devgroup_member_sig,devgroup_json=excluded.devgroup_json,devgroup_profile_sig=excluded.devgroup_profile_sig"; int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node->node_id); if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 2, node->public_key, 32, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, node->ed25519_public_key, 32, SQLITE_STATIC); const void* blobs[] = {r->ed, r->x, r->member_sig, r->profile_sig}; const int sizes[] = {32,32,64,64}; for (unsigned i = 0; i < 4 && rc == SQLITE_OK; i++) { int col = i == 3 ? 8 : 4 + (int)i; rc = r->json[0] ? sqlite3_bind_blob(st, col, blobs[i], sizes[i], SQLITE_STATIC) : sqlite3_bind_null(st, col); } if (rc == SQLITE_OK) rc = r->json[0] ? sqlite3_bind_text(st, 7, r->json, -1, SQLITE_STATIC) : sqlite3_bind_null(st, 7); if (rc == SQLITE_OK) rc = sqlite3_step(st); if (rc == SQLITE_DONE && sqlite3_changes(db) != 1) rc = SQLITE_CONSTRAINT; return dg_finish(db, st, rc, SQLITE_DONE); } int devgroup_load_local(struct UTUN_INSTANCE* inst, struct devgroup_record* out, uint8_t ep[32], uint8_t xp[32]) { sqlite3* db = inst ? inst->topo_sqlite_db : NULL; if (!db || !out || !ep || !xp) return dg_error("load arguments"); sqlite3_stmt* st = NULL; struct devgroup_record r = {0}; uint8_t e[32] = {0}, x[32] = {0}; int found = 0; int rc = sqlite3_prepare_v2(db, "SELECT ed_priv,x_priv,record FROM devgroup_local WHERE node_id=?", -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); if (rc == SQLITE_OK) rc = sqlite3_step(st); if (rc == SQLITE_ROW) { if (sqlite3_column_bytes(st, 0) != 32 || sqlite3_column_bytes(st, 1) != 32 || devgroup_decode(&r, sqlite3_column_blob(st, 2), sqlite3_column_bytes(st, 2)) || !r.json[0]) rc = SQLITE_CORRUPT; else { memcpy(e, sqlite3_column_blob(st, 0), 32); memcpy(x, sqlite3_column_blob(st, 1), 32); found = 1; rc = SQLITE_DONE; } } int error = dg_finish(db, st, rc, SQLITE_DONE); if (!error && found) { uint8_t ed[32], pub[32]; if (devgroup_public_keys(e, x, ed, pub) || memcmp(ed, r.ed, 32) || memcmp(pub, r.x, 32) || devgroup_verify(&r, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey)) error = dg_error("stored identity"); } if (!error) { *out = r; memcpy(ep, e, 32); memcpy(xp, x, 32); } OPENSSL_cleanse(e, 32); OPENSSL_cleanse(x, 32); return error ? -1 : found; } int devgroup_latest(sqlite3* db, const uint8_t id[32], struct devgroup_record* out, struct devgroup_profile* profile) { if (!db || !id || !out || !profile) return dg_error("latest arguments"); sqlite3_stmt* st = NULL; struct devgroup_record best = {0}; struct devgroup_profile bp = {0}; int found = 0; int rc = sqlite3_prepare_v2(db, "SELECT devgroup_x,devgroup_member_sig,devgroup_json,devgroup_profile_sig," "node_id,x25519_pubkey,ed25519_pubkey FROM nodes WHERE devgroup_ed=?", -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 1, id, 32, SQLITE_STATIC); if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { struct devgroup_record r = {0}; struct devgroup_profile p; int n = sqlite3_column_bytes(st, 2); const char* json = (const char*)sqlite3_column_text(st, 2); if (sqlite3_column_bytes(st, 0) != 32 || sqlite3_column_bytes(st, 1) != 64 || sqlite3_column_bytes(st, 3) != 64 || sqlite3_column_bytes(st, 5) != 32 || sqlite3_column_bytes(st, 6) != 32 || !json || n <= 0 || n > DEVGROUP_PROFILE_MAX || memchr(json, 0, n)) { rc = SQLITE_CORRUPT; break; } memcpy(r.ed, id, 32); memcpy(r.x, sqlite3_column_blob(st, 0), 32); memcpy(r.member_sig, sqlite3_column_blob(st, 1), 64); memcpy(r.json, json, n); memcpy(r.profile_sig, sqlite3_column_blob(st, 3), 64); if (devgroup_verify(&r, (uint64_t)sqlite3_column_int64(st, 4), sqlite3_column_blob(st, 5), sqlite3_column_blob(st, 6)) || devgroup_profile_read(&r, &p)) { rc = SQLITE_CORRUPT; break; } if (!found || devgroup_profile_compare(&p, &bp) > 0) { best = r; bp = p; found = 1; } } if (dg_finish(db, st, rc, SQLITE_DONE)) return -1; *out = best; *profile = bp; return found; } int devgroup_member(sqlite3* db, uint64_t node, uint8_t id[32], char name[64]) { sqlite3_stmt* st = NULL; struct devgroup_record r = {0}, best; struct devgroup_profile profile; int rc = sqlite3_prepare_v2(db, "SELECT devgroup_ed,devgroup_x,devgroup_member_sig,devgroup_json,devgroup_profile_sig," "x25519_pubkey,ed25519_pubkey FROM nodes WHERE node_id=? AND devgroup_ed IS NOT NULL", -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node); if (rc == SQLITE_OK) rc = sqlite3_step(st); int found = rc == SQLITE_ROW; if (found) { int n = sqlite3_column_bytes(st, 3); const char* json = (const char*)sqlite3_column_text(st, 3); if (sqlite3_column_bytes(st, 0) != 32 || sqlite3_column_bytes(st, 1) != 32 || sqlite3_column_bytes(st, 2) != 64 || sqlite3_column_bytes(st, 4) != 64 || sqlite3_column_bytes(st, 5) != 32 || sqlite3_column_bytes(st, 6) != 32 || !json || n <= 0 || n > DEVGROUP_PROFILE_MAX || memchr(json, 0, n)) rc = SQLITE_CORRUPT; else { memcpy(r.ed, sqlite3_column_blob(st, 0), 32); memcpy(r.x, sqlite3_column_blob(st, 1), 32); memcpy(r.member_sig, sqlite3_column_blob(st, 2), 64); memcpy(r.json, json, n); memcpy(r.profile_sig, sqlite3_column_blob(st, 4), 64); rc = devgroup_verify(&r, node, sqlite3_column_blob(st, 5), sqlite3_column_blob(st, 6)) ? SQLITE_CORRUPT : SQLITE_DONE; } } if (dg_finish(db, st, rc, SQLITE_DONE)) return -1; memset(id, 0, 32); memset(name, 0, 64); if (!found) return 0; if (devgroup_latest(db, r.ed, &best, &profile) != 1) return dg_error("member profile unavailable"); memcpy(id, r.ed, 32); memcpy(name, profile.name, 64); return 1; }