Add typed DM parties and verified recipient snapshots
This commit is contained in:
1 parent
980afcdea8
commit
58896d179e
2 files changed
+270
No files matched your search
@@ -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 <sqlite3.h>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/* dm_identity — стороны личной беседы и снимок известных устройств.
|
||||
* DEVICE сохраняет пару узлов; GROUPS разрешает каждую сторону в подгруппу
|
||||
* либо узел при её отсутствии. Ошибка проверки не означает отсутствие группы.
|
||||
* Вызывать в uasync-потоке ядра; выходные структуры меняются только при успехе.
|
||||
* Стороны сохраняются в беседе: повторное разрешение при отправке запрещено. */
|
||||
#ifndef UTUN_DM_IDENTITY_H
|
||||
#define UTUN_DM_IDENTITY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user