Browse Source

fix: accept invite public keys with a leading zero byte

proxy
evgeny 3 days ago
parent
commit
c63e93715f
  1. 9
      src/chat/invite_link.c
  2. 23
      src/routing_layer/topo_group_invite.c
  3. 268
      tests/test_invite_group_create.c

9
src/chat/invite_link.c

@ -6,6 +6,7 @@
*/ */
#include "invite_link.h" #include "invite_link.h"
#include "../transport_layer/secure_channel.h" #include "../transport_layer/secure_channel.h"
#include "../../lib/debug_config.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@ -164,7 +165,13 @@ int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t b
} }
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) { int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) {
if (!data || !out || data->pubkey[0] == 0) return -1; if (!data || !out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invite_link_encode: missing input or output"); return -1;
}
static const uint8_t empty_key[INVITE_PUBKEY_SIZE] = {0};
if (!memcmp(data->pubkey, empty_key, sizeof(empty_key))) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "invite_link_encode: empty public key"); return -1;
}
if (data->addrCount == 0) return -1; if (data->addrCount == 0) return -1;
uint8_t raw[4096]; size_t pos = 0; uint8_t raw[4096]; size_t pos = 0;
raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = INVITE_LINK_VERSION;

23
src/routing_layer/topo_group_invite.c

@ -439,7 +439,23 @@ void topo_group_invite_init(struct UTUN_INSTANCE* inst) {
int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id,
struct TOPO_NODE* ni, uint64_t node_id, struct TOPO_NODE* ni, uint64_t node_id,
tgi_cb_t cb, void* cb_arg) { tgi_cb_t cb, void* cb_arg) {
if (!inst || !inst->topo_groups || !cb || !ni) return -1; if (!inst || !inst->topo_groups || !cb || !ni) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: invalid join arguments group=%016llx", (unsigned long long)group_id);
return -1;
}
uint64_t nid = node_id;
if (!nid || nid == inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: invalid peer=%016llx group=%016llx",
(unsigned long long)nid, (unsigned long long)group_id); return -1;
}
if (!ni->v4_addrs && !ni->v6_addrs) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: no addresses for peer=%016llx", (unsigned long long)nid); return -1;
}
/* Нулевой первый байт допустим; отсутствующим считаем только полностью пустой ключ. */
static const uint8_t empty_key[SC_PUBKEY_SIZE] = {0};
if (!memcmp(ni->public_key, empty_key, sizeof(empty_key))) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: empty public key for peer=%016llx", (unsigned long long)nid); return -1;
}
/* 1. Найти или создать группу */ /* 1. Найти или создать группу */
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
@ -449,11 +465,6 @@ int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id,
if (!group) return -1; if (!group) return -1;
} }
uint64_t nid = node_id;
if (!nid || nid == inst->node_id) return -1;
if (!ni->v4_addrs && !ni->v6_addrs) return -1;
if (!ni->public_key[0]) return -1;
/* 2. Копировать ni в group_ni */ /* 2. Копировать ni в group_ni */
struct TOPO_GROUPS* groups = inst->topo_groups; struct TOPO_GROUPS* groups = inst->topo_groups;
struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1; struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1;

268
tests/test_invite_group_create.c

@ -1,165 +1,129 @@
/** /* INVITE_INFO получает подписанное описание канала, но не добавляет нового мембера.
* @file test_invite_group_create.c * Фиксированный X25519-ключ ответчика начинается с 00: это допустимый ключ. */
* @brief Full invite flow: topo_group_invite_join → B is member → TGI_EVENT_JOIN
*/
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdarg.h>
#include "../lib/platform_compat.h" #include "../lib/platform_compat.h"
#include "../lib/mem.h" #include "../lib/socket_compat.h"
#include "../lib/debug_config.h" #include "../lib/debug_config.h"
#include "test_utils.h"
#ifndef _WIN32
#include <unistd.h>
#endif
#include "../src/transport_layer/etcp.h"
#include "../src/transport_layer/etcp_connections.h"
#include "../src/config_parser.h"
#include "../src/config_updater.h"
#include "../src/utun_instance.h" #include "../src/utun_instance.h"
#include "../src/chat/chat_core.h"
#include "../src/chat/invite_link.h"
#include "../src/routing_layer/topo_group.h" #include "../src/routing_layer/topo_group.h"
#include "../src/routing_layer/topo_node.h"
#include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/topo_node_sqlite.h"
#include "../src/routing_layer/topo_group_invite.h" #include "../src/routing_layer/topo_group_invite.h"
#include "../src/transport_layer/etcp_connections.h"
#define TIMEOUT_TB 50000 #define REQUIRE(expr) do { if (!(expr)) { \
#define POLL_MS 20 DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "test_invite_group_create: line=%d failed: %s", __LINE__, #expr); \
goto cleanup; \
static void fail(const char* msg) { fprintf(stderr, "FAIL: %s\n", msg); fflush(stderr); exit(1); } } } while (0)
static void wf(const char* path, const char* fmt, ...) { struct invite_result {
va_list ap; va_start(ap, fmt); int calls, event;
FILE* f = fopen(path, "w"); uint64_t node_id, group_id;
if (!f) { fail("fopen"); return; } };
vfprintf(f, fmt, ap); fclose(f);
va_end(ap); /* Сохранить результат; жизненный цикл экземпляров меняется после выхода из callback. */
} static void invite_done(uint64_t node_id, uint64_t group_id, int event, void* arg) {
struct invite_result* result = arg;
static char* ls(const char* p, const char* k) { result->calls++; result->event = event; result->node_id = node_id; result->group_id = group_id;
char b[1024]; FILE* f = fopen(p, "r"); if (!f) return NULL; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite result: peer=%016llx group=%016llx event=%d calls=%d",
size_t n = fread(b, 1, sizeof(b) - 1, f); fclose(f); b[n] = 0; (unsigned long long)node_id, (unsigned long long)group_id, event, result->calls);
char* x = strstr(b, k); if (!x) return NULL;
x += strlen(k) + 1; while (*x == ' ' || *x == '\t') x++;
char* r = u_strdup(x); char* e = r; while (*e && *e != '\n' && *e != '\r') e++; *e = 0;
return r;
}
static int result = 0;
static void ccb(uint64_t nid, uint64_t gid, int ev, void* arg) {
(void)gid; (void)arg;
fprintf(stderr, "CB: ev=%d node=0x%llx\n", ev, (unsigned long long)nid); fflush(stderr);
if (ev == TGI_EVENT_JOIN) result = 1;
if (ev == TGI_EVENT_TIMEOUT) result = 2;
} }
static void to_cb(void* arg) { (void)arg; fprintf(stderr, "TIMEOUT\n"); fflush(stderr); result = 2; }
int main(void) { int main(void) {
debug_config_init(); int result = 1;
debug_set_level(DEBUG_LEVEL_TRACE); struct UASYNC* ua = NULL;
debug_enable_file_output("/tmp/test_inv_crash.log", 1); struct UTUN_INSTANCE *a = NULL, *b = NULL;
utun_instance_set_tun_init_enabled(0); struct invite_result reply = {0};
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
char tdir[] = "/tmp/utin_XXXXXX"; debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO);
if (test_mkdtemp(tdir) != 0) { fail("mkdtemp"); return 1; } debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_INFO);
char ca[256], cb[256]; debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO);
snprintf(ca, sizeof(ca), "%s/a.conf", tdir);
snprintf(cb, sizeof(cb), "%s/b.conf", tdir); ua = uasync_create(); REQUIRE(ua);
int porta = 52000 + (getpid() % 10000), portb = porta + 1; /* У каждого узла своя in-memory БД и настоящий UDP-сокет на свободном порту. */
a = utun_instance_create_from_str(ua,
wf(ca, "[global]\ntun_ip=10.94.0.1/24\ntun_ifname=tun89\ndb_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", tdir, porta); "[global]\nmy_node_name=invite-A\n"
wf(cb, "[global]\ntun_ip=10.94.0.2/24\ntun_ifname=tun88\ndb_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", tdir, portb); "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
config_ensure_keys_and_node_id(ca); config_ensure_keys_and_node_id(cb); "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
"[server: udp]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n"
char* rva = ls(ca, "priv"); char* pua = ls(ca, "pub"); char* pub = ls(cb, "pub"); "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");
wf(ca, "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.94.0.1/24\ntun_ifname=tun89\ndb_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[client: to_b]\nkeepalive=1\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n[allowed_keys]\nallow_all=1\n", rva, pua, tdir, porta, pub, portb); b = utun_instance_create_from_str(ua,
char* rvb = ls(cb, "priv"); "[global]\nmy_node_name=invite-B\n"
wf(cb, "[global]\nmy_private_key=%s\nmy_public_key=%s\ntun_ip=10.94.0.2/24\ntun_ifname=tun88\ndb_path=%s\n[server: s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n", rvb, pub, tdir, portb); "my_private_key=a00a000000000000000000000000000000000000000000000000000000000000\n"
u_free(rva); u_free(pua); u_free(pub); u_free(rvb); "my_public_key=00b607cef42a8324d990aaa6f19d190d3ee9120369ade4d0f94b539edb050b64\n"
"[server: udp]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n"
struct UASYNC* ua = uasync_create(); "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");
struct UTUN_INSTANCE* a = utun_instance_create(ua, ca); REQUIRE(a && b);
if (!a) { fail("create A"); goto clean; } REQUIRE(utun_core_start(a) == 0 && chat_service_start(a) == 0);
utun_instance_init(a); topo_group_invite_init(a); REQUIRE(utun_core_start(b) == 0 && chat_service_start(b) == 0);
topo_group_invite_init(a); topo_group_invite_init(b);
/* ── Add a TCP socket (simulating Android STCP server) ── */ REQUIRE(a->topo_sqlite_db != b->topo_sqlite_db && !a->utun_started && !b->utun_started);
{ REQUIRE(b->my_keys.public_key[0] == 0 && b->my_keys.public_key[1] != 0);
struct ETCP_SOCKET* ts = u_calloc(1, sizeof(*ts));
ts->instance = a; chat_core_create_channel_auto(b, "TestCh");
ts->type = CFG_SERVER_TYPE_PUBLIC; REQUIRE(b->topo_groups->group_list->head);
ts->sock_id = (uint8_t)a->next_socket_id++; struct TOPO_GROUP* group_b = (struct TOPO_GROUP*)b->topo_groups->group_list->head;
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); REQUIRE(group_b->group_type == TOPO_GROUP_TYPE_CHAT);
sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */ uint64_t gid = group_b->group_id;
sin.sin_port = htons((uint16_t)55555); REQUIRE(!topo_groups_find(a->topo_groups, gid));
memcpy(&ts->local_addr, &sin, sizeof(sin));
ts->interface_addr = ts->local_addr; struct sockaddr_in bound = {0}; socklen_t bound_len = sizeof(bound);
ts->next = a->etcp_sockets; a->etcp_sockets = ts; REQUIRE(b->etcp_sockets && !b->etcp_sockets->is_tcp);
fprintf(stderr, "Added TCP socket ts=%p next=%p\n", (void*)ts, (void*)ts->next); fflush(stderr); REQUIRE(getsockname(b->etcp_sockets->fd, (struct sockaddr*)&bound, &bound_len) == 0);
} REQUIRE(bound.sin_family == AF_INET && bound.sin_port != 0);
fprintf(stderr, "etcp_sockets=%p\n", (void*)a->etcp_sockets); fflush(stderr); struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .protocol = TOPO_PROTO_UDP, .type = TOPO_ADDR_INTERFACE };
address.port = ntohs(bound.sin_port);
/* Get B's node_id and pubkey from its instance */ struct TOPO_NODE node = { .node_id = b->node_id, .v4_addrs = &address };
struct UTUN_INSTANCE* b = utun_instance_create(ua, cb); /* Пустой ключ отклоняется до создания группы и без callback. */
if (!b) { fail("create B"); a->running = 0; utun_instance_destroy(a); uasync_destroy(ua, 0); goto clean; } REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1);
utun_instance_init(b); topo_group_invite_init(b); REQUIRE(!reply.calls && !topo_groups_find(a->topo_groups, gid));
memcpy(node.public_key, b->my_keys.public_key, sizeof(node.public_key));
/* ── Set up B as member of group 0x7777777700000001 ── */
{ /* Создание ссылки тоже не должно отбрасывать допустимый нулевой префикс ключа. */
uint64_t gid = 0x7777777700000001ULL; struct InviteData invite = { .channelId = gid, .join_key = 123, .addrCount = 1 }, decoded;
char ch_str[32]; snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)gid); invite.addrs[0].family = 4; invite.addrs[0].proto = INVITE_PROTO_UDP; invite.addrs[0].port = address.port;
topo_groups_create_group(b->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_str); memcpy(invite.addrs[0].address, address.addr, 4);
const char* ch_name = "TestCh"; char link[1024], error[128];
uint8_t sig_msg[256]; size_t slen = 0; REQUIRE(invite_link_encode(&invite, NULL, link, sizeof(link)) == -1);
/* msg: ch_id\0 || name\0 || owner(8 LE) || x25519_pub(32) || ed_pub(32) */ memcpy(invite.pubkey, node.public_key, sizeof(invite.pubkey));
size_t ch_id_len = strlen(ch_str) + 1; memcpy(sig_msg + slen, ch_str, ch_id_len); slen += ch_id_len; REQUIRE(invite_link_encode(&invite, "test-password", link, sizeof(link)) > 0);
size_t name_len = strlen(ch_name) + 1; memcpy(sig_msg + slen, ch_name, name_len); slen += name_len; REQUIRE(invite_link_decode(link, strlen(link), &decoded, error, sizeof(error)) == 0);
uint64_t owner = b->node_id; memcpy(sig_msg + slen, &owner, 8); slen += 8; REQUIRE(decoded.nodeId == b->node_id && decoded.channelId == gid && decoded.join_key == invite.join_key);
memcpy(sig_msg + slen, b->my_keys.public_key, 32); slen += 32; REQUIRE(!memcmp(decoded.pubkey, node.public_key, sizeof(decoded.pubkey)));
memcpy(sig_msg + slen, b->my_ed25519_pubkey, 32); slen += 32; REQUIRE(decoded.addrCount == 1 && decoded.addrs[0].port == address.port && decoded.addrs[0].proto == INVITE_PROTO_UDP);
uint8_t ch_sig[64]; REQUIRE(!memcmp(decoded.addrs[0].address, address.addr, 4) && !strcmp(decoded.password, "test-password"));
sc_ed25519_sign(b->my_ed25519_privkey, sig_msg, slen, ch_sig); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite start: peer=%016llx key_prefix=%02x%02x port=%u",
topo_node_sqlite_channel_put(b->topo_sqlite_db, ch_str, ch_name, owner, (unsigned long long)b->node_id, node.public_key[0], node.public_key[1], address.port);
b->my_keys.public_key, NULL, REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == 0);
b->my_ed25519_pubkey, NULL, ch_sig); uint64_t deadline = get_time_tb() + 50000;
fprintf(stderr, "B: channel setup gid=0x%016llX ch=%s name=%s\n", while (!reply.calls && get_time_tb() < deadline) uasync_poll(ua, 20);
(unsigned long long)gid, ch_str, ch_name); fflush(stderr); REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_JOIN);
} REQUIRE(reply.node_id == b->node_id && reply.group_id == gid);
uint64_t nid_b = b->node_id; struct TOPO_GROUP* group_a = topo_groups_find(a->topo_groups, gid);
uint8_t pk_b[32]; memcpy(pk_b, b->my_keys.public_key, 32); REQUIRE(group_a && group_a->group_type == TOPO_GROUP_TYPE_CHAT);
char name[128]; uint64_t owner = 0;
/* Build TOPO_NODE for invite */ uint8_t x25519[32], ed25519[32], signature[64];
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni)); REQUIRE(topo_node_sqlite_channel_get(a->topo_sqlite_db, group_a->channel_id, name, sizeof(name),
ni->node_id = nid_b; memcpy(ni->public_key, pk_b, 32); &owner, x25519, ed25519, signature) == 0);
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4)); REQUIRE(strcmp(name, "TestCh") == 0 && owner == b->node_id);
uint8_t ip[4] = {127, 0, 0, 1}; memcpy(a4->addr, ip, 4); uint8_t expected_x[32], expected_ed[32], expected_signature[64];
a4->port = (uint16_t)portb; a4->type = TOPO_ADDR_NAT; a4->protocol = 1; REQUIRE(topo_node_sqlite_channel_get(b->topo_sqlite_db, group_b->channel_id, name, sizeof(name),
ni->v4_addrs = a4; &owner, expected_x, expected_ed, expected_signature) == 0);
REQUIRE(!memcmp(x25519, expected_x, 32) && !memcmp(ed25519, expected_ed, 32));
uint64_t gid = 0x7777777700000001ULL; REQUIRE(!memcmp(signature, expected_signature, 64));
fprintf(stderr, "=== topo_group_invite_join gid=0x%016llX nid=0x%016llX ===\n", /* Получение описания не заменяет протокол добавления: B ещё не дал членство A. */
(unsigned long long)gid, (unsigned long long)nid_b); fflush(stderr); REQUIRE(!topo_node_sqlite_member_in_channel(b->topo_sqlite_db, group_b->channel_id, a->node_id));
REQUIRE(!topo_group_peer_ready(group_b, a->node_id));
int r = topo_group_invite_join(a, gid, ni, nid_b, ccb, NULL); result = 0;
fprintf(stderr, "topo_group_invite_join => %d\n", r); fflush(stderr); cleanup:
if (a) utun_instance_destroy(a);
void* tt = uasync_set_timeout(ua, TIMEOUT_TB, NULL, to_cb, "to"); if (b) utun_instance_destroy(b);
int el = 0; /* Дать event loop освободить отменённые таймеры перед проверкой ресурсов. */
while (!result && el < TIMEOUT_TB + 5000) { uasync_poll(ua, POLL_MS); el += POLL_MS; } if (ua) uasync_poll(ua, 0);
if (tt) uasync_cancel_timeout(ua, tt); if (ua) uasync_destroy(ua, 0);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test_invite_group_create: %s", result ? "FAIL" : "PASS");
a->running = 0; utun_instance_destroy(a); return result;
b->running = 0; utun_instance_destroy(b);
uasync_destroy(ua, 0);
u_free(a4); u_free(ni);
fprintf(stderr, "=== DONE result=%d ===\n", result); fflush(stderr);
clean:
test_unlink(ca); test_unlink(cb); test_rmdir(tdir);
debug_disable_file_output();
return (result == 1) ? 0 : 1;
} }

Loading…
Cancel
Save