test(chat): cover channel input contracts and TCP creation result
This commit is contained in:
1 parent
f1cada4c02
commit
8003413d97
2 files changed
+122
-13
No files matched your search
+11
-9
@@ -117,15 +117,15 @@ int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, u
|
||||
// ── Создание канала (GUI → uasync) ──
|
||||
|
||||
struct chat_channel_create {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel_id[64];
|
||||
char name[128];
|
||||
uint64_t owner_node_id;
|
||||
uint8_t x25519_pubkey[32];
|
||||
uint8_t x25519_privkey[32];
|
||||
uint8_t ed25519_pubkey[32];
|
||||
uint8_t ed25519_privkey[32];
|
||||
uint8_t signature[64];
|
||||
struct UTUN_INSTANCE* inst; /* Заимствован trampoline; прямой вызов использует свой аргумент inst. */
|
||||
char channel_id[64]; /* NUL-строка: каноническое десятичное число 1..INT64_MAX. */
|
||||
char name[128]; /* NUL-строка до127 байт, без молчаливого обрезания. */
|
||||
uint64_t owner_node_id; /* node_id создающего instance. */
|
||||
uint8_t x25519_pubkey[32]; /* Канальный ключ шифрования; должен соответствовать privkey. */
|
||||
uint8_t x25519_privkey[32];/* Независимый приватный X25519-ключ канала. */
|
||||
uint8_t ed25519_pubkey[32];/* Канальный ключ подписи; должен соответствовать raw seed. */
|
||||
uint8_t ed25519_privkey[32];/* Raw Ed25519 seed, без преобразования sc_derive_ed25519_pubkey. */
|
||||
uint8_t signature[64]; /* Подпись id\0 || name\0 || owner(native u64) || X25519 || Ed25519. */
|
||||
};
|
||||
|
||||
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
|
||||
@@ -141,6 +141,7 @@ int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id
|
||||
* с открытой транзакцией запрашивает stop/закрытие БД владельцем вне callback. */
|
||||
int chat_core_create_channel(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req);
|
||||
|
||||
// Забирает u_malloc-структуру chat_channel_create, очищает ключи и освобождает её после вызова.
|
||||
void chat_core_create_channel_trampoline(void* arg);
|
||||
|
||||
/* Сгенерировать независимые пары X25519/Ed25519 и создать канал. ID>0 — сохранён,0 — отказ.
|
||||
@@ -148,6 +149,7 @@ void chat_core_create_channel_trampoline(void* arg);
|
||||
* временные приватные ключи очищаются. Внешняя транзакция отклоняется до генерации. */
|
||||
uint64_t chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
// Забирает и освобождает u_malloc-структуру chat_create_auto_arg; instance должен дожить до выполнения.
|
||||
void chat_core_create_channel_auto_trampoline(void* arg);
|
||||
|
||||
// ── Локальное удаление канала (GUI → uasync) ──
|
||||
|
||||
+111
-4
@@ -8,10 +8,12 @@
|
||||
#include "../src/chat/chat_core.h"
|
||||
#include "../src/chat/chat_core_priv.h"
|
||||
#include "../src/chat/chat_event.h"
|
||||
#include "../src/chat/chat_headless_control.h"
|
||||
#include "../src/chat/member_sync.h"
|
||||
#include "../src/routing_layer/topo_node_sqlite.h"
|
||||
#include "../src/routing_layer/topo_group.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
#include "../lib/socket_compat.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
|
||||
@@ -172,13 +174,13 @@ static void snapshot(sqlite3* db, uint8_t out[32]) {
|
||||
|
||||
extern int __real_EVP_DigestSign(EVP_MD_CTX*, unsigned char*, size_t*, const unsigned char*, size_t);
|
||||
int __wrap_EVP_DigestSign(EVP_MD_CTX* ctx, unsigned char* signature, size_t* length, const unsigned char* data, size_t size) {
|
||||
if (fail_sign_number && active && size == 112 && !memcmp(data + 72, active->my_keys.public_key, 32) && !--fail_sign_number) {
|
||||
if (data && fail_sign_number && active && size == 112 && !memcmp(data + 72, active->my_keys.public_key, 32) && !--fail_sign_number) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "channel test: injected signature failure bytes=%zu", size); return 0;
|
||||
}
|
||||
if (active && size == MEMBER_SIGN_MSG_SIZE && !memcmp(data, active->my_keys.public_key, 32) &&
|
||||
if (data && active && size == MEMBER_SIGN_MSG_SIZE && !memcmp(data, active->my_keys.public_key, 32) &&
|
||||
!memcmp(data + 32, MEMBER_SIGN_SUFFIX, MEMBER_SIGN_SUFFIX_LEN) && take_crypto(K_INVITATION)) return 0;
|
||||
if (size >= 80 && !memcmp(data + 72, "{\"name\":", 8) && take_crypto(K_PROFILE)) return 0;
|
||||
const uint8_t* zero = memchr(data, 0, size);
|
||||
if (data && size >= 80 && !memcmp(data + 72, "{\"name\":", 8) && take_crypto(K_PROFILE)) return 0;
|
||||
const uint8_t* zero = data ? memchr(data, 0, size) : NULL;
|
||||
if (zero && zero - data < 20 && (size_t)(zero - data + 1) + sizeof("crypto refusal") <= size &&
|
||||
!memcmp(zero + 1, "crypto refusal", sizeof("crypto refusal")) && take_crypto(K_DESCRIPTION)) return 0;
|
||||
return __real_EVP_DigestSign(ctx, signature, length, data, size);
|
||||
@@ -236,6 +238,108 @@ static void retry(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
static int veto(void* arg) { (*(unsigned*)arg)++; return 1; }
|
||||
|
||||
static void descriptor_regressions(void) {
|
||||
uint8_t x[32] = {1}, ed[32] = {2}, out[CHAT_CHANNEL_DESCRIPTION_MAX], expected[CHAT_CHANNEL_DESCRIPTION_MAX];
|
||||
const char* id = "9223372036854775807"; char name[257]; memset(name, 'n', 255); name[255] = 0;
|
||||
uint64_t owner = UINT64_C(0x0123456789abcdef); size_t offset = 0;
|
||||
memcpy(expected, id, 20); offset += 20; memcpy(expected + offset, name, 256); offset += 256;
|
||||
memcpy(expected + offset, &owner, 8); offset += 8;
|
||||
memcpy(expected + offset, x, 32); offset += 32; memcpy(expected + offset, ed, 32); offset += 32;
|
||||
assert(offset == sizeof(out) && chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out)) == (int)offset);
|
||||
assert(!memcmp(out, expected, sizeof(out))); memset(out, 0xa5, sizeof(out)); memset(expected, 0xa5, sizeof(expected));
|
||||
assert(chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out) - 1) < 0 && !memcmp(out, expected, sizeof(out)));
|
||||
name[255] = 'n'; name[256] = 0;
|
||||
assert(chat_channel_description_data(id, name, owner, x, ed, out, sizeof(out)) < 0 && !memcmp(out, expected, sizeof(out)));
|
||||
const char* invalid[] = {"", "0", "01", "-1", "1x", " 1", "9223372036854775808", "18446744073709551615", "12345678901234567890"};
|
||||
for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
|
||||
assert(chat_channel_description_data(invalid[i], "", owner, x, ed, out, sizeof(out)) < 0);
|
||||
assert(!memcmp(out, expected, sizeof(out)));
|
||||
}
|
||||
assert(chat_channel_description_data("1", "", owner, x, ed, out, 75) == 75);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel description: canonical ID range,255-byte name,capacity,unchanged error output,exact shared wire");
|
||||
}
|
||||
|
||||
static void direct_refused(struct UTUN_INSTANCE* inst, const struct chat_channel_create* req) {
|
||||
uint8_t before[32], after[32]; snapshot(reader, before);
|
||||
unsigned events = channel_events; int groups = queue_entry_count(inst->topo_groups->group_list);
|
||||
assert(chat_core_create_channel(inst, req) < 0 && sqlite3_get_autocommit(inst->topo_sqlite_db));
|
||||
snapshot(reader, after); assert(!memcmp(before, after, 32) && events == channel_events);
|
||||
assert(groups == queue_entry_count(inst->topo_groups->group_list));
|
||||
}
|
||||
|
||||
static void direct_regressions(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_channel_create original = {.inst = inst, .owner_node_id = inst->node_id}, request;
|
||||
sqlite3_stmt* st = NULL;
|
||||
assert(sqlite3_prepare_v2(reader, "SELECT channel_id,name,x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature "
|
||||
"FROM channels ORDER BY channel_id LIMIT 1", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
||||
snprintf(original.channel_id, sizeof(original.channel_id), "%s", sqlite3_column_text(st, 0));
|
||||
snprintf(original.name, sizeof(original.name), "%s", sqlite3_column_text(st, 1));
|
||||
void* keys[] = {original.x25519_pubkey, original.x25519_privkey, original.ed25519_pubkey, original.ed25519_privkey, original.signature};
|
||||
for (unsigned i = 0; i < 5; i++) {
|
||||
int bytes = i == 4 ? 64 : 32; assert(sqlite3_column_bytes(st, (int)i + 2) == bytes);
|
||||
memcpy(keys[i], sqlite3_column_blob(st, (int)i + 2), (size_t)bytes);
|
||||
}
|
||||
assert(sqlite3_finalize(st) == SQLITE_OK); direct_refused(inst, &original); direct_refused(inst, NULL);
|
||||
request = original; request.owner_node_id ^= 1; direct_refused(inst, &request);
|
||||
request = original; request.x25519_privkey[8] ^= 1; direct_refused(inst, &request);
|
||||
request = original; request.ed25519_privkey[8] ^= 1; direct_refused(inst, &request);
|
||||
request = original; request.signature[0] ^= 1; direct_refused(inst, &request);
|
||||
request = original; memset(request.name, 'n', sizeof(request.name)); direct_refused(inst, &request);
|
||||
request = original; memset(request.channel_id, '1', sizeof(request.channel_id)); direct_refused(inst, &request);
|
||||
const char* invalid[] = {"0", "01", "9223372036854775808", "1x"};
|
||||
for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
|
||||
request = original; snprintf(request.channel_id, sizeof(request.channel_id), "%s", invalid[i]); direct_refused(inst, &request);
|
||||
}
|
||||
assert(sqlite3_exec(inst->topo_sqlite_db, "BEGIN", NULL, NULL, NULL) == SQLITE_OK);
|
||||
assert(chat_core_create_channel(inst, &original) < 0 && !sqlite3_get_autocommit(inst->topo_sqlite_db));
|
||||
assert(sqlite3_exec(inst->topo_sqlite_db, "ROLLBACK", NULL, NULL, NULL) == SQLITE_OK);
|
||||
char name[129]; memset(name, 'n', 128); name[128] = 0; refused(inst, name);
|
||||
request = original; snprintf(request.channel_id, sizeof(request.channel_id), "9223372036854775807");
|
||||
memset(request.name, 'n', 127); request.name[127] = 0; uint8_t description[CHAT_CHANNEL_DESCRIPTION_MAX];
|
||||
int bytes = chat_channel_description_data(request.channel_id, request.name, request.owner_node_id,
|
||||
request.x25519_pubkey, request.ed25519_pubkey, description, sizeof(description));
|
||||
assert(bytes > 0 && sc_ed25519_sign(request.ed25519_privkey, description, (size_t)bytes, request.signature) == SC_OK);
|
||||
int count = channels(); unsigned events = channel_events;
|
||||
assert(!chat_core_create_channel(inst, &request) && channels() == count + 1 && channel_events == events + 1);
|
||||
direct_refused(inst, &request); OPENSSL_cleanse(&original, sizeof(original)); OPENSSL_cleanse(&request, sizeof(request));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel API: malformed/occupied IDs,owner/key/signature/name checks,foreign transaction;127-byte name/max-ID commit");
|
||||
}
|
||||
|
||||
static void rpc_request(struct UASYNC* ua, socket_t fd, const char* request, int id, int success) {
|
||||
assert(send(fd, request, (int)strlen(request), 0) == (ssize_t)strlen(request));
|
||||
char response[1024] = {0}; size_t length = 0;
|
||||
for (int attempt = 0; attempt < 2500 && !strchr(response, '\n'); attempt++) {
|
||||
uasync_poll(ua, 20); ssize_t received = recv(fd, response + length, sizeof(response) - length - 1, 0);
|
||||
if (received < 0) { int error = socket_get_error(); assert(error == ERR_AGAIN || error == ERR_WOULDBLOCK || error == ERR_INTR); continue; }
|
||||
assert(received > 0 && length + (size_t)received < sizeof(response) - 1);
|
||||
length += (size_t)received; response[length] = 0;
|
||||
}
|
||||
assert(strchr(response, '\n')); sqlite3_stmt* st = NULL;
|
||||
assert(sqlite3_prepare_v2(reader, "SELECT json_extract(?1,'$.id'),json_extract(?1,'$.ok'),"
|
||||
"json_extract(?1,'$.data.created'),json_extract(?1,'$.data.channel_id'),json_extract(?1,'$.error')", -1, &st, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_bind_text(st, 1, response, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
||||
assert(sqlite3_column_int(st, 0) == id && sqlite3_column_int(st, 1) == success);
|
||||
if (success) {
|
||||
assert(sqlite3_column_int(st, 2) == 1 && sqlite3_column_type(st, 3) == SQLITE_TEXT);
|
||||
assert(strtoull((const char*)sqlite3_column_text(st, 3), NULL, 10) > 0);
|
||||
} else assert(sqlite3_column_type(st, 4) == SQLITE_TEXT);
|
||||
assert(sqlite3_finalize(st) == SQLITE_OK);
|
||||
}
|
||||
|
||||
static void headless_regressions(struct UTUN_INSTANCE* inst, struct UASYNC* ua) {
|
||||
assert(!chat_headless_control_init(ua, inst, "127.0.0.1", 0));
|
||||
socket_t fd = socket(AF_INET, SOCK_STREAM, 0); assert(fd != SOCKET_INVALID);
|
||||
struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons(chat_headless_control_get_port(inst))};
|
||||
assert(inet_pton(AF_INET, "127.0.0.1", &address.sin_addr) == 1 && !connect(fd, (struct sockaddr*)&address, sizeof(address)));
|
||||
assert(!socket_set_nonblocking(fd)); int count = channels();
|
||||
sql_prefix = "BEGIN IMMEDIATE /* channel creation */"; sql_suffix = NULL; sql_fault = Q_EXEC;
|
||||
rpc_request(ua, fd, "{\"id\":17,\"cmd\":\"create_channel\",\"name\":\"RPC failed\"}\n", 17, 0);
|
||||
assert(sql_fault == Q_NONE && channels() == count);
|
||||
rpc_request(ua, fd, "{\"id\":18,\"cmd\":\"create_channel\",\"name\":\"RPC created\"}\n", 18, 1);
|
||||
assert(channels() == count + 1); socket_close_wrapper(fd); chat_headless_control_destroy(inst); chat_event_set_handler(inst, event);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS channel TCP API: failed commit path returns ok:false;retry returns committed channel_id");
|
||||
}
|
||||
|
||||
static void crypto_regressions(struct UTUN_INSTANCE* inst) {
|
||||
const int types[] = {EVP_PKEY_X25519, EVP_PKEY_ED25519}; unsigned cases = 0;
|
||||
for (unsigned type = 0; type < 2; type++) {
|
||||
@@ -320,8 +424,11 @@ int main(void) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "channel test: profile bind failure ID=%llu channels=%d events=%u",
|
||||
(unsigned long long)created, channels(), channel_events);
|
||||
assert(!fail_profile_binding && !created && channels() == 1 && channel_events == 1);
|
||||
descriptor_regressions();
|
||||
direct_regressions(inst);
|
||||
crypto_regressions(inst);
|
||||
sql_regressions(inst);
|
||||
headless_regressions(inst, ua);
|
||||
int committed = channels();
|
||||
uint8_t before[32], after[32]; snapshot(reader, before); unsigned events = channel_events;
|
||||
sql_prefix = "COMMIT /* channel creation */"; sql_suffix = NULL; sql_fault = Q_EXEC; failed_rollback = 1; inst->running = 1;
|
||||
|
||||
Reference in new issue
Block a user