From 58896d179ecb3f78b2921144f4bc62afab39c30f Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 12:06:14 +0100 Subject: [PATCH] Add typed DM parties and verified recipient snapshots --- src/dm/dm_identity.c | 200 +++++++++++++++++++++++++++++++++++++++++++ src/dm/dm_identity.h | 70 +++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 src/dm/dm_identity.c create mode 100644 src/dm/dm_identity.h diff --git a/src/dm/dm_identity.c b/src/dm/dm_identity.c new file mode 100644 index 000000000..c6a6ab486 --- /dev/null +++ b/src/dm/dm_identity.c @@ -0,0 +1,200 @@ +#include "dm_identity.h" +#include "dm_crypto.h" +#include "../utun_instance.h" +#include "../routing_layer/devgroup.h" +#include "../transport_layer/secure_channel.h" +#include "../../lib/debug_config.h" +#include +#include +#include +#include +#include + +static int identity_error(struct UTUN_INSTANCE* inst, const char* stage, uint64_t node) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM identity: %s node=%llu sql=%s", stage, (unsigned long long)node, + inst && inst->topo_sqlite_db ? sqlite3_errmsg(inst->topo_sqlite_db) : "unavailable"); + return -1; +} + +/* Проверка формы не зависит от доступности устройства и локального каталога. */ +static int party_valid(const struct dm_party* party) { + static const uint8_t zero[32] = {0}; + if (!party || !memcmp(party->ed, zero, 32) || !memcmp(party->x, zero, 32)) return 0; + if (party->kind == DM_PARTY_GROUP) return party->node == 0; + return party->kind == DM_PARTY_DEVICE && party->node && party->node == sc_derive_node_id_from_pubkey(party->x); +} + +int dm_party_encode(const struct dm_party* party, uint8_t out[DM_PARTY_WIRE_SIZE]) { + if (!out || !party_valid(party)) return identity_error(NULL, "invalid party encode", party ? party->node : 0); + out[0] = party->kind; + for (unsigned i = 0; i < 8; i++) out[1 + i] = (uint8_t)(party->node >> (8 * i)); + memcpy(out + 9, party->ed, 32); memcpy(out + 41, party->x, 32); + return 0; +} + +int dm_party_decode(const uint8_t data[DM_PARTY_WIRE_SIZE], struct dm_party* out) { + if (!data || !out) return identity_error(NULL, "invalid party decode arguments", 0); + struct dm_party party = {.kind = data[0]}; + for (unsigned i = 0; i < 8; i++) party.node |= (uint64_t)data[1 + i] << (8 * i); + memcpy(party.ed, data + 9, 32); memcpy(party.x, data + 41, 32); + if (!party_valid(&party)) return identity_error(NULL, "invalid party encoding", party.node); + *out = party; + return 0; +} + +int dm_party_equal(const struct dm_party* a, const struct dm_party* b) { + return a && b && a->kind == b->kind && a->node == b->node && !memcmp(a->ed, b->ed, 32) && !memcmp(a->x, b->x, 32); +} + +/* Собственное устройство берём из ядра, удалённое — из проверенной проекции nodes. */ +int dm_party_resolve(struct UTUN_INSTANCE* inst, uint64_t node, enum dm_mode mode, struct dm_party* out) { + if (!inst || !inst->topo_sqlite_db || !node || !out || (mode != DM_MODE_DEVICE && mode != DM_MODE_GROUPS)) + return identity_error(inst, "invalid resolve arguments", node); + struct dm_party party = {.kind = DM_PARTY_DEVICE, .node = node}; + if (node == inst->node_id) { + memcpy(party.ed, inst->my_ed25519_pubkey, 32); memcpy(party.x, inst->my_keys.public_key, 32); + if (mode == DM_MODE_GROUPS) { + struct devgroup_record record; uint8_t ed[32] = {0}, x[32] = {0}; + int found = devgroup_load_local(inst, &record, ed, x); + OPENSSL_cleanse(ed, sizeof(ed)); OPENSSL_cleanse(x, sizeof(x)); + if (found < 0) return identity_error(inst, "local subgroup verification failed", node); + if (found) { + party.kind = DM_PARTY_GROUP; party.node = 0; + memcpy(party.ed, record.ed, 32); memcpy(party.x, record.x, 32); + } + } + } else { + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, + "SELECT x25519_pubkey,ed25519_pubkey FROM nodes WHERE node_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32 && sqlite3_column_bytes(st, 1) == 32) { + memcpy(party.x, sqlite3_column_blob(st, 0), 32); memcpy(party.ed, sqlite3_column_blob(st, 1), 32); + } else { + sqlite3_finalize(st); + return identity_error(inst, "missing or damaged device keys", node); + } + if (sqlite3_finalize(st) != SQLITE_OK) return identity_error(inst, "device query finalization failed", node); + if (!party_valid(&party)) return identity_error(inst, "device ID/key mismatch", node); + if (mode == DM_MODE_GROUPS) { + uint8_t id[32]; char name[64]; + int found = devgroup_member(inst->topo_sqlite_db, node, id, name); + if (found < 0) return identity_error(inst, "subgroup membership verification failed", node); + if (found) { + struct devgroup_record record; struct devgroup_profile profile; + if (devgroup_latest(inst->topo_sqlite_db, id, &record, &profile) != 1) + return identity_error(inst, "subgroup profile unavailable", node); + party.kind = DM_PARTY_GROUP; party.node = 0; + memcpy(party.ed, record.ed, 32); memcpy(party.x, record.x, 32); + } + } + } + if (!party_valid(&party)) return identity_error(inst, "invalid resolved party", node); + *out = party; + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM identity: resolved node=%llu mode=%d kind=%u", + (unsigned long long)node, mode, party.kind); + return 0; +} + +int dm_party_member(struct UTUN_INSTANCE* inst, const struct dm_party* party, uint64_t node) { + if (!party_valid(party) || !node) return identity_error(inst, "invalid membership arguments", node); + if (party->kind == DM_PARTY_DEVICE && party->node != node) return 0; + struct dm_party current; + if (dm_party_resolve(inst, node, party->kind == DM_PARTY_GROUP ? DM_MODE_GROUPS : DM_MODE_DEVICE, ¤t)) return -1; + return dm_party_equal(party, ¤t); +} + +int dm_party_private(struct UTUN_INSTANCE* inst, const struct dm_party* party, uint8_t out[32]) { + if (!inst || !out || !party_valid(party)) return identity_error(inst, "invalid secret request", 0); + if (party->kind == DM_PARTY_DEVICE) { + if (party->node != inst->node_id || memcmp(party->x, inst->my_keys.public_key, 32) || + memcmp(party->ed, inst->my_ed25519_pubkey, 32)) return identity_error(inst, "device secret unavailable", party->node); + memcpy(out, inst->my_keys.private_key, 32); + return 0; + } + struct devgroup_record record; uint8_t ed[32] = {0}, x[32] = {0}; + int rc = devgroup_load_local(inst, &record, ed, x); + int found = rc == 1 && !memcmp(party->ed, record.ed, 32) && !memcmp(party->x, record.x, 32); + if (found) memcpy(out, x, 32); + OPENSSL_cleanse(ed, sizeof(ed)); OPENSSL_cleanse(x, sizeof(x)); + if (!found) return identity_error(inst, "pinned subgroup secret unavailable", inst->node_id); + return 0; +} + +uint64_t dm_identity_conv_id(enum dm_mode mode, const struct dm_party parties[2]) { + if (!parties || (mode != DM_MODE_DEVICE && mode != DM_MODE_GROUPS) || !party_valid(parties) || + !party_valid(parties + 1) || dm_party_equal(parties, parties + 1)) { + identity_error(NULL, "invalid conversation parties", 0); return 0; + } + if (mode == DM_MODE_DEVICE) { + if (parties[0].kind != DM_PARTY_DEVICE || parties[1].kind != DM_PARTY_DEVICE || parties[0].node == parties[1].node) { + identity_error(NULL, "device mode requires distinct devices", 0); return 0; + } + return dm_derive_conv_id(parties[0].node, parties[1].node); + } + const char domain[] = "utun-dm-groups-v1"; + uint8_t wire[2][DM_PARTY_WIRE_SIZE], data[sizeof(domain) + 2 * DM_PARTY_WIRE_SIZE], hash[32]; + if (dm_party_encode(parties, wire[0]) || dm_party_encode(parties + 1, wire[1])) return 0; + unsigned first = memcmp(wire[0], wire[1], DM_PARTY_WIRE_SIZE) > 0; + memcpy(data, domain, sizeof(domain)); memcpy(data + sizeof(domain), wire[first], DM_PARTY_WIRE_SIZE); + memcpy(data + sizeof(domain) + DM_PARTY_WIRE_SIZE, wire[1 - first], DM_PARTY_WIRE_SIZE); + SHA256(data, sizeof(data), hash); + uint64_t id = 0; + for (unsigned i = 0; i < 8; i++) id |= (uint64_t)hash[i] << (8 * i); + id &= INT64_MAX; + return id ? id : 1; +} + +/* Добавление не молча обрезает snapshot и не допускает принадлежность обеим сторонам. */ +static int targets_add(struct dm_target targets[DM_TARGET_MAX], size_t* count, size_t capacity, uint64_t node, unsigned side) { + for (size_t i = 0; i < *count; i++) { + if (targets[i].node != node) continue; + if (targets[i].side == side) return 0; + return identity_error(NULL, "device belongs to both parties", node); + } + if (*count >= capacity || *count >= DM_TARGET_MAX) return identity_error(NULL, "recipient snapshot limit exceeded", node); + targets[*count].node = node; targets[*count].side = (uint8_t)side; ++*count; + return 0; +} + +static int target_compare(const void* a, const void* b) { + uint64_t x = ((const struct dm_target*)a)->node, y = ((const struct dm_target*)b)->node; + return x > y ? 1 : x < y ? -1 : 0; +} + +int dm_identity_targets(struct UTUN_INSTANCE* inst, const struct dm_party parties[2], + struct dm_target* out, size_t capacity, size_t* count) { + if (!inst || !inst->topo_sqlite_db || !parties || !out || !count || !capacity || + !party_valid(parties) || !party_valid(parties + 1) || dm_party_equal(parties, parties + 1)) + return identity_error(inst, "invalid recipient snapshot arguments", 0); + struct dm_target targets[DM_TARGET_MAX]; size_t n = 0; + for (unsigned side = 0; side < 2; side++) { + const struct dm_party* party = parties + side; + if (party->kind == DM_PARTY_DEVICE) { + if (dm_party_member(inst, party, party->node) != 1 || targets_add(targets, &n, capacity, party->node, side)) return -1; + continue; + } + size_t before = n; + int local = dm_party_member(inst, party, inst->node_id); + if (local < 0 || (local && targets_add(targets, &n, capacity, inst->node_id, side))) return -1; + sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT node_id FROM nodes WHERE devgroup_ed=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 1, party->ed, 32, SQLITE_STATIC); + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + uint64_t node = (uint64_t)sqlite3_column_int64(st, 0); + if (node == inst->node_id) continue; + int member = dm_party_member(inst, party, node); + if (member != 1 || targets_add(targets, &n, capacity, node, side)) { + sqlite3_finalize(st); return identity_error(inst, "inconsistent recipient membership", node); + } + } + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) return identity_error(inst, "recipient query failed", 0); + if (n == before) return identity_error(inst, "pinned party has no known devices", 0); + } + qsort(targets, n, sizeof(targets[0]), target_compare); + memcpy(out, targets, n * sizeof(targets[0])); *count = n; + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM identity: recipient snapshot complete count=%zu", n); + return 0; +} diff --git a/src/dm/dm_identity.h b/src/dm/dm_identity.h new file mode 100644 index 000000000..eff21e5ad --- /dev/null +++ b/src/dm/dm_identity.h @@ -0,0 +1,70 @@ +/* dm_identity — стороны личной беседы и снимок известных устройств. + * DEVICE сохраняет пару узлов; GROUPS разрешает каждую сторону в подгруппу + * либо узел при её отсутствии. Ошибка проверки не означает отсутствие группы. + * Вызывать в uasync-потоке ядра; выходные структуры меняются только при успехе. + * Стороны сохраняются в беседе: повторное разрешение при отправке запрещено. */ +#ifndef UTUN_DM_IDENTITY_H +#define UTUN_DM_IDENTITY_H + +#include +#include + +struct UTUN_INSTANCE; + +#ifdef __cplusplus +extern "C" { +#endif + +enum dm_mode { DM_MODE_DEVICE = 0, DM_MODE_GROUPS = 1 }; +enum dm_party_kind { DM_PARTY_DEVICE = 0, DM_PARTY_GROUP = 1 }; +#define DM_PARTY_WIRE_SIZE 73 +#define DM_TARGET_MAX 128 + +struct dm_party { + uint8_t kind; /* dm_party_kind, независимо от режима беседы. */ + uint64_t node; /* DEVICE: ID узла; GROUP: строго 0. */ + uint8_t ed[32]; /* Публичный ключ подписи узла или ID подгруппы. */ + uint8_t x[32]; /* Проверенный публичный X25519 ключ той же стороны. */ +}; + +struct dm_target { + uint64_t node; /* Конкретное устройство, в том числе офлайн. */ + uint8_t side; /* Индекс стороны в переданном массиве parties[2]. */ +}; + +/* 0/-1: разрешить известный node для mode, включая проверку локальных секретов + * для собственной подгруппы. Отсутствующий/повреждённый узел — ошибка. */ +int dm_party_resolve(struct UTUN_INSTANCE* inst, uint64_t node, enum dm_mode mode, struct dm_party* out); + +/* Проверить текущую принадлежность узла закреплённой стороне: 1=да,0=нет,-1=ошибка. + * Не использовать текущую принадлежность вместо подписанного исторического snapshot. */ +int dm_party_member(struct UTUN_INSTANCE* inst, const struct dm_party* party, uint64_t node); + +/* 0/-1: получить собственный X25519 секрет строго указанной стороны. + * out — 32 байта; вызывающий очищает секрет после использования. */ +int dm_party_private(struct UTUN_INSTANCE* inst, const struct dm_party* party, uint8_t out[32]); + +/* Канонический codec: kind:u8,node:LE64,ed:32,x:32; padding структуры не передаётся. + * 0/-1, размеры буферов ровно DM_PARTY_WIRE_SIZE; decode проверяет форму и ID узла. */ +int dm_party_encode(const struct dm_party* party, uint8_t out[DM_PARTY_WIRE_SIZE]); +int dm_party_decode(const uint8_t data[DM_PARTY_WIRE_SIZE], struct dm_party* out); + +/* 1 при совпадении всех полей (без padding), иначе 0. Аргументы обязательны. */ +int dm_party_equal(const struct dm_party* a, const struct dm_party* b); + +/* 0 при ошибке, иначе положительный 63-битный ID. DEVICE использует прежний + * dm_derive_conv_id, GROUPS — домен и отсортированные полные стороны. + * Одинаковые стороны отклоняются; ID не заменяет проверку полной пары. */ +uint64_t dm_identity_conv_id(enum dm_mode mode, const struct dm_party parties[2]); + +/* 0/-1: собрать уникальные известные устройства обеих закреплённых сторон, + * включая источник и офлайн. Сортировка по node; источник — обычная запись. + * Переполнение capacity или DM_TARGET_MAX отклоняет весь snapshot. Каждая + * сторона должна иметь хотя бы один узел; ошибки SQL/подписей не пропускаются. */ +int dm_identity_targets(struct UTUN_INSTANCE* inst, const struct dm_party parties[2], + struct dm_target* out, size_t capacity, size_t* count); + +#ifdef __cplusplus +} +#endif +#endif