feat: mutual Realm peer handshake over TCP (0.9.11)

Exchange enrolled certificates and Ed25519 challenge signatures so
same-Realm instances can authenticate each other after discovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-28 22:49:33 +08:00
co-authored by Cursor
parent 5ee82f126d
commit 08ca613a0e
6 changed files with 460 additions and 2 deletions
+2 -1
View File
@@ -123,6 +123,7 @@ set(CST_CORE_SOURCES
src/worktree.c
src/realm.c
src/discover.c
src/peer_auth.c
src/attach.c
src/clipboard.c
src/pdf_text.c
@@ -388,7 +389,7 @@ set(_CST_CORE_TESTS test_llm_body test_prefix_stable test_sse_decode
test_undo test_config test_models test_model_registry test_sandbox
test_keymap test_shell_compact test_skills
test_plugins test_mcp test_subagent test_runtime test_realm test_discover
test_lowil
test_peer_auth test_lowil
test_win_utf8 test_os_dir test_tmpdir
test_tool_meta test_embed)
if(CST_BUILD_WEBUI)
+2
View File
@@ -106,6 +106,7 @@ CORE_SRC = \
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
src/pdf_text.c \
src/session.c src/runtime.c src/worktree.c src/realm.c src/discover.c \
src/peer_auth.c \
src/tools/tools.c \
src/tools/patch.c \
src/tools/registry.c \
@@ -175,6 +176,7 @@ TESTS = \
tests/test_runtime \
tests/test_realm \
tests/test_discover \
tests/test_peer_auth \
tests/test_lowil \
tests/test_win_utf8 \
tests/test_os_dir \
+36
View File
@@ -0,0 +1,36 @@
#ifndef CST_PEER_H
#define CST_PEER_H
#include "cst_realm.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Mutual Realm certificate challenge over TCP (cleartext for now; TLS wrap
* is the next slice). Same Realm public key required on both sides. */
typedef struct {
cst_realm_cert_t remote;
char host[64];
int port;
} cst_peer_session_t;
/* Dial host:port, prove our cert, verify theirs. 0 on success. */
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);
/* Accept one connection on listening socket (from os_net_listen). */
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);
#ifdef __cplusplus
}
#endif
#endif
+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9
#define CST_VERSION_PATCH 10
#define CST_VERSION_PATCH 11
#define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
+334
View File
@@ -0,0 +1,334 @@
#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>
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 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], int timeout_ms,
cst_realm_cert_t *remote) {
if (send_hello(s, self, timeout_ms) != 0)
return -1;
uint8_t nonce[32];
if (read_chal(s, timeout_ms, nonce) != 0)
return -1;
if (send_auth(s, self_sk, nonce, timeout_ms) != 0)
return -1;
if (read_hello(s, realm_pk, timeout_ms, remote) != 0)
return -1;
if (fill_random(nonce, 32) != 0)
return -1;
if (send_chal(s, nonce, timeout_ms) != 0)
return -1;
if (read_auth(s, remote, nonce, timeout_ms) != 0)
return -1;
if (send_ok(s, timeout_ms) != 0 || read_ok(s, timeout_ms) != 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], int timeout_ms,
cst_realm_cert_t *remote) {
if (read_hello(s, realm_pk, timeout_ms, remote) != 0)
return -1;
uint8_t nonce[32];
if (fill_random(nonce, 32) != 0)
return -1;
if (send_chal(s, nonce, timeout_ms) != 0)
return -1;
if (read_auth(s, remote, nonce, timeout_ms) != 0)
return -1;
if (send_hello(s, self, timeout_ms) != 0)
return -1;
if (read_chal(s, timeout_ms, nonce) != 0)
return -1;
if (send_auth(s, self_sk, nonce, timeout_ms) != 0)
return -1;
if (read_ok(s, timeout_ms) != 0 || send_ok(s, timeout_ms) != 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;
os_net_init();
os_socket_t s = os_net_connect(host, port, timeout_ms);
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, timeout_ms, &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;
os_net_close(s);
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;
int timed = 0;
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;
}
cst_realm_cert_t remote;
if (handshake_as_server(s, self, self_sk, realm_pk, timeout_ms, &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;
os_net_close(s);
return 0;
}
+85
View File
@@ -0,0 +1,85 @@
#define _POSIX_C_SOURCE 200809L
#include "cst_net.h"
#include "cst_peer.h"
#include "cst_realm.h"
#include "cst_test_util.h"
#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
typedef struct {
os_socket_t listen;
cst_realm_cert_t cert;
uint8_t sk[64];
uint8_t realm_pk[32];
int ok;
char remote_id[64];
} srv_t;
static void *server_main(void *p) {
srv_t *s = p;
cst_peer_session_t sess;
char err[128];
s->ok = cst_peer_accept((intptr_t)s->listen, &s->cert, s->sk, s->realm_pk,
5000, &sess, err, sizeof(err)) == 0;
if (s->ok)
snprintf(s->remote_id, sizeof(s->remote_id), "%s", sess.remote.instance_id);
return NULL;
}
int main(void) {
os_net_init();
cst_realm_keys_t realm;
assert(cst_realm_generate(&realm) == 0);
cst_realm_cert_t ca, cb;
uint8_t sa[32], ska[64], sb[32], skb[64];
assert(cst_realm_enroll(&realm, "peer-a", "A", 0, &ca, sa, ska) == 0);
assert(cst_realm_enroll(&realm, "peer-b", "B", 0, &cb, sb, skb) == 0);
char err[128];
os_socket_t ls = os_net_listen("127.0.0.1", 0, err, sizeof(err));
assert(ls != OS_SOCKET_BAD);
int port = os_net_port(ls);
assert(port > 0);
srv_t srv = {.listen = ls, .cert = cb};
memcpy(srv.sk, skb, 64);
memcpy(srv.realm_pk, realm.pk, 32);
pthread_t th;
assert(pthread_create(&th, NULL, server_main, &srv) == 0);
cst_peer_session_t sess;
assert(cst_peer_dial("127.0.0.1", port, &ca, ska, realm.pk, 5000, &sess, err,
sizeof(err)) == 0);
assert(!strcmp(sess.remote.instance_id, "peer-b"));
pthread_join(th, NULL);
assert(srv.ok);
assert(!strcmp(srv.remote_id, "peer-a"));
/* Foreign realm must fail. */
cst_realm_keys_t other;
assert(cst_realm_generate(&other) == 0);
cst_realm_cert_t cf;
uint8_t sf[32], skf[64];
assert(cst_realm_enroll(&other, "peer-f", "F", 0, &cf, sf, skf) == 0);
os_socket_t ls2 = os_net_listen("127.0.0.1", 0, err, sizeof(err));
assert(ls2 != OS_SOCKET_BAD);
int port2 = os_net_port(ls2);
srv_t srv2 = {.listen = ls2, .cert = cb};
memcpy(srv2.sk, skb, 64);
memcpy(srv2.realm_pk, realm.pk, 32);
assert(pthread_create(&th, NULL, server_main, &srv2) == 0);
assert(cst_peer_dial("127.0.0.1", port2, &cf, skf, other.pk, 3000, &sess, err,
sizeof(err)) != 0);
pthread_join(th, NULL);
assert(!srv2.ok);
os_net_close(ls);
os_net_close(ls2);
os_net_cleanup();
puts("test_peer_auth: OK");
return 0;
}