309 lines
17 KiB
C
309 lines
17 KiB
C
/* Реальные A–S–B/C соединения: S хранит только ciphertext. Запросы переживают
|
|
* офлайн автора/restart; B получает ключ после решения, C получает отказ. */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <openssl/sha.h>
|
|
#include "../lib/platform_compat.h"
|
|
#include "../lib/debug_config.h"
|
|
#include "../lib/mem.h"
|
|
#include "../src/utun_instance.h"
|
|
#include "../src/ntp_time.h"
|
|
#include "../src/chat/chat_core.h"
|
|
#include "../src/chat/chat_attachment.h"
|
|
#include "../src/chat/member_sync.h"
|
|
#include "../src/chat/chat_setting.h"
|
|
#include "../src/routing_layer/topo_node_sqlite.h"
|
|
#include "../src/routing_layer/topo_group.h"
|
|
#include "../src/routing_layer/etcp_router.h"
|
|
#include "../src/transport_layer/secure_channel.h"
|
|
#include "../src/transport_layer/etcp.h"
|
|
#include "../src/media_async/media_async.h"
|
|
#include "../src/media_async/attachment_send.h"
|
|
#include "test_utils.h"
|
|
#include "test_clock.h"
|
|
#include "test_net.h"
|
|
#define GROUP 4242424242424244ULL
|
|
#define CHANNEL "4242424242424244"
|
|
#define FILE_BYTES (3 * 32768 + 17)
|
|
enum { A, B, S, M, NODES }; /* M — второй запрашивающий клиент; S — storage+supernode. */
|
|
struct media_test {
|
|
struct UASYNC* ua;
|
|
struct UTUN_INSTANCE* nodes[NODES];
|
|
struct SC_MYKEYS keys[NODES];
|
|
uint64_t ids[NODES];
|
|
uint16_t ports[NODES];
|
|
uint8_t ed[NODES][32], ch_x[32], ch_ed[32], ch_priv[32], ch_sig[64], message[32];
|
|
char dir[512], file[768];
|
|
unsigned phase, ticks, heartbeats, hold;
|
|
int failed;
|
|
void* timer;
|
|
};
|
|
static int value(struct UTUN_INSTANCE* inst, const char* sql) {
|
|
if (!inst) return -1;
|
|
sqlite3_stmt* st = NULL; int result = -1;
|
|
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW)
|
|
result = sqlite3_column_int(st, 0);
|
|
sqlite3_finalize(st); return result;
|
|
}
|
|
static int live(struct UTUN_INSTANCE* inst, uint64_t peer) {
|
|
return inst && etcp_router_get_path(inst, GROUP, peer, ETCP_RT_ID_ATTACHMENT_KEYS).next_hop_node_id != 0;
|
|
}
|
|
static void failure(struct media_test* t, const char* reason) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted attachment test failed phase=%u reason=%s", t->phase, reason);
|
|
t->failed = 1; uasync_stop(t->ua);
|
|
}
|
|
static void hex(const uint8_t* p, size_t len, char* out) {
|
|
for (size_t i = 0; i < len; i++) snprintf(out + i * 2, 3, "%02x", p[i]);
|
|
}
|
|
static int start_node(struct media_test* t, int index) {
|
|
char path[768], pub[65], priv[65], server[65], db[768];
|
|
hex(t->keys[index].public_key, 32, pub); hex(t->keys[index].private_key, 32, priv);
|
|
snprintf(db, sizeof(db), "%s/db%d", t->dir, index); utun_mkdir(db, 0700);
|
|
snprintf(path, sizeof(path), "%s/%d.conf", t->dir, index);
|
|
FILE* f = fopen(path, "w");
|
|
if (!f) return -1;
|
|
fprintf(f, "[global]\nmy_private_key=%s\nmy_public_key=%s\ndb_path=%s\n"
|
|
"[server:s1]\naddr=127.0.0.1:%d\ntype=public\n[allowed_keys]\nallow_all=1\n"
|
|
"[chatserver]\nstorage_autoload=0\n", priv, pub, db, t->ports[index]);
|
|
if (index != S) {
|
|
hex(t->keys[S].public_key, 32, server);
|
|
fprintf(f, "[client:to_s]\npeer_public_key=%s\nlink=s1:127.0.0.1:0\n", server);
|
|
}
|
|
fclose(f);
|
|
t->nodes[index] = utun_instance_create(t->ua, path);
|
|
if (t->nodes[index]) {
|
|
t->ports[index] = ss_get_port(&test_etcp_bound_socket(t->nodes[index], "s1")->local_addr);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "encrypted attachment fixture: node=%d port=%u retained for restart", index, t->ports[index]);
|
|
}
|
|
if (t->nodes[index] && index != S) test_etcp_set_peer(t->nodes[index], "to_s", t->nodes[S], "s1");
|
|
return t->nodes[index] ? utun_instance_init(t->nodes[index]) : -1;
|
|
}
|
|
|
|
static int setup(struct media_test* t) {
|
|
for (int i = 0; i < NODES; i++) {
|
|
struct UTUN_INSTANCE* inst = t->nodes[i];
|
|
if (topo_node_sqlite_channel_put(inst->topo_sqlite_db, CHANNEL, "encrypted-attachment-test", t->ids[A],
|
|
t->ch_x, NULL, t->ch_ed, i == A ? t->ch_priv : NULL, t->ch_sig)) return -1;
|
|
chat_core_ensure_channel_ready(inst, CHANNEL);
|
|
for (int j = 0; j < NODES; j++) {
|
|
uint8_t msg[512], join[64], adm_sig[64], certificate[64];
|
|
uint64_t ts = ntp_time_get_seconds(inst);
|
|
int n = member_sync_build_join_msg(t->ch_x, t->ch_ed, t->ids[j], t->keys[j].public_key, ts, msg, sizeof(msg));
|
|
if (n < 0 || sc_ed25519_sign(t->nodes[j]->my_ed25519_privkey, msg, (size_t)n, join) != SC_OK) return -1;
|
|
const char* tags = j == S ? "{\"supernode\":\"yes\",\"storage\":\"yes\",\"ver\":\"1\"}" : "{\"ver\":\"1\"}";
|
|
size_t off = strlen(tags);
|
|
memcpy(msg, tags, off); memcpy(msg + off, &t->ids[j], 8);
|
|
if (sc_ed25519_sign(t->ch_priv, msg, off + 8, adm_sig) != SC_OK) return -1;
|
|
char info[64]; snprintf(info, sizeof(info), "{\"name\":\"node%d\"}", j);
|
|
if (member_sync_sign_pubkey(t->ch_priv, t->keys[j].public_key, certificate)) return -1;
|
|
if (member_sync_put(inst, CHANNEL, t->ids[j], t->keys[j].public_key, t->ed[j], join, ts,
|
|
NULL, 0, info, tags, adm_sig, 0, 0, certificate) < 0) return -1;
|
|
}
|
|
if (i != S) member_sync_start(inst, t->ids[S], CHANNEL, NULL, NULL);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* Ключи и метаданные до подтверждения отсутствуют даже у скачавшего storage. */
|
|
static int snapshot(struct media_test* t) {
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(t->nodes[A]->topo_sqlite_db, "SELECT message_id FROM attachment_keys WHERE message_id IS NOT NULL", -1, &st, NULL)) return -1;
|
|
int ok = sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32;
|
|
if (ok) memcpy(t->message, sqlite3_column_blob(st, 0), 32);
|
|
sqlite3_finalize(st); return ok ? 0 : -1;
|
|
}
|
|
static int readable(struct media_test* t, int index) {
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(t->nodes[index]->topo_sqlite_db, "SELECT plain_path FROM attachment_keys WHERE ready=1 AND length(key)=32", -1, &st, NULL)) return 0;
|
|
char path[1024] = "";
|
|
if (sqlite3_step(st) == SQLITE_ROW) snprintf(path, sizeof(path), "%s", sqlite3_column_text(st, 0));
|
|
sqlite3_finalize(st);
|
|
FILE* f = path[0] ? fopen(path, "rb") : NULL;
|
|
if (!f) return 0;
|
|
int ok = 1;
|
|
for (unsigned i = 0; i < FILE_BYTES; i++) if (fgetc(f) != (int)((i * 73 + 19) & 255)) { ok = 0; break; }
|
|
if (fgetc(f) != EOF) ok = 0;
|
|
fclose(f); return ok;
|
|
}
|
|
static void download(struct media_test* t, int index) {
|
|
int id = value(t->nodes[index], "SELECT id FROM msg_" CHANNEL " ORDER BY id LIMIT 1");
|
|
if (id <= 0) { failure(t, "history not available before download"); return; }
|
|
chat_core_attachment_download(t->nodes[index], CHANNEL, id);
|
|
}
|
|
/* Вход ПОСЛЕ router позволяет отдельно проверить требования флагов и точной идентичности. */
|
|
static void inject(struct media_test* t, int source, int target, unsigned flags, int corrupt) {
|
|
struct UTUN_INSTANCE* inst = t->nodes[target];
|
|
struct ETCP_CONN* conn = instance_find_conn(inst, t->ids[S]);
|
|
sqlite3_stmt* st = NULL;
|
|
if (!conn || sqlite3_prepare_v2(t->nodes[A]->topo_sqlite_db,
|
|
"SELECT crypto_id,cipher_hash,key FROM attachment_keys", -1, &st, NULL) || sqlite3_step(st) != SQLITE_ROW) {
|
|
sqlite3_finalize(st); failure(t, "control fixture"); return;
|
|
}
|
|
struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + 117);
|
|
if (!e) { sqlite3_finalize(st); failure(t, "control allocation"); return; }
|
|
memset(e->dgram, 0, ROUTER_SVC_PAYLOAD_OFF + 117); e->len = ROUTER_SVC_PAYLOAD_OFF + 117;
|
|
e->dgram[0] = ETCP_RT_ID_ATTACHMENT_KEYS;
|
|
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &t->ids[source], 8); memcpy(e->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8);
|
|
uint64_t group = GROUP; memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8); e->dgram[ROUTER_SVC_FLAGS_OFF] = flags;
|
|
uint8_t* b = e->dgram + ROUTER_SVC_PAYLOAD_OFF;
|
|
memcpy(b, "AK01", 4); b[4] = 3; memcpy(b + 5, t->message, 32);
|
|
memcpy(b + 37, sqlite3_column_blob(st, 0), 16); memcpy(b + 53, sqlite3_column_blob(st, 1), 32);
|
|
memcpy(b + 85, sqlite3_column_blob(st, 2), 32); sqlite3_finalize(st);
|
|
if (corrupt) b[53] ^= 1;
|
|
inst->router_bindings.callbacks[ETCP_RT_ID_ATTACHMENT_KEYS](conn, e);
|
|
}
|
|
static void tick(void* arg) {
|
|
struct media_test* t = arg; t->ticks++; t->heartbeats++;
|
|
struct UTUN_INSTANCE* a = t->nodes[A];
|
|
struct UTUN_INSTANCE* b = t->nodes[B];
|
|
struct UTUN_INSTANCE* s = t->nodes[S];
|
|
struct UTUN_INSTANCE* c = t->nodes[M];
|
|
if (t->ticks % 200 == 0) DEBUG_INFO(DEBUG_CATEGORY_MEDIA,
|
|
"encrypted attachment test waiting phase=%u ticks=%u messages=A:%d B:%d S:%d C:%d", t->phase, t->ticks,
|
|
value(a, "SELECT count(*) FROM msg_" CHANNEL), value(b, "SELECT count(*) FROM msg_" CHANNEL),
|
|
value(s, "SELECT count(*) FROM msg_" CHANNEL), value(c, "SELECT count(*) FROM msg_" CHANNEL));
|
|
switch (t->phase) {
|
|
case 0:
|
|
if (live(a, t->ids[S]) && live(b, t->ids[S]) && live(c, t->ids[S])) {
|
|
if (setup(t)) { failure(t, "signed group members"); return; } t->phase++;
|
|
}
|
|
/* UTUN routes precede CHAT routes; setup once physical links are UP. */
|
|
else if (t->ticks >= 20 && instance_find_conn(a, t->ids[S]) && instance_find_conn(b, t->ids[S]) && instance_find_conn(c, t->ids[S])) {
|
|
if (setup(t)) { failure(t, "signed group members"); return; } t->phase++;
|
|
}
|
|
break;
|
|
case 1:
|
|
if (live(a, t->ids[B]) && live(a, t->ids[M])) {
|
|
if (chat_setting_set(s, "storage_autoload", "1")) { failure(t, "storage autoload setting"); return; }
|
|
struct attachment_send_req* req = u_calloc(1, sizeof(*req));
|
|
if (!req) { failure(t, "send allocation"); return; }
|
|
req->inst = a; req->encrypted = 1; req->info.kind = ATTACHMENT_FILE;
|
|
snprintf(req->target, sizeof(req->target), "%s", CHANNEL);
|
|
snprintf(req->source, sizeof(req->source), "%s", t->file);
|
|
snprintf(req->info.name, sizeof(req->info.name), "private-name.bin");
|
|
attachment_send_trampoline(req); t->phase++;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (value(s, "SELECT count(*) FROM msg_" CHANNEL) == 1 && value(b, "SELECT count(*) FROM msg_" CHANNEL) == 1 &&
|
|
value(c, "SELECT count(*) FROM msg_" CHANNEL) == 1 && !snapshot(t)) {
|
|
if (value(b, "SELECT count(*) FROM msg_" CHANNEL " WHERE instr(CAST(data AS TEXT),'private-name.bin')>0") != 0) {
|
|
failure(t, "plaintext filename leaked into group history"); return;
|
|
}
|
|
t->phase++;
|
|
}
|
|
break;
|
|
case 3:
|
|
if (value(s, "SELECT downloaded FROM attachment_keys") == 1) {
|
|
if (value(s, "SELECT count(*) FROM attachment_key_requests") || value(s, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL")) {
|
|
failure(t, "storage requested or received a key"); return;
|
|
}
|
|
utun_instance_destroy(a); t->nodes[A] = NULL;
|
|
download(t, B); download(t, M); t->phase++;
|
|
}
|
|
break;
|
|
case 4:
|
|
if (value(b, "SELECT count(*) FROM attachment_key_outbox") == 1 && value(c, "SELECT count(*) FROM attachment_key_outbox") == 1) {
|
|
if (readable(t, B) || readable(t, M)) { failure(t, "plaintext before author consent"); return; }
|
|
if (start_node(t, A)) { failure(t, "author restart"); return; } t->phase++;
|
|
}
|
|
break;
|
|
case 5:
|
|
if (value(a, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") == 2) {
|
|
inject(t, S, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 0);
|
|
inject(t, A, B, ROUTER_FLAG_SIGNED, 0);
|
|
inject(t, A, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 1);
|
|
if (value(b, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL") != 0) { failure(t, "forged control accepted"); return; }
|
|
utun_instance_destroy(a); t->nodes[A] = NULL;
|
|
if (start_node(t, A)) { failure(t, "pending request restart"); return; } t->phase++;
|
|
}
|
|
break;
|
|
case 6:
|
|
if (live(a, t->ids[B]) && live(a, t->ids[M])) {
|
|
if (value(a, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") != 2 ||
|
|
chat_attachment_decide(a, CHANNEL, t->message, t->ids[B], 1) ||
|
|
chat_attachment_decide(a, CHANNEL, t->message, t->ids[M], 0)) { failure(t, "author decisions after restart"); return; }
|
|
t->phase++;
|
|
}
|
|
break;
|
|
case 7:
|
|
if (readable(t, B) && value(c, "SELECT state FROM attachment_key_requests WHERE incoming=0") == 3 &&
|
|
value(a, "SELECT count(*) FROM attachment_key_outbox") == 0) {
|
|
if (readable(t, M) || value(c, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL") ||
|
|
value(s, "SELECT count(*) FROM attachment_keys WHERE key IS NOT NULL")) { failure(t, "key disclosed to denied/storage participant"); return; }
|
|
for (int i = 0; i < NODES; i++) {
|
|
if (value(t->nodes[i], "SELECT count(*) FROM media_files WHERE location NOT LIKE '%.uenc'") != 0) {
|
|
failure(t, "plaintext indexed for network distribution"); return;
|
|
}
|
|
}
|
|
utun_instance_destroy(b); t->nodes[B] = NULL;
|
|
if (start_node(t, B)) { failure(t, "recipient restart"); return; } t->phase++;
|
|
}
|
|
break;
|
|
case 8:
|
|
if (live(b, t->ids[A]) && readable(t, B)) {
|
|
inject(t, A, B, ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED, 0);
|
|
if (!readable(t, B) || value(b, "SELECT count(*) FROM attachment_key_requests") != 1 ||
|
|
value(a, "SELECT count(*) FROM attachment_key_requests") != 2) { failure(t, "duplicate grant changed persisted access"); return; }
|
|
t->phase++;
|
|
}
|
|
break;
|
|
case 9:
|
|
if (++t->hold >= 20) { t->phase++; uasync_stop(t->ua); return; }
|
|
break;
|
|
}
|
|
if (t->failed) return;
|
|
if (t->ticks > 12000) { failure(t, "timeout"); return; }
|
|
t->timer = uasync_set_timeout(t->ua, 100, t, tick, "encrypted_attachment_test");
|
|
}
|
|
int main(void) {
|
|
struct media_test t = {0};
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
|
test_clock_init(1000); test_clock_sync_wall_time();
|
|
if (getenv("UTUN_TEST_DEBUG")) {
|
|
debug_set_category_level_by_name("dm", "debug"); debug_set_category_level_by_name("media", "debug");
|
|
debug_set_category_level_by_name("chat_sync", "debug"); debug_set_category_level_by_name("member_sync", "info");
|
|
debug_set_category_level_by_name("bgp", "info"); debug_set_category_level_by_name("etcp_route", "info");
|
|
debug_set_category_level_by_name("connection", "debug"); debug_set_category_level_by_name("etcp", "info");
|
|
}
|
|
utun_instance_set_tun_init_enabled(0);
|
|
snprintf(t.dir, sizeof(t.dir), "/tmp/utun_encrypted_attachment_XXXXXX");
|
|
if (test_mkdtemp(t.dir)) return 1;
|
|
for (int i = 0; i < NODES; i++) {
|
|
if (sc_generate_keypair(&t.keys[i]) || sc_derive_ed25519_pubkey(t.keys[i].private_key, t.ed[i])) return 1;
|
|
t.ids[i] = sc_derive_node_id_from_pubkey(t.keys[i].public_key);
|
|
}
|
|
struct SC_MYKEYS channel;
|
|
if (sc_generate_keypair(&channel)) return 1;
|
|
memcpy(t.ch_x, channel.public_key, 32);
|
|
if (sc_derive_ed25519_pubkey(channel.private_key, t.ch_ed)) return 1;
|
|
uint8_t channel_seed[SHA512_DIGEST_LENGTH];
|
|
SHA512(channel.private_key, 32, channel_seed);
|
|
memcpy(t.ch_priv, channel_seed, 32);
|
|
uint8_t signed_channel[256];
|
|
size_t off = sizeof(CHANNEL);
|
|
memcpy(signed_channel, CHANNEL, off);
|
|
const char name[] = "encrypted-attachment-test";
|
|
memcpy(signed_channel + off, name, sizeof(name)); off += sizeof(name);
|
|
memcpy(signed_channel + off, &t.ids[A], 8); off += 8;
|
|
memcpy(signed_channel + off, t.ch_x, 32); off += 32; memcpy(signed_channel + off, t.ch_ed, 32); off += 32;
|
|
if (sc_ed25519_sign(t.ch_priv, signed_channel, off, t.ch_sig)) return 1;
|
|
snprintf(t.file, sizeof(t.file), "%s/source.bin", t.dir);
|
|
FILE* f = fopen(t.file, "wb");
|
|
if (!f) return 1;
|
|
for (unsigned i = 0; i < FILE_BYTES; i++) fputc((int)((i * 73 + 19) & 255), f);
|
|
fclose(f);
|
|
t.ua = uasync_create();
|
|
if (!t.ua || start_node(&t, S) || start_node(&t, A) || start_node(&t, M) || start_node(&t, B)) return 1;
|
|
t.timer = uasync_set_timeout(t.ua, 100, &t, tick, "dm_media_test");
|
|
test_clock_mainloop(t.ua);
|
|
for (int i = 0; i < NODES; i++) if (t.nodes[i]) utun_instance_destroy(t.nodes[i]);
|
|
uasync_destroy(t.ua, 0);
|
|
test_clock_finish();
|
|
printf("test_encrypted_attachment: phase=%u heartbeats=%u %s artifacts=%s\n", t.phase, t.heartbeats,
|
|
!t.failed && t.phase == 10 ? "PASS" : "FAIL", t.dir);
|
|
return t.failed || t.phase != 10;
|
|
}
|