294 lines
18 KiB
C
294 lines
18 KiB
C
#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 <sqlite3.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/sha.h>
|
|
#include <openssl/crypto.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
|
|
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;
|
|
}
|