#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "cst_peer.h" #include "cJSON.h" #include "cst_net.h" #include "cst_util.h" #include "monocypher.h" #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif /* Wall-clock deadline so a multi-step handshake cannot stretch to * N_steps * timeout_ms (that starved the daemon accept loop). */ static int64_t peer_now_ms(void) { #ifdef _WIN32 return (int64_t)GetTickCount64(); #else struct timeval tv; gettimeofday(&tv, NULL); return (int64_t)tv.tv_sec * 1000 + (int64_t)tv.tv_usec / 1000; #endif } static int peer_remain_ms(int64_t deadline_ms) { int64_t left = deadline_ms - peer_now_ms(); if (left <= 0) return 0; if (left > 600000) return 600000; return (int)left; } static void hex_encode(const uint8_t *in, size_t n, char *out) { static const char *H = "0123456789abcdef"; for (size_t i = 0; i < n; i++) { out[i * 2] = H[in[i] >> 4]; out[i * 2 + 1] = H[in[i] & 15]; } out[n * 2] = 0; } static int hex_nibble(int c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static int hex_decode(const char *s, uint8_t *out, size_t n) { if (!s || strlen(s) != n * 2) return -1; for (size_t i = 0; i < n; i++) { int a = hex_nibble(s[i * 2]), b = hex_nibble(s[i * 2 + 1]); if (a < 0 || b < 0) return -1; out[i] = (uint8_t)((a << 4) | b); } return 0; } static int fill_random(uint8_t *buf, size_t n) { FILE *f = fopen("/dev/urandom", "rb"); if (!f) return -1; size_t got = fread(buf, 1, n, f); fclose(f); return got == n ? 0 : -1; } static int send_line(os_socket_t s, const char *line, int timeout_ms) { size_t n = strlen(line); cst_buf_t b = {0}; if (cst_buf_append(&b, line, n) || cst_buf_puts(&b, "\n")) { cst_buf_free(&b); return -1; } int rc = os_net_send_all(s, b.buf, b.len, timeout_ms); cst_buf_free(&b); return rc; } static char *recv_line(os_socket_t s, int timeout_ms) { cst_buf_t b = {0}; char ch; for (;;) { long r = os_net_recv(s, &ch, 1, timeout_ms); if (r <= 0) { cst_buf_free(&b); return NULL; } if (ch == '\n') break; if (ch != '\r') cst_buf_append(&b, &ch, 1); if (b.len > 65536) { cst_buf_free(&b); return NULL; } } if (!b.buf) cst_buf_puts(&b, ""); return cst_buf_steal(&b); } static int send_hello(os_socket_t s, const cst_realm_cert_t *self, int timeout_ms) { char *cert = cst_realm_cert_to_json(self); if (!cert) return -1; cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "v", 1); cJSON_AddStringToObject(o, "op", "hello"); cJSON *c = cJSON_Parse(cert); free(cert); if (!c) { cJSON_Delete(o); return -1; } cJSON_AddItemToObject(o, "cert", c); char *line = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!line) return -1; int rc = send_line(s, line, timeout_ms); free(line); return rc; } static int read_hello(os_socket_t s, const uint8_t realm_pk[32], int timeout_ms, cst_realm_cert_t *remote) { char *line = recv_line(s, timeout_ms); if (!line) return -1; cJSON *o = cJSON_Parse(line); free(line); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); const cJSON *cert = cJSON_GetObjectItemCaseSensitive(o, "cert"); int ok = 0; if (cJSON_IsString(op) && !strcmp(op->valuestring, "hello") && cert) { char *js = cJSON_PrintUnformatted(cert); if (js && cst_realm_cert_from_json(js, remote) == 0 && cst_realm_verify_cert(realm_pk, remote, (int64_t)time(NULL)) == 0) ok = 1; free(js); } cJSON_Delete(o); return ok ? 0 : -1; } static int send_chal(os_socket_t s, const uint8_t nonce[32], int timeout_ms) { char hx[65]; hex_encode(nonce, 32, hx); cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "v", 1); cJSON_AddStringToObject(o, "op", "chal"); cJSON_AddStringToObject(o, "nonce", hx); char *line = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!line) return -1; int rc = send_line(s, line, timeout_ms); free(line); return rc; } static int read_chal(os_socket_t s, int timeout_ms, uint8_t nonce[32]) { char *line = recv_line(s, timeout_ms); if (!line) return -1; cJSON *o = cJSON_Parse(line); free(line); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); const cJSON *n = cJSON_GetObjectItemCaseSensitive(o, "nonce"); int ok = cJSON_IsString(op) && !strcmp(op->valuestring, "chal") && cJSON_IsString(n) && hex_decode(n->valuestring, nonce, 32) == 0; cJSON_Delete(o); return ok ? 0 : -1; } static int send_auth(os_socket_t s, const uint8_t sk[64], const uint8_t nonce[32], int timeout_ms) { uint8_t sig[64]; if (cst_realm_challenge_sign(sk, nonce, 32, sig) != 0) return -1; char hx[129]; hex_encode(sig, 64, hx); cJSON *o = cJSON_CreateObject(); cJSON_AddNumberToObject(o, "v", 1); cJSON_AddStringToObject(o, "op", "auth"); cJSON_AddStringToObject(o, "sig", hx); char *line = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!line) return -1; int rc = send_line(s, line, timeout_ms); free(line); return rc; } static int read_auth(os_socket_t s, const cst_realm_cert_t *remote, const uint8_t nonce[32], int timeout_ms) { char *line = recv_line(s, timeout_ms); if (!line) return -1; cJSON *o = cJSON_Parse(line); free(line); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); const cJSON *sig = cJSON_GetObjectItemCaseSensitive(o, "sig"); uint8_t raw[64]; int ok = cJSON_IsString(op) && !strcmp(op->valuestring, "auth") && cJSON_IsString(sig) && hex_decode(sig->valuestring, raw, 64) == 0 && cst_realm_challenge_check(remote, nonce, 32, raw) == 0; cJSON_Delete(o); return ok ? 0 : -1; } static int send_ok(os_socket_t s, int timeout_ms) { return send_line(s, "{\"v\":1,\"op\":\"ok\"}", timeout_ms); } static int read_ok(os_socket_t s, int timeout_ms) { char *line = recv_line(s, timeout_ms); if (!line) return -1; cJSON *o = cJSON_Parse(line); free(line); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); int ok = cJSON_IsString(op) && !strcmp(op->valuestring, "ok"); cJSON_Delete(o); return ok ? 0 : -1; } static int send_kx_pk(os_socket_t s, const uint8_t pk[32], int timeout_ms) { char hex[65]; hex_encode(pk, 32, hex); cJSON *o = cJSON_CreateObject(); if (!o) return -1; cJSON_AddNumberToObject(o, "v", 1); cJSON_AddStringToObject(o, "op", "kx"); cJSON_AddStringToObject(o, "pk", hex); char *line = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!line) return -1; int rc = send_line(s, line, timeout_ms); free(line); return rc; } static int read_kx_pk(os_socket_t s, uint8_t pk[32], int timeout_ms) { char *line = recv_line(s, timeout_ms); if (!line) return -1; cJSON *o = cJSON_Parse(line); free(line); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); const cJSON *p = cJSON_GetObjectItemCaseSensitive(o, "pk"); int ok = cJSON_IsString(op) && !strcmp(op->valuestring, "kx") && cJSON_IsString(p) && hex_decode(p->valuestring, pk, 32) == 0; cJSON_Delete(o); return ok ? 0 : -1; } static int peer_kx(os_socket_t s, int as_client, uint8_t out_key[32], int timeout_ms) { uint8_t eph_sk[32], eph_pk[32], their_pk[32], shared[32]; if (fill_random(eph_sk, 32) != 0) return -1; crypto_x25519_public_key(eph_pk, eph_sk); if (as_client) { if (send_kx_pk(s, eph_pk, timeout_ms) != 0 || read_kx_pk(s, their_pk, timeout_ms) != 0) { crypto_wipe(eph_sk, sizeof(eph_sk)); return -1; } } else { if (read_kx_pk(s, their_pk, timeout_ms) != 0 || send_kx_pk(s, eph_pk, timeout_ms) != 0) { crypto_wipe(eph_sk, sizeof(eph_sk)); return -1; } } crypto_x25519(shared, eph_sk, their_pk); crypto_wipe(eph_sk, sizeof(eph_sk)); crypto_blake2b_ctx ctx; crypto_blake2b_init(&ctx, 32); crypto_blake2b_update(&ctx, shared, 32); crypto_blake2b_update(&ctx, (const uint8_t *)"cst-peer-aead-v1", 16); crypto_blake2b_final(&ctx, out_key); crypto_wipe(shared, sizeof(shared)); return 0; } static void ctr_nonce(uint8_t nonce[24], uint64_t ctr) { memset(nonce, 0, 24); for (int i = 0; i < 8; i++) nonce[16 + i] = (uint8_t)((ctr >> (56 - 8 * i)) & 0xff); } static int send_sealed(cst_peer_session_t *sess, const void *plain, size_t n, int timeout_ms) { if (!sess || !sess->sealed || n > 2 * 1024 * 1024) return -1; os_socket_t s = (os_socket_t)sess->sock; uint8_t nonce[24]; ctr_nonce(nonce, sess->send_ctr++); uint8_t *cipher = malloc(n ? n : 1); uint8_t mac[16]; if (!cipher) return -1; crypto_aead_lock(cipher, mac, sess->aead_key, nonce, NULL, 0, (const uint8_t *)plain, n); uint32_t body = 24u + (uint32_t)n + 16u; uint8_t hdr[4] = {(uint8_t)(body >> 24), (uint8_t)(body >> 16), (uint8_t)(body >> 8), (uint8_t)body}; int rc = -1; if (os_net_send_all(s, hdr, 4, timeout_ms) == 0 && os_net_send_all(s, nonce, 24, timeout_ms) == 0 && (n == 0 || os_net_send_all(s, cipher, n, timeout_ms) == 0) && os_net_send_all(s, mac, 16, timeout_ms) == 0) rc = 0; free(cipher); return rc; } static int recv_exact(os_socket_t s, void *buf, size_t n, int timeout_ms) { size_t got = 0; while (got < n) { long r = os_net_recv(s, (char *)buf + got, n - got, timeout_ms); if (r <= 0) return -1; got += (size_t)r; } return 0; } static int recv_sealed(cst_peer_session_t *sess, uint8_t **out, size_t *out_n, int timeout_ms) { *out = NULL; *out_n = 0; if (!sess || !sess->sealed) return -1; os_socket_t s = (os_socket_t)sess->sock; uint8_t hdr[4]; if (recv_exact(s, hdr, 4, timeout_ms) != 0) return -1; uint32_t body = ((uint32_t)hdr[0] << 24) | ((uint32_t)hdr[1] << 16) | ((uint32_t)hdr[2] << 8) | (uint32_t)hdr[3]; if (body < 24 + 16 || body > 24 + 16 + 2 * 1024 * 1024) return -1; size_t n = body - 24 - 16; uint8_t nonce[24], mac[16]; uint8_t *cipher = malloc(n ? n : 1); uint8_t *plain = malloc(n + 1); if (!cipher || !plain) { free(cipher); free(plain); return -1; } if (recv_exact(s, nonce, 24, timeout_ms) != 0 || (n && recv_exact(s, cipher, n, timeout_ms) != 0) || recv_exact(s, mac, 16, timeout_ms) != 0) { free(cipher); free(plain); return -1; } uint8_t expect[24]; ctr_nonce(expect, sess->recv_ctr); if (memcmp(nonce, expect, 24) != 0 || crypto_aead_unlock(plain, mac, sess->aead_key, nonce, NULL, 0, cipher, n) != 0) { free(cipher); free(plain); return -1; } sess->recv_ctr++; free(cipher); plain[n] = 0; *out = plain; *out_n = n; return 0; } static int handshake_as_client(os_socket_t s, const cst_realm_cert_t *self, const uint8_t self_sk[64], const uint8_t realm_pk[32], int64_t deadline_ms, cst_realm_cert_t *remote) { int t; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_hello(s, self, t) != 0) return -1; uint8_t nonce[32]; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_chal(s, t, nonce) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_auth(s, self_sk, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_hello(s, realm_pk, t, remote) != 0) return -1; if (fill_random(nonce, 32) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_chal(s, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_auth(s, remote, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_ok(s, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_ok(s, t) != 0) return -1; return 0; } static int handshake_as_server(os_socket_t s, const cst_realm_cert_t *self, const uint8_t self_sk[64], const uint8_t realm_pk[32], int64_t deadline_ms, cst_realm_cert_t *remote) { int t; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_hello(s, realm_pk, t, remote) != 0) return -1; uint8_t nonce[32]; if (fill_random(nonce, 32) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_chal(s, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_auth(s, remote, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_hello(s, self, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_chal(s, t, nonce) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_auth(s, self_sk, nonce, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || read_ok(s, t) != 0) return -1; if ((t = peer_remain_ms(deadline_ms)) <= 0 || send_ok(s, t) != 0) return -1; return 0; } int cst_peer_dial(const char *host, int port, const cst_realm_cert_t *self, const uint8_t self_sk[64], const uint8_t realm_pk[32], int timeout_ms, cst_peer_session_t *out, char *err, size_t err_sz) { if (err && err_sz) err[0] = 0; if (!host || !self || !self_sk || !realm_pk || !out) return -1; if (timeout_ms <= 0) timeout_ms = 3000; int64_t deadline = peer_now_ms() + timeout_ms; os_net_init(); int t = peer_remain_ms(deadline); os_socket_t s = os_net_connect(host, port, t); if (s == OS_SOCKET_BAD) { if (err && err_sz) snprintf(err, err_sz, "connect failed"); return -1; } cst_realm_cert_t remote; if (handshake_as_client(s, self, self_sk, realm_pk, deadline, &remote) != 0) { if (err && err_sz) snprintf(err, err_sz, "peer handshake failed"); os_net_close(s); return -1; } memset(out, 0, sizeof(*out)); out->remote = remote; snprintf(out->host, sizeof(out->host), "%s", host); out->port = port; out->sock = (intptr_t)s; t = peer_remain_ms(deadline); if (t <= 0 || peer_kx(s, 1, out->aead_key, t) != 0) { if (err && err_sz) snprintf(err, err_sz, "peer key exchange failed"); cst_peer_close(out); return -1; } out->sealed = 1; return 0; } int cst_peer_accept(intptr_t listen_sock, const cst_realm_cert_t *self, const uint8_t self_sk[64], const uint8_t realm_pk[32], int timeout_ms, cst_peer_session_t *out, char *err, size_t err_sz) { if (err && err_sz) err[0] = 0; if (!self || !self_sk || !realm_pk || !out) return -1; if (timeout_ms <= 0) timeout_ms = 3000; int timed = 0; /* Accept wait uses the caller budget; handshake+kx share a fresh deadline * after the connection is taken, so a long accept wait does not shrink the * crypto exchange to zero. */ os_socket_t s = os_net_accept((os_socket_t)listen_sock, timeout_ms, &timed); if (s == OS_SOCKET_BAD) { if (err && err_sz) snprintf(err, err_sz, timed ? "accept timeout" : "accept failed"); return -1; } int64_t deadline = peer_now_ms() + (timeout_ms < 3000 ? 3000 : timeout_ms); cst_realm_cert_t remote; if (handshake_as_server(s, self, self_sk, realm_pk, deadline, &remote) != 0) { if (err && err_sz) snprintf(err, err_sz, "peer handshake failed"); os_net_close(s); return -1; } memset(out, 0, sizeof(*out)); out->remote = remote; os_net_peer(s, out->host, sizeof(out->host)); out->port = 0; out->sock = (intptr_t)s; int t = peer_remain_ms(deadline); if (t <= 0 || peer_kx(s, 0, out->aead_key, t) != 0) { if (err && err_sz) snprintf(err, err_sz, "peer key exchange failed"); cst_peer_close(out); return -1; } out->sealed = 1; return 0; } void cst_peer_close(cst_peer_session_t *s) { if (!s) return; if (s->sock != 0 && s->sock != (intptr_t)OS_SOCKET_BAD) os_net_close((os_socket_t)s->sock); s->sock = (intptr_t)OS_SOCKET_BAD; crypto_wipe(s->aead_key, sizeof(s->aead_key)); s->sealed = 0; s->send_ctr = s->recv_ctr = 0; } static int send_sync_bundle(cst_peer_session_t *sess, const cst_peer_sync_bundle_t *b, int timeout_ms) { cJSON *o = cJSON_CreateObject(); if (!o) return -1; cJSON_AddNumberToObject(o, "v", 1); cJSON_AddStringToObject(o, "op", "sync"); cJSON_AddNumberToObject(o, "revision", (double)b->revision); cJSON_AddStringToObject(o, "parliament_id", b->parliament_id); cJSON_AddStringToObject(o, "project_id", b->project_id); cJSON_AddStringToObject(o, "chair", b->chair_instance); cJSON_AddStringToObject(o, "context", b->context ? b->context : ""); char *txt = cJSON_PrintUnformatted(o); cJSON_Delete(o); if (!txt) return -1; int rc = send_sealed(sess, txt, strlen(txt), timeout_ms); free(txt); return rc; } static int read_sync_bundle(cst_peer_session_t *sess, cst_peer_sync_bundle_t *b, int timeout_ms) { memset(b, 0, sizeof(*b)); uint8_t *plain = NULL; size_t n = 0; if (recv_sealed(sess, &plain, &n, timeout_ms) != 0) return -1; cJSON *o = cJSON_ParseWithLength((char *)plain, n); free(plain); if (!o) return -1; const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); if (!cJSON_IsString(op) || strcmp(op->valuestring, "sync") != 0) { cJSON_Delete(o); return -1; } const cJSON *v; v = cJSON_GetObjectItemCaseSensitive(o, "revision"); if (cJSON_IsNumber(v)) b->revision = (unsigned long)v->valuedouble; v = cJSON_GetObjectItemCaseSensitive(o, "parliament_id"); if (cJSON_IsString(v)) snprintf(b->parliament_id, sizeof(b->parliament_id), "%s", v->valuestring); v = cJSON_GetObjectItemCaseSensitive(o, "project_id"); if (cJSON_IsString(v)) snprintf(b->project_id, sizeof(b->project_id), "%s", v->valuestring); v = cJSON_GetObjectItemCaseSensitive(o, "chair"); if (cJSON_IsString(v)) snprintf(b->chair_instance, sizeof(b->chair_instance), "%s", v->valuestring); v = cJSON_GetObjectItemCaseSensitive(o, "context"); if (cJSON_IsString(v)) b->context = strdup(v->valuestring); else b->context = strdup(""); cJSON_Delete(o); return b->context ? 0 : -1; } int cst_peer_sync(cst_peer_session_t *s, cst_peer_sync_bundle_t *local, int timeout_ms, char *err, size_t err_sz) { if (err && err_sz) err[0] = 0; if (!s || !local || s->sock == (intptr_t)OS_SOCKET_BAD || !s->sealed) { if (err && err_sz) snprintf(err, err_sz, "bad peer session"); return -1; } if (send_sync_bundle(s, local, timeout_ms) != 0) { if (err && err_sz) snprintf(err, err_sz, "sync send failed"); return -1; } cst_peer_sync_bundle_t remote; if (read_sync_bundle(s, &remote, timeout_ms) != 0) { if (err && err_sz) snprintf(err, err_sz, "sync recv failed"); return -1; } if (remote.revision > local->revision) { free(local->context); local->context = remote.context; remote.context = NULL; local->revision = remote.revision; snprintf(local->parliament_id, sizeof(local->parliament_id), "%s", remote.parliament_id); snprintf(local->project_id, sizeof(local->project_id), "%s", remote.project_id); snprintf(local->chair_instance, sizeof(local->chair_instance), "%s", remote.chair_instance); } else { free(remote.context); } return 0; }