You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

157 lines
11 KiB

/* Настоящие ядра/UDP/router: подпись группы, четыре блока, backpressure и restart. */
#include "../src/chat/group_avatar.c"
#include "../src/chat/member_sync.h"
#include "test_utils.h"
#include <assert.h>
#define GROUP 6767676767ULL
#define CHANNEL "6767676767"
static struct UTUN_INSTANCE* nodes[2];
static struct UASYNC* ua;
static unsigned received_blocks;
static uint64_t old_version;
static int capture_blocks;
static uint8_t group_private[32], group_public[32], group_x[32];
static void hex(const uint8_t* input, char* out) { for (unsigned i = 0; i < 32; i++) snprintf(out + i * 2, 3, "%02x", input[i]); }
static int hold(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, const uint8_t* data, size_t size) {
(void)inst; (void)group; (void)peer; (void)data; (void)size; return 0;
}
static void capture(int level, const char* category, const char* message) {
(void)level; (void)category;
if (!capture_blocks || !strstr(message, "group avatar: block received")) return;
received_blocks++;
struct group_avatar_record current; assert(!group_avatar_get(nodes[1], GROUP, &current));
assert(current.version == old_version); // Даже последний блок ещё не опубликован до проверки всей записи.
}
static void put_member(unsigned destination, unsigned member) {
uint8_t join[64], tree[64], message[512];
int n = member_sync_build_join_msg(group_x, group_public, nodes[member]->node_id,
nodes[member]->my_keys.public_key, 123, message, sizeof(message)); assert(n > 0);
assert(sc_ed25519_sign(nodes[member]->my_ed25519_privkey, message, n, join) == SC_OK);
assert(member_sync_sign_pubkey(group_private, nodes[member]->my_keys.public_key, tree) == 0);
assert(member_sync_put(nodes[destination], CHANNEL, nodes[member]->node_id, nodes[member]->my_keys.public_key,
nodes[member]->my_ed25519_pubkey, join, 123, NULL, 0, "{}", NULL, NULL, 0, 0, tree) >= 0);
}
static int wait_record(unsigned node, uint64_t version, const uint8_t hash[32]) {
uint64_t deadline = get_time_tb() + 150000;
while (get_time_tb() < deadline) {
struct group_avatar_record r;
if (!group_avatar_get(nodes[node], GROUP, &r) && r.version == version && !memcmp(r.hash, hash, 32)) return 1;
uasync_poll(ua, 2);
}
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "test group avatar: sync timeout node=%u version=%llu", node, (unsigned long long)version); return 0;
}
static void set_key(unsigned node, const uint8_t* key) {
sqlite3_stmt* st; assert(sqlite3_prepare_v2(nodes[node]->topo_sqlite_db,
"UPDATE channels SET ed25519_privkey=? WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK);
if (key) sqlite3_bind_blob(st, 1, key, 32, SQLITE_STATIC); else sqlite3_bind_null(st, 1);
sqlite3_bind_text(st, 2, CHANNEL, -1, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_DONE); sqlite3_finalize(st);
}
static int alter_calls;
static int reject_second_alter(void* arg, int action, const char* a, const char* b, const char* c, const char* d) {
(void)arg; (void)a; (void)b; (void)c; (void)d;
return action == SQLITE_ALTER_TABLE && ++alter_calls == 2 ? SQLITE_DENY : SQLITE_OK;
}
static void schema_test(void) {
sqlite3* db; assert(sqlite3_open(":memory:", &db) == SQLITE_OK);
assert(sqlite3_exec(db, "CREATE TABLE channels(channel_id TEXT PRIMARY KEY,name TEXT,ed25519_privkey BLOB);"
"INSERT INTO channels VALUES('42','retained',zeroblob(32))", NULL, NULL, NULL) == SQLITE_OK);
sqlite3_set_authorizer(db, reject_second_alter, NULL); assert(ga_schema(db) < 0); assert(sqlite3_get_autocommit(db));
sqlite3_set_authorizer(db, NULL, NULL);
sqlite3_stmt* st = NULL;
assert(sqlite3_prepare_v2(db, "SELECT avatar_version FROM channels", -1, &st, NULL) != SQLITE_OK); sqlite3_finalize(st);
assert(!ga_schema(db) && !ga_schema(db));
assert(sqlite3_prepare_v2(db, "SELECT name,length(ed25519_privkey),avatar_version FROM channels", -1, &st, NULL) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW && !strcmp((const char*)sqlite3_column_text(st, 0), "retained") &&
sqlite3_column_int(st, 1) == 32 && sqlite3_column_int(st, 2) == 0); sqlite3_finalize(st);
assert(sqlite3_close(db) == SQLITE_OK);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_CHAT, DEBUG_LEVEL_DEBUG);
schema_test();
utun_instance_set_tun_init_enabled(0); ua = uasync_create(); assert(ua);
struct SC_MYKEYS keys[2], channel;
assert(sc_generate_keypair(&channel) == SC_OK); memcpy(group_x, channel.public_key, 32);
uint8_t seed[64]; SHA512(channel.private_key, 32, seed); memcpy(group_private, seed, 32);
assert(sc_derive_ed25519_pubkey(channel.private_key, group_public) == 0);
for (unsigned i = 0; i < 2; i++) assert(sc_generate_keypair(&keys[i]) == SC_OK);
int port = 19000 + (int)(getpid() % 1000);
char directory[512] = "/tmp/utun-group-avatar-XXXXXX"; assert(!test_mkdtemp(directory));
for (unsigned i = 0; i < 2; i++) {
char pub[65], priv[65], peer[65], config[2048]; hex(keys[i].public_key, pub); hex(keys[i].private_key, priv); hex(keys[1-i].public_key, peer);
snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\ndb_path=%s/node%u\n"
"[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\nmember_files_enabled=0\n"
"[client:peer]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", priv, pub, directory, i, port + i, peer, port + 1-i);
nodes[i] = utun_instance_create_from_str(ua, config); assert(nodes[i] && !utun_instance_init(nodes[i]));
}
uint8_t signature[64] = {0};
struct TOPO_PEER_REQUEST* connections[2] = {0};
for (unsigned i = 0; i < 2; i++) {
assert(!topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, CHANNEL, "avatar", nodes[0]->node_id,
group_x, NULL, group_public, i ? NULL : group_private, signature));
chat_core_ensure_channel_ready(nodes[i], CHANNEL);
put_member(i, 0); put_member(i, 1);
assert(!topo_group_peer_open(topo_groups_find(nodes[i]->topo_groups, GROUP), nodes[1-i]->node_id, &connections[i]));
}
uint64_t deadline = get_time_tb() + 150000;
while ((topo_group_peer_phase(connections[0]) != TOPO_PEER_READY || topo_group_peer_phase(connections[1]) != TOPO_PEER_READY) &&
get_time_tb() < deadline) uasync_poll(ua, 2);
assert(topo_group_peer_phase(connections[0]) == TOPO_PEER_READY && topo_group_peer_phase(connections[1]) == TOPO_PEER_READY);
uint8_t image[GROUP_AVATAR_MAX + 1]; for (unsigned i = 0; i < sizeof(image); i++) image[i] = (uint8_t)(i * 73 + 19);
assert(group_avatar_set(nodes[0], GROUP, image, sizeof(image)) < 0);
assert(group_avatar_set(nodes[1], GROUP, image, 1) < 0);
assert(!group_avatar_set(nodes[0], GROUP, image, GROUP_AVATAR_MAX));
struct group_avatar_record first; assert(!group_avatar_get(nodes[0], GROUP, &first));
capture_blocks = 1; debug_set_log_hook(capture);
assert(wait_record(1, first.version, first.hash)); capture_blocks = 0; debug_set_log_hook(NULL);
assert(received_blocks == 4);
struct group_avatar_record copy; assert(!group_avatar_get(nodes[1], GROUP, &copy)); assert(!memcmp(copy.data, image, GROUP_AVATAR_MAX));
copy.signature[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, &copy) < 0);
copy = first; copy.data[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, &copy) < 0);
assert(group_avatar_apply(nodes[1], GROUP + 1, &first) < 0);
// Повторное описание группы сохраняет ключ владельца, аватарку и UI-состояние.
assert(!topo_node_sqlite_channel_put(nodes[0]->topo_sqlite_db, CHANNEL, "avatar", nodes[0]->node_id,
group_x, NULL, group_public, NULL, signature));
assert(!group_avatar_get(nodes[0], GROUP, &copy) && copy.version == first.version);
uint8_t private_key[32]; assert(!topo_node_sqlite_channel_get_priv(nodes[0]->topo_sqlite_db, CHANNEL, private_key));
uint8_t wrong_pub[32] = {1};
assert(topo_node_sqlite_channel_put(nodes[0]->topo_sqlite_db, CHANNEL, "avatar", nodes[0]->node_id,
group_x, NULL, wrong_pub, NULL, signature) < 0);
assert(!group_avatar_get(nodes[0], GROUP, &copy) && copy.version == first.version);
etcp_router_set_receive_ready(nodes[1], ETCP_RT_ID_GROUP_AVATAR, hold);
image[0] ^= 1; assert(!group_avatar_set(nodes[0], GROUP, image, 4097));
struct group_avatar_record next; assert(!group_avatar_get(nodes[0], GROUP, &next));
deadline = get_time_tb() + 20000; while (get_time_tb() < deadline) uasync_poll(ua, 2);
assert(!group_avatar_get(nodes[1], GROUP, &copy) && copy.version == first.version);
etcp_router_conn_restart(nodes[0], GROUP, nodes[1]->node_id, ETCP_RT_ID_GROUP_AVATAR);
etcp_router_set_receive_ready(nodes[1], ETCP_RT_ID_GROUP_AVATAR, ga_receive_ready);
etcp_router_resume_receive(nodes[1], GROUP, nodes[0]->node_id, ETCP_RT_ID_GROUP_AVATAR);
assert(wait_record(1, next.version, next.hash)); assert(group_avatar_apply(nodes[1], GROUP, &first) == 0);
// Держатель ключа, который не является создателем группы, тоже может обновить картинку.
set_key(1, group_private); assert(!group_avatar_set(nodes[1], GROUP, image, 1)); set_key(1, NULL);
assert(!group_avatar_get(nodes[1], GROUP, &next)); assert(wait_record(0, next.version, next.hash));
copy = next; copy.data[0]++;
SHA256(copy.data, copy.size, copy.hash); uint8_t signed_bytes[GROUP_AVATAR_MAX + 64];
assert(sc_ed25519_sign(group_private, signed_bytes, ga_message(GROUP, &copy, signed_bytes), copy.signature) == SC_OK);
int accepted = ga_compare(copy.version, copy.hash, next.version, next.hash) > 0;
assert(group_avatar_apply(nodes[0], GROUP, &copy) == accepted);
assert(!group_avatar_set(nodes[0], GROUP, NULL, 0)); assert(!group_avatar_get(nodes[0], GROUP, &next));
assert(wait_record(1, next.version, next.hash)); assert(!group_avatar_get(nodes[1], GROUP, &copy) && !copy.size);
assert(group_avatar_apply(nodes[1], GROUP, &first) == 0);
topo_group_peer_close(connections[0]); topo_group_peer_close(connections[1]);
utun_instance_destroy(nodes[1]); utun_instance_destroy(nodes[0]);
char config[1024]; snprintf(config, sizeof(config), "[global]\ndb_path=%s/node0\n[chatserver]\nmember_files_enabled=0\n", directory);
nodes[0] = utun_instance_create_from_str(ua, config); assert(nodes[0]);
assert(!utun_core_start(nodes[0]) && !chat_service_start(nodes[0]));
assert(!group_avatar_get(nodes[0], GROUP, &copy) && copy.version == next.version && !copy.size);
utun_instance_destroy(nodes[0]); uasync_destroy(ua, 1);
puts("group avatar: PASS limits, signatures, four 4KiB blocks, atomicity, restart, key holders and tombstones"); return 0;
}