diff --git a/tests/Makefile.am b/tests/Makefile.am index 1fca14ffb..44d3520a3 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -95,6 +95,7 @@ check_PROGRAMS = \ test_tcp_proxy_server \ test_db_sync_schema \ test_member_adapter \ + test_group_policy \ test_chat_feedback \ test_encrypted_file \ test_encrypted_attachment \ @@ -1000,6 +1001,9 @@ test_chat_sync_schedule_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I test_chat_sync_schedule_LDFLAGS = $(test_clock_ldflags) test_chat_sync_schedule_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_member_adapter_SOURCES = test_member_adapter.c test_member_purge.c +test_group_policy_SOURCES = test_group_policy.c +test_group_policy_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib +test_group_policy_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_member_adapter_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/lib test_member_adapter_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_member_adapter_LDFLAGS = -Wl,--wrap=EVP_PKEY_new_raw_public_key -Wl,--wrap=EVP_MD_CTX_new \ diff --git a/tests/test_group_policy.c b/tests/test_group_policy.c new file mode 100644 index 000000000..83bdc39f2 --- /dev/null +++ b/tests/test_group_policy.c @@ -0,0 +1,111 @@ +/* Реальные подписи, SQLite, Merkle страницы и одна реализация расчёта допуска. */ +#include "../src/chat/member_sync.c" +#include + +static uint8_t group_x[32] = {81}, group_private[32] = {82}, group_public[32]; +struct person { uint64_t id; uint8_t x[32], ed[32], private[32], join[64], tree[64], role[64]; }; +static struct person people[6]; + +static void public_key(const uint8_t private[32], uint8_t public[32]) { + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, private, 32); assert(key); + size_t size = 32; assert(EVP_PKEY_get_raw_public_key(key, public, &size) == 1); EVP_PKEY_free(key); +} + +static void setup(struct UTUN_INSTANCE* inst, struct UASYNC* ua, int owner) { + memset(inst, 0, sizeof(*inst)); inst->ua = ua; inst->node_id = people[owner ? 0 : 1].id; + assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); + assert(!topo_node_sqlite_init(inst->topo_sqlite_db)); + assert(!topo_node_sqlite_channel_put(inst->topo_sqlite_db, "1001", "policy", people[0].id, + group_x, NULL, group_public, owner ? group_private : NULL, (uint8_t[64]){0})); + assert(!member_sync_init(inst)); +} + +static void put_person(struct UTUN_INSTANCE* inst, unsigned i, unsigned parent) { + struct person* p = &people[i]; + const char* role = i == 1 ? "{\"ver\":\"1\",\"admin\":\"e1\"}" : NULL; + uint8_t body[128]; + if (role) { + int n = member_sync_build_owner_msg(p->id, role, body, sizeof(body)); assert(n > 0); + assert(sc_ed25519_sign(group_private, body, n, p->role) == SC_OK); + } + assert(member_sync_sign_join(i ? people[parent].private : group_private, p->join, p->tree) == 0); + assert(member_sync_put(inst, "1001", p->id, p->x, p->ed, p->join, 1, NULL, 0, "{\"name\":\"member\"}", + role, role ? p->role : NULL, 0, i ? people[parent].id : 0, p->tree) >= 0); +} + +static void token(struct UTUN_INSTANCE* inst, char out[65]) { + uint8_t hash[32]; assert(!merkle_sync_read_hash(inst, MT_DATA_MEMBERS, "1001", 0, 0, hash)); + for (int i = 0; i < 32; i++) snprintf(out + i * 2, 3, "%02x", hash[i]); +} + +static int admitted(struct UTUN_INSTANCE* inst, unsigned person) { + return topo_node_sqlite_member_allowed(inst->topo_sqlite_db, "1001", people[person].id); +} + +static void page(struct UTUN_INSTANCE* src, struct UTUN_INSTANCE* dst, uint64_t prefix) { + uint8_t data[8192]; size_t size = sizeof(data); int more; merkle_key_t after = {{0}}, next; + assert(!_member_get_page(src, "1001", prefix, 0, &after, data, &size, &next, &more) && !more); + assert(!_member_apply_items(dst, "1001", src->node_id, data, size)); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + struct UASYNC* ua = uasync_create(); assert(ua); public_key(group_private, group_public); + for (unsigned i = 0; i < 6; i++) { + struct person* p = &people[i]; p->x[0] = i + 1; p->private[0] = i + 11; + public_key(p->private, p->ed); p->id = sc_derive_node_id_from_pubkey(p->x); + uint8_t body[112]; assert(member_sync_build_join_msg(group_x, group_public, p->id, p->x, 1, body, sizeof(body)) == 112); + assert(sc_ed25519_sign(p->private, body, sizeof(body), p->join) == SC_OK); + } + struct UTUN_INSTANCE source, target; setup(&source, ua, 1); setup(&target, ua, 0); + const unsigned parents[] = {0,0,0,2,3,1}; + for (unsigned i = 0; i < 6; i++) { put_person(&source, i, parents[i]); put_person(&target, i, parents[i]); } + assert(admitted(&source, 0) && admitted(&source, 1) && admitted(&source, 2) && admitted(&source, 5)); + assert(!admitted(&source, 3) && !admitted(&source, 4)); + assert(group_policy_can_invite(&source, "1001", people[0].id) == 1); + assert(group_policy_can_invite(&source, "1001", people[1].id) == 1); + assert(group_policy_can_invite(&source, "1001", people[2].id) == 0); + char baseline[65], current[65], *json = NULL; token(&source, baseline); + assert(!group_policy_command(&source, "1001", 1, baseline, NULL, &json)); u_free(json); + assert(admitted(&source, 3) && admitted(&source, 4) && group_policy_can_invite(&source, "1001", people[2].id) == 1); + page(&source, &target, 0); member_policy_result(&target, "1001", source.node_id, MT_OK); + assert(admitted(&target, 3) && admitted(&target, 4)); + assert(!group_policy_command(&source, "1001", 0, NULL, NULL, &json)); + assert(strstr(json, "\"affected\":[{") && strstr(json, "\"can_edit\":true")); u_free(json); + token(&source, baseline); + assert(group_policy_command(&target, "1001", 0, baseline, NULL, &json) < 0 && strstr(json, "forbidden")); u_free(json); + assert(group_policy_command(&source, "1001", 0, "stale", NULL, &json) == -2); u_free(json); + assert(group_policy_command(&source, "1001", 0, baseline, "{}", &json) < 0); u_free(json); + char keep[64]; snprintf(keep, sizeof(keep), "[\"%llu\"]", (unsigned long long)people[3].id); + assert(sqlite3_exec(source.topo_sqlite_db, "CREATE TRIGGER reject_policy BEFORE UPDATE ON group_policy " + "BEGIN SELECT RAISE(ABORT,'rollback test'); END", NULL, NULL, NULL) == SQLITE_OK); + assert(group_policy_command(&source, "1001", 0, baseline, keep, &json) < 0); u_free(json); + token(&source, current); assert(!strcmp(current, baseline) && admitted(&source, 4)); + uint8_t signature[64]; uint64_t signer; + assert(!topo_node_sqlite_member_get_sign(source.topo_sqlite_db, "1001", people[3].id, &signer, signature)); + assert(signer == people[2].id); + assert(sqlite3_exec(source.topo_sqlite_db, "DROP TRIGGER reject_policy", NULL, NULL, NULL) == SQLITE_OK); + assert(!group_policy_command(&source, "1001", 0, baseline, keep, &json)); u_free(json); + assert(admitted(&source, 3) && !admitted(&source, 4)); + assert(!topo_node_sqlite_member_get_sign(source.topo_sqlite_db, "1001", people[3].id, &signer, signature) && !signer); + /* Политика раньше переподписи: текущий допуск сохраняется до полной контрольной точки. */ + page(&source, &target, 0); assert(admitted(&target, 3) && admitted(&target, 4)); + const uint64_t mask = ~UINT64_C(0) << (64 - MT_MAX_LEVEL * 5); + for (unsigned i = 0; i < 6; i++) page(&source, &target, people[i].id & mask); + assert(admitted(&target, 3) && admitted(&target, 4)); + assert(!member_finish(&target, "1001")); member_policy_result(&target, "1001", source.node_id, MT_OK); + assert(admitted(&target, 3) && !admitted(&target, 4)); + token(&source, baseline); token(&target, current); assert(!strcmp(baseline, current)); + /* Поздняя старая подпись не вытесняет подпись группы той же эпохи. */ + put_person(&target, 3, 2); token(&target, current); assert(!strcmp(baseline, current)); + member_sync_destroy(&target); assert(!member_sync_init(&target)); + assert(admitted(&target, 3) && !admitted(&target, 4)); + /* Другое join_sig не может использовать подтверждение прежнего JOIN. */ + struct ms_member_rec forged = {.node_id = people[3].id, .x25519 = people[3].x, .join_sig = people[4].join, + .signature = signature, .signed_by = 0}; + assert(member_sync_verify_signature(source.topo_sqlite_db, "1001", &forged) == 0); + member_sync_destroy(&source); member_sync_destroy(&target); + assert(sqlite3_close(source.topo_sqlite_db) == SQLITE_OK && sqlite3_close(target.topo_sqlite_db) == SQLITE_OK); + uasync_destroy(ua, 0); puts("PASS: default, roles, cascade, selection, rollback, stale preview, page order, signature precedence, restart"); + return 0; +}