Files
CST-agent/src/peer_auth.c
T
WHCandCursor de2eaf87f7 fix: stop parliament accept starvation on outbound dial (0.9.22)
Peer dial used per-step read timeouts so a stuck handshake could block the
single-threaded daemon for tens of seconds; inbound hellos piled up unread
(CLOSE-WAIT). Use a wall-clock dial budget, drain accepts before each dial,
skip self, and only one side initiates idle polls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 13:19:03 +08:00

662 lines
20 KiB
C

#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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#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;
}