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
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, ¤t)); |
|
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, ©)); assert(!memcmp(copy.data, image, GROUP_AVATAR_MAX)); |
|
copy.signature[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, ©) < 0); |
|
copy = first; copy.data[0] ^= 1; assert(group_avatar_apply(nodes[1], GROUP, ©) < 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.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.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.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, ©, 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, ©) == 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.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.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; |
|
}
|
|
|