Add common DM parser and signed subgroup recipient context

This commit is contained in:
evgeny committed 2026-11-03 12:18:25 +01:00
1 parent d81b3139fa
commit 2d8d57be47
2 files changed
+204

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
#include "dm_protocol.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 <string.h>
static int protocol_error(const char* stage) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "DM protocol: rejected stage=%s", stage);
return -1;
}
static uint64_t protocol_le64(const uint8_t* p) {
uint64_t value = 0;
for (unsigned i = 0; i < 8; i++) value |= (uint64_t)p[i] << (8 * i);
return value;
}
int dm_message_parse(const uint8_t* body, size_t available, struct dm_message_view* out) {
if (!body || !out || available < 33) return protocol_error("header length");
struct dm_message_view view = {0}; size_t pos = 33;
memcpy(&view.conv, body, 8); memcpy(&view.seq, body + 8, 8);
memcpy(&view.timestamp, body + 16, 8); memcpy(&view.author, body + 24, 8);
view.content_type_size = body[32];
if (!view.content_type_size) {
view.mode = DM_MODE_GROUPS;
if (available - pos < 1 + 2 * DM_PARTY_WIRE_SIZE + 2 || body[pos++] != 1) return protocol_error("context version/size");
if (dm_party_decode(body + pos, view.parties) || dm_party_decode(body + pos + DM_PARTY_WIRE_SIZE, view.parties + 1))
return protocol_error("context parties");
pos += 2 * DM_PARTY_WIRE_SIZE;
view.target_count = (uint16_t)(body[pos] | ((uint16_t)body[pos + 1] << 8)); pos += 2;
if (view.target_count < 2 || view.target_count > DM_TARGET_MAX) return protocol_error("target count");
size_t targets_size = (size_t)view.target_count * DM_TARGET_PROOF_SIZE;
if (available - pos < targets_size + 1) return protocol_error("target list length");
view.targets = body + pos; pos += targets_size;
view.content_type_size = body[pos++];
if (!view.content_type_size) return protocol_error("empty content type");
}
if (available - pos < (size_t)view.content_type_size + 2) return protocol_error("content type length");
view.content_type = body + pos; pos += view.content_type_size;
memcpy(&view.ciphertext_size, body + pos, 2); pos += 2;
if (view.ciphertext_size < 16 || view.ciphertext_size > 1024 + 16 || available - pos < view.ciphertext_size)
return protocol_error("ciphertext length");
view.ciphertext = body + pos; pos += view.ciphertext_size;
if (view.content_type_size == 4 && !memcmp(view.content_type, "file", 4)) {
if (available - pos < 56) return protocol_error("media descriptor length");
view.media = body + pos; pos += 56;
}
if (available - pos < 64) return protocol_error("signature length");
view.signature = body + pos; view.size = pos + 64;
*out = view;
return 0;
}
int dm_message_target(const struct dm_message_view* view, uint64_t node, uint8_t x[32], uint8_t ed[32]) {
if (!view || view->mode != DM_MODE_GROUPS || !view->targets) return -1;
for (unsigned i = 0; i < view->target_count; i++) {
const uint8_t* target = view->targets + i * DM_TARGET_PROOF_SIZE;
if (protocol_le64(target) != node) continue;
if (x) memcpy(x, target + 9, 32);
if (ed) memcpy(ed, target + 41, 32);
return target[8] <= 1 ? target[8] : -1;
}
return -1;
}
int dm_message_verify_targets(const struct dm_message_view* view) {
if (!view) return protocol_error("missing view");
if (view->mode == DM_MODE_DEVICE) return 0;
if (view->mode != DM_MODE_GROUPS || !view->targets || view->target_count < 2 || view->target_count > DM_TARGET_MAX ||
view->conv != dm_identity_conv_id(DM_MODE_GROUPS, view->parties)) return protocol_error("conversation identity");
unsigned sides[2] = {0}; uint64_t previous = 0; int author_found = 0;
for (unsigned i = 0; i < view->target_count; i++) {
const uint8_t* target = view->targets + i * DM_TARGET_PROOF_SIZE;
uint64_t node = protocol_le64(target); unsigned side = target[8];
if (!node || node <= previous || side > 1 || node != sc_derive_node_id_from_pubkey(target + 9))
return protocol_error("target order/identity");
previous = node; sides[side]++;
const struct dm_party* party = view->parties + side;
if (party->kind == DM_PARTY_DEVICE) {
static const uint8_t zero[64] = {0};
if (node != party->node || memcmp(party->x, target + 9, 32) || memcmp(party->ed, target + 41, 32) ||
memcmp(target + 73, zero, 64)) return protocol_error("device party certificate");
} else {
struct devgroup_record record = {0};
memcpy(record.ed, party->ed, 32); memcpy(record.x, party->x, 32); memcpy(record.member_sig, target + 73, 64);
if (devgroup_verify_binding(&record, node, target + 9, target + 41)) return protocol_error("group certificate signature");
}
if (node == view->author) {
if (side) return protocol_error("author on recipient side");
author_found = 1;
}
}
return author_found && sides[0] && sides[1] ? 0 : protocol_error("missing author/party");
}
int dm_context_encode(struct UTUN_INSTANCE* inst, const struct dm_party parties[2],
uint8_t* out, size_t capacity, size_t* length) {
if (!out || !length) return protocol_error("encode arguments");
struct dm_target targets[DM_TARGET_MAX]; size_t count = 0;
if (dm_identity_targets(inst, parties, targets, DM_TARGET_MAX, &count)) return -1;
size_t size = 1 + 2 * DM_PARTY_WIRE_SIZE + 2 + count * DM_TARGET_PROOF_SIZE;
if (capacity < size) return protocol_error("encode capacity");
uint8_t data[DM_CONTEXT_MAX]; size_t pos = 1;
data[0] = 1;
if (dm_party_encode(parties, data + pos) || dm_party_encode(parties + 1, data + pos + DM_PARTY_WIRE_SIZE)) return -1;
pos += 2 * DM_PARTY_WIRE_SIZE; data[pos++] = (uint8_t)count; data[pos++] = (uint8_t)(count >> 8);
struct dm_message_view view = {.mode = DM_MODE_GROUPS, .target_count = (uint16_t)count,
.targets = data + pos, .author = inst->node_id, .conv = dm_identity_conv_id(DM_MODE_GROUPS, parties)};
memcpy(view.parties, parties, sizeof(view.parties));
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) goto sql_error;
for (size_t i = 0; i < count; i++) {
uint8_t* target = data + pos; const struct dm_party* party = parties + targets[i].side;
for (unsigned n = 0; n < 8; n++) target[n] = (uint8_t)(targets[i].node >> (8 * n));
target[8] = targets[i].side;
rc = sqlite3_reset(st);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)targets[i].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) goto sql_error;
memcpy(target + 9, sqlite3_column_blob(st, 0), 32); memcpy(target + 41, sqlite3_column_blob(st, 1), 32);
memset(target + 73, 0, 64);
if (party->kind == DM_PARTY_GROUP) {
struct devgroup_record member;
if (devgroup_read_member(inst->topo_sqlite_db, targets[i].node, &member) != 1 ||
memcmp(member.ed, party->ed, 32) || memcmp(member.x, party->x, 32)) {
sqlite3_finalize(st); return protocol_error("encode membership changed");
}
memcpy(target + 73, member.member_sig, 64);
}
pos += DM_TARGET_PROOF_SIZE;
}
rc = sqlite3_finalize(st); st = NULL;
if (rc != SQLITE_OK) goto sql_error;
if (dm_message_verify_targets(&view)) return -1;
memcpy(out, data, size); *length = size;
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM protocol: encoded context conv=%llu targets=%zu bytes=%zu",
(unsigned long long)view.conv, count, size);
return 0;
sql_error:
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM protocol: encode node query failed rc=%d sql=%s", rc, sqlite3_errmsg(inst->topo_sqlite_db));
sqlite3_finalize(st);
return -1;
}
+57
View File
@@ -0,0 +1,57 @@
/* dm_protocol — единый разбор тела DEVICE/GROUPS и сертификатов адресатов.
* DEVICE сохраняет существующий формат. GROUPS занимает зарезервированный
* ct_len=0 и помещает типизированные стороны/адресатов перед настоящим ct.
* Тело подписывается целиком, включая снимок; parser не проверяет подпись автора.
* Указатели view заимствуют входной буфер. SQL-функция encode вызывается в uasync. */
#ifndef UTUN_DM_PROTOCOL_H
#define UTUN_DM_PROTOCOL_H
#include "dm_identity.h"
#ifdef __cplusplus
extern "C" {
#endif
#define DM_TARGET_PROOF_SIZE 137 /* node:LE64,side:u8,x:32,ed:32,member_sig:64. */
#define DM_CONTEXT_MAX (1 + 2 * DM_PARTY_WIRE_SIZE + 2 + DM_TARGET_MAX * DM_TARGET_PROOF_SIZE)
#define DM_BODY_MAX (33 + DM_CONTEXT_MAX + 1 + 255 + 2 + 1024 + 16 + 56 + 64)
struct dm_message_view {
uint64_t conv, seq, timestamp, author; /* Общий префикс, native-endian. */
enum dm_mode mode;
struct dm_party parties[2]; /* Только GROUPS: сторона автора, другая сторона. */
uint16_t target_count; /* GROUPS: снимок включает автора. DEVICE: 0. */
const uint8_t* targets; /* target_count сертификатов по DM_TARGET_PROOF_SIZE. */
const uint8_t* content_type; /* Не NUL-строка, ровно content_type_size байт. */
uint8_t content_type_size;
const uint8_t* ciphertext; /* ciphertext_size байт, включая CCM tag. */
uint16_t ciphertext_size;
const uint8_t* media; /* UUID/size/hash:56 либо NULL. */
const uint8_t* signature; /* Подпись автора:64. */
size_t size; /* Длина тела; parser допускает хвост после него. */
};
/* 0/-1: проверить границы/форму тела и вернуть view. Выход не меняется при ошибке.
* Проверка точной длины, подписей и адресата — обязанность принимающего модуля. */
int dm_message_parse(const uint8_t* body, size_t available, struct dm_message_view* out);
/* 0/-1: GROUPS проверяет ID беседы, упорядоченный уникальный snapshot, обе стороны,
* наличие автора на стороне 0 и подписи удостоверений. DEVICE — пустая операция:
* ID/подпись конкретной пары проверяет dm_core. Ключи самих устройств связаны с node_id. */
int dm_message_verify_targets(const struct dm_message_view* view);
/* GROUPS: найти node в снимке, 0/1 — сторона, -1 — отсутствует. При успехе
* x/ed (если не NULL) получают по 32 байта. Для DEVICE всегда -1. */
int dm_message_target(const struct dm_message_view* view, uint64_t node, uint8_t x[32], uint8_t ed[32]);
/* 0/-1: сформировать контекст GROUPS (без префикса сообщения/нулевого ct_len).
* VERSION=1:u8, parties:2*73,count:LE16,sorted certificates:count*137.
* Сторона 0 должна включать текущего отправителя. out меняется только при успехе;
* DM_CONTEXT_MAX достаточно. Источник включается, неизвестные устройства не ищутся. */
int dm_context_encode(struct UTUN_INSTANCE* inst, const struct dm_party parties[2],
uint8_t* out, size_t capacity, size_t* length);
#ifdef __cplusplus
}
#endif
#endif