218 lines
13 KiB
C
218 lines
13 KiB
C
/* Настоящие core/chat/router callback/SQLite/crypto: атомарность импорта и момент события успеха. */
|
|
#include "utun_instance.h"
|
|
#include "chat/chat_core.h"
|
|
#include "chat/chat_event.h"
|
|
#include "chat/chat_admin.h"
|
|
#include "routing_layer/topo_node_sqlite.h"
|
|
#include "routing_layer/etcp_router.h"
|
|
#include "transport_layer/etcp.h"
|
|
#include "transport_layer/secure_channel.h"
|
|
#include "../lib/platform_compat.h"
|
|
#include "../lib/debug_config.h"
|
|
#include "../lib/mem.h"
|
|
#include <assert.h>
|
|
#include <openssl/evp.h>
|
|
|
|
enum fault { NONE, SAVEPOINT, PREPARE, BIND_X, BIND_ED, BIND_CHANNEL, STEP, FINALIZE, RELEASE, ED_CRYPTO, X_CRYPTO, FAULTS };
|
|
static enum fault fault;
|
|
static unsigned hits[FAULTS], rollback_hits;
|
|
static int fail_rollback;
|
|
static const char* import_sql = "UPDATE channels SET x25519_privkey=?, ed25519_privkey=? WHERE channel_id=?";
|
|
|
|
static int take(enum fault target) {
|
|
if (fault != target) return 0;
|
|
fault = NONE; hits[target]++;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "admin test: injected fault=%u", target); return 1;
|
|
}
|
|
static int is_import(sqlite3_stmt* st) { return st && !strcmp(sqlite3_sql(st), import_sql); }
|
|
|
|
extern int __real_sqlite3_exec(sqlite3*, const char*, int (*)(void*, int, char**, char**), void*, char**);
|
|
int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char**, char**), void* arg, char** error) {
|
|
if (!strcmp(sql, "SAVEPOINT channel_private_keys") && take(SAVEPOINT)) return SQLITE_NOMEM;
|
|
if (!strcmp(sql, "RELEASE channel_private_keys") && take(RELEASE)) return SQLITE_BUSY;
|
|
if (fail_rollback && strstr(sql, "ROLLBACK TO channel_private_keys")) {
|
|
fail_rollback = 0; rollback_hits++;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "admin test: injected rollback failure"); return SQLITE_IOERR;
|
|
}
|
|
return __real_sqlite3_exec(db, sql, cb, arg, error);
|
|
}
|
|
extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**);
|
|
int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int len, sqlite3_stmt** st, const char** tail) {
|
|
if (!strcmp(sql, import_sql) && take(PREPARE)) { *st = NULL; return SQLITE_IOERR; }
|
|
return __real_sqlite3_prepare_v2(db, sql, len, st, tail);
|
|
}
|
|
extern int __real_sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int, void (*)(void*));
|
|
int __wrap_sqlite3_bind_blob(sqlite3_stmt* st, int index, const void* data, int len, void (*destroy)(void*)) {
|
|
if (is_import(st) && ((index == 1 && take(BIND_X)) || (index == 2 && take(BIND_ED)))) return SQLITE_NOMEM;
|
|
return __real_sqlite3_bind_blob(st, index, data, len, destroy);
|
|
}
|
|
extern int __real_sqlite3_bind_text(sqlite3_stmt*, int, const char*, int, void (*)(void*));
|
|
int __wrap_sqlite3_bind_text(sqlite3_stmt* st, int index, const char* data, int len, void (*destroy)(void*)) {
|
|
if (is_import(st) && index == 3 && take(BIND_CHANNEL)) return SQLITE_NOMEM;
|
|
return __real_sqlite3_bind_text(st, index, data, len, destroy);
|
|
}
|
|
extern int __real_sqlite3_step(sqlite3_stmt*);
|
|
int __wrap_sqlite3_step(sqlite3_stmt* st) {
|
|
if (is_import(st) && take(STEP)) return SQLITE_IOERR;
|
|
return __real_sqlite3_step(st);
|
|
}
|
|
extern int __real_sqlite3_finalize(sqlite3_stmt*);
|
|
int __wrap_sqlite3_finalize(sqlite3_stmt* st) {
|
|
int fail = is_import(st) && take(FINALIZE);
|
|
int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc;
|
|
}
|
|
extern EVP_PKEY* __real_EVP_PKEY_new_raw_private_key(int, ENGINE*, const unsigned char*, size_t);
|
|
EVP_PKEY* __wrap_EVP_PKEY_new_raw_private_key(int type, ENGINE* engine, const unsigned char* data, size_t len) {
|
|
if ((type == EVP_PKEY_ED25519 && take(ED_CRYPTO)) || (type == EVP_PKEY_X25519 && take(X_CRYPTO))) return NULL;
|
|
return __real_EVP_PKEY_new_raw_private_key(type, engine, data, len);
|
|
}
|
|
|
|
struct fixture {
|
|
struct UASYNC* ua;
|
|
struct UTUN_INSTANCE* inst;
|
|
struct SC_MYKEYS channel;
|
|
uint8_t ed[32], public_ed[32], other[32];
|
|
unsigned events;
|
|
};
|
|
static struct fixture* active;
|
|
|
|
static void sql(struct fixture* f, const char* query) {
|
|
int rc = sqlite3_exec(f->inst->topo_sqlite_db, query, NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "admin test SQL rc=%d: %s", rc, sqlite3_errmsg(f->inst->topo_sqlite_db));
|
|
assert(rc == SQLITE_OK);
|
|
}
|
|
/* Независимая проверка обеих колонок; NULL означает ожидаемое отсутствие приватного ключа. */
|
|
static void keys(struct fixture* f, const char* channel, const uint8_t* x, const uint8_t* ed) {
|
|
sqlite3_stmt* st = NULL; sqlite3* db = f->inst->topo_sqlite_db;
|
|
assert(sqlite3_prepare_v2(db, "SELECT x25519_privkey,ed25519_privkey FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK);
|
|
assert(sqlite3_bind_text(st, 1, channel, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
|
const uint8_t* expected[] = {x, ed};
|
|
for (int i = 0; i < 2; i++) {
|
|
if (expected[i]) assert(sqlite3_column_bytes(st, i) == 32 && !memcmp(sqlite3_column_blob(st, i), expected[i], 32));
|
|
else assert(sqlite3_column_type(st, i) == SQLITE_NULL);
|
|
}
|
|
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
|
|
}
|
|
static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
|
if (type != CHAT_EVT_ADMIN_KEY_RECEIVED) return;
|
|
struct fixture* f = active;
|
|
assert(f && inst == f->inst && sqlite3_get_autocommit(inst->topo_sqlite_db));
|
|
assert(len == 12 && data[0] == 3 && !memcmp(data + 1, "901", 3));
|
|
uint64_t source; memcpy(&source, data + 4, 8); assert(source == 123);
|
|
keys(f, "901", f->channel.private_key, f->ed); f->events++;
|
|
}
|
|
static void setup(struct fixture* f) {
|
|
memset(f, 0, sizeof(*f)); f->ua = uasync_create(); assert(f->ua);
|
|
f->inst = utun_instance_create_from_str(f->ua,
|
|
"[global]\nmy_node_name=admin test\n[server:udp]\naddr=127.0.0.1:0\ntype=public\n"
|
|
"[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=0\n");
|
|
assert(f->inst && utun_core_start(f->inst) == 0 && chat_service_start(f->inst) == 0);
|
|
assert(sc_generate_keypair(&f->channel) == SC_OK); memset(f->ed, 7, 32); memset(f->other, 5, 32);
|
|
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, f->ed, 32); assert(key);
|
|
size_t len = 32; assert(EVP_PKEY_get_raw_public_key(key, f->public_ed, &len) == 1 && len == 32); EVP_PKEY_free(key);
|
|
uint8_t signature[64] = {0};
|
|
assert(topo_node_sqlite_channel_put(f->inst->topo_sqlite_db, "901", "admin", f->inst->node_id,
|
|
f->channel.public_key, NULL, f->public_ed, NULL, signature) == 0);
|
|
assert(topo_node_sqlite_channel_put(f->inst->topo_sqlite_db, "902", "other", f->inst->node_id,
|
|
f->channel.public_key, f->other, f->public_ed, f->other, signature) == 0);
|
|
assert(chat_core_ensure_channel_ready(f->inst, "901") == 0 && chat_core_ensure_channel_ready(f->inst, "902") == 0);
|
|
active = f; chat_event_set_handler(f->inst, event);
|
|
}
|
|
static void cleanup(struct fixture* f) {
|
|
assert(fault == NONE && !fail_rollback && sqlite3_get_autocommit(f->inst->topo_sqlite_db));
|
|
keys(f, "902", f->other, f->other);
|
|
chat_event_set_handler(f->inst, NULL); active = NULL;
|
|
utun_instance_destroy(f->inst); uasync_destroy(f->ua, 0);
|
|
}
|
|
/* Доставка собственной расшифрованной записи через callback; сетевой путь и шифрование проверяются отдельно. */
|
|
static void receive(struct fixture* f, uint64_t group, const uint8_t ed[32], const uint8_t x[32]) {
|
|
struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_HDR_SIZE + 72); assert(e);
|
|
e->len = ROUTER_SVC_HDR_SIZE + 72; memset(e->dgram, 0, e->len);
|
|
uint64_t source = 123;
|
|
e->dgram[0] = ETCP_RT_ID_ADMIN_KEY;
|
|
e->dgram[ROUTER_SVC_FLAGS_OFF] = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED;
|
|
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &source, 8);
|
|
memcpy(e->dgram + ROUTER_SVC_DST_OFF, &f->inst->node_id, 8);
|
|
memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8);
|
|
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF, &group, 8);
|
|
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 8, ed, 32);
|
|
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 40, x, 32);
|
|
struct ETCP_CONN conn = {.instance = f->inst};
|
|
assert(f->inst->router_bindings.callbacks[ETCP_RT_ID_ADMIN_KEY]);
|
|
f->inst->router_bindings.callbacks[ETCP_RT_ID_ADMIN_KEY](&conn, e);
|
|
}
|
|
static void test_faults(void) {
|
|
struct fixture f; setup(&f);
|
|
for (int existing = 0; existing < 2; existing++) for (enum fault error = SAVEPOINT; error < FAULTS; error++) {
|
|
if (existing) assert(topo_node_sqlite_channel_set_privs(f.inst->topo_sqlite_db, "901", f.channel.private_key, f.ed) == 0);
|
|
else sql(&f, "UPDATE channels SET x25519_privkey=NULL,ed25519_privkey=NULL WHERE channel_id='901'");
|
|
unsigned before = f.events; fault = error;
|
|
receive(&f, 901, f.ed, f.channel.private_key);
|
|
assert(fault == NONE && f.events == before && sqlite3_get_autocommit(f.inst->topo_sqlite_db));
|
|
keys(&f, "901", existing ? f.channel.private_key : NULL, existing ? f.ed : NULL);
|
|
keys(&f, "902", f.other, f.other);
|
|
receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == before + 1);
|
|
}
|
|
cleanup(&f);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: every SQL/crypto failure preserves absent/existing keys; retry confirms only committed pair");
|
|
}
|
|
static int refuse_commit(void* arg) { (*(unsigned*)arg)++; return 1; }
|
|
static void test_sql_and_replay(void) {
|
|
struct fixture f; setup(&f); sqlite3* db = f.inst->topo_sqlite_db;
|
|
unsigned commits = 0; sqlite3_commit_hook(db, refuse_commit, &commits);
|
|
receive(&f, 901, f.ed, f.channel.private_key);
|
|
sqlite3_commit_hook(db, NULL, NULL);
|
|
assert(commits == 1 && !f.events && sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL);
|
|
const char* triggers[] = {
|
|
"CREATE TRIGGER deny_admin BEFORE UPDATE ON channels BEGIN SELECT RAISE(ABORT,'admin denied');END",
|
|
"CREATE TRIGGER deny_admin BEFORE UPDATE ON channels BEGIN SELECT RAISE(IGNORE);END"
|
|
};
|
|
for (unsigned i = 0; i < 2; i++) {
|
|
sql(&f, triggers[i]); receive(&f, 901, f.ed, f.channel.private_key);
|
|
assert(!f.events && sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL); sql(&f, "DROP TRIGGER deny_admin");
|
|
}
|
|
assert(topo_node_sqlite_channel_set_privs(db, "903", f.channel.private_key, f.ed) < 0 && sqlite3_get_autocommit(db));
|
|
receive(&f, 903, f.ed, f.channel.private_key); assert(!f.events);
|
|
uint8_t bad[32] = {1};
|
|
receive(&f, 901, bad, f.channel.private_key); receive(&f, 901, f.ed, bad);
|
|
assert(!f.events); keys(&f, "901", NULL, NULL);
|
|
receive(&f, 901, f.ed, f.channel.private_key); receive(&f, 901, f.ed, f.channel.private_key);
|
|
assert(f.events == 2); cleanup(&f);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: actual commit veto, UPDATE error/skip, missing channel, wrong keys and duplicate delivery");
|
|
}
|
|
static void test_outer_transaction(void) {
|
|
struct fixture f; setup(&f); sqlite3* db = f.inst->topo_sqlite_db;
|
|
sql(&f, "BEGIN;UPDATE channels SET name='outer' WHERE channel_id='902'");
|
|
receive(&f, 901, f.ed, f.channel.private_key); assert(!f.events && !sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL);
|
|
fault = FINALIZE;
|
|
assert(topo_node_sqlite_channel_set_privs(db, "901", f.channel.private_key, f.ed) < 0 && fault == NONE);
|
|
assert(!sqlite3_get_autocommit(db)); keys(&f, "901", NULL, NULL);
|
|
char name[32];
|
|
assert(topo_node_sqlite_channel_get(db, "902", name, sizeof(name), NULL, NULL, NULL, NULL) == 0 && !strcmp(name, "outer"));
|
|
assert(topo_node_sqlite_channel_set_privs(db, "901", f.channel.private_key, f.ed) == 0);
|
|
assert(!sqlite3_get_autocommit(db) && !f.events); keys(&f, "901", f.channel.private_key, f.ed);
|
|
sql(&f, "ROLLBACK"); keys(&f, "901", NULL, NULL);
|
|
receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == 1); cleanup(&f);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: receiver rejects external transaction; helper preserves caller ownership and rollback");
|
|
}
|
|
static void test_rollback_failure(void) {
|
|
struct fixture f; setup(&f); int running = f.inst->running; f.inst->running = 1;
|
|
fault = RELEASE; fail_rollback = 1; receive(&f, 901, f.ed, f.channel.private_key);
|
|
assert(fault == NONE && !fail_rollback && !f.events && !f.inst->running && !sqlite3_get_autocommit(f.inst->topo_sqlite_db));
|
|
sql(&f, "ROLLBACK"); keys(&f, "901", NULL, NULL); f.inst->running = running;
|
|
receive(&f, 901, f.ed, f.channel.private_key); assert(f.events == 1); cleanup(&f);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: rollback failure never confirms import and stops the instance");
|
|
}
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
|
debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
|
|
size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0);
|
|
chat_core_transfer_admin(NULL, "901", 123);
|
|
struct UTUN_INSTANCE empty = {0}; chat_core_transfer_admin(&empty, "901", 123);
|
|
test_faults(); test_sql_and_replay(); test_outer_transaction(); test_rollback_failure();
|
|
for (unsigned i = SAVEPOINT; i < FAULTS; i++) assert(hits[i] == (i == FINALIZE || i == RELEASE ? 3 : 2));
|
|
assert(rollback_hits == 1 && u_get_allocated_count() == baseline);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS admin: NULL context, 22 SQL/crypto faults + rollback fault, allocations restored");
|
|
return 0;
|
|
}
|