feat: seal peer sync with X25519 + XChaCha20-Poly1305 (0.9.17)

Encrypt parliament context sync after Realm mutual auth without pulling in OpenSSL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 11:16:26 +08:00
co-authored by Cursor
parent a3e115539e
commit 5900b53642
5 changed files with 241 additions and 56 deletions
+6 -1
View File
@@ -53,13 +53,18 @@ cst-agent --new-project my-app
清空共享上下文、撤销主席与登录票,并停止本机服务。Realm 密钥保留。 清空共享上下文、撤销主席与登录票,并停止本机服务。Realm 密钥保留。
## 传输安全
Peer 在 Realm 证书互鉴后做 **临时 X25519 密钥交换**,后续议会上下文同步走
**XChaCha20-Poly1305**(Monocypher,无需 OpenSSL)。握手前的发现/证书交换仍是明文元数据。
## Docker 冒烟 ## Docker 冒烟
```bash ```bash
bash scripts/docker_parliament_test.sh bash scripts/docker_parliament_test.sh
``` ```
Alpine 内构建,chair↔member 握手。 Alpine 内构建,chair↔member 握手 + 密封通道。
## 相关标志 ## 相关标志
+10 -5
View File
@@ -8,17 +8,22 @@
extern "C" { extern "C" {
#endif #endif
/* Mutual Realm certificate challenge over TCP (cleartext for now; TLS wrap /* Mutual Realm certificate challenge over TCP, then ephemeral X25519 +
* is the next slice). Same Realm public key required on both sides. */ * XChaCha20-Poly1305 (Monocypher) for the session. Same Realm public key
* required on both sides. */
typedef struct { typedef struct {
cst_realm_cert_t remote; cst_realm_cert_t remote;
char host[64]; char host[64];
int port; int port;
intptr_t sock; /* open until cst_peer_close */ intptr_t sock; /* open until cst_peer_close */
uint8_t aead_key[32];
uint64_t send_ctr;
uint64_t recv_ctr;
int sealed; /* 1 after key exchange */
} cst_peer_session_t; } cst_peer_session_t;
/* Dial host:port, prove our cert, verify theirs. 0 on success. /* Dial host:port, prove our cert, verify theirs, establish AEAD session.
* Leaves sock open for post-handshake sync; call cst_peer_close. */ * Leaves sock open for post-handshake sync; call cst_peer_close. */
int cst_peer_dial(const char *host, int port, const cst_realm_cert_t *self, 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], const uint8_t self_sk[64], const uint8_t realm_pk[32],
@@ -33,8 +38,8 @@ int cst_peer_accept(intptr_t listen_sock, const cst_realm_cert_t *self,
void cst_peer_close(cst_peer_session_t *s); void cst_peer_close(cst_peer_session_t *s);
/* After handshake: exchange parliament revision/context. Higher revision wins /* After handshake: exchange parliament revision/context over the sealed
* and is written locally via the callbacks. Returns 0 on success. */ * channel. Higher revision wins. Returns 0 on success. */
typedef struct { typedef struct {
unsigned long revision; unsigned long revision;
char parliament_id[64]; char parliament_id[64];
+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0 #define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9 #define CST_VERSION_MINOR 9
#define CST_VERSION_PATCH 16 #define CST_VERSION_PATCH 17
#define CST_VERSION_STR_(x) #x #define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x) #define CST_VERSION_STR(x) CST_VERSION_STR_(x)
+195 -49
View File
@@ -223,6 +223,160 @@ static int read_ok(os_socket_t s, int timeout_ms) {
return ok ? 0 : -1; 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, static int handshake_as_client(os_socket_t s, const cst_realm_cert_t *self,
const uint8_t self_sk[64], const uint8_t self_sk[64],
const uint8_t realm_pk[32], int timeout_ms, const uint8_t realm_pk[32], int timeout_ms,
@@ -299,6 +453,13 @@ int cst_peer_dial(const char *host, int port, const cst_realm_cert_t *self,
snprintf(out->host, sizeof(out->host), "%s", host); snprintf(out->host, sizeof(out->host), "%s", host);
out->port = port; out->port = port;
out->sock = (intptr_t)s; out->sock = (intptr_t)s;
if (peer_kx(s, 1, out->aead_key, timeout_ms) != 0) {
if (err && err_sz)
snprintf(err, err_sz, "peer key exchange failed");
cst_peer_close(out);
return -1;
}
out->sealed = 1;
return 0; return 0;
} }
@@ -330,6 +491,13 @@ int cst_peer_accept(intptr_t listen_sock, const cst_realm_cert_t *self,
os_net_peer(s, out->host, sizeof(out->host)); os_net_peer(s, out->host, sizeof(out->host));
out->port = 0; out->port = 0;
out->sock = (intptr_t)s; out->sock = (intptr_t)s;
if (peer_kx(s, 0, out->aead_key, timeout_ms) != 0) {
if (err && err_sz)
snprintf(err, err_sz, "peer key exchange failed");
cst_peer_close(out);
return -1;
}
out->sealed = 1;
return 0; return 0;
} }
@@ -339,10 +507,13 @@ void cst_peer_close(cst_peer_session_t *s) {
if (s->sock != 0 && s->sock != (intptr_t)OS_SOCKET_BAD) if (s->sock != 0 && s->sock != (intptr_t)OS_SOCKET_BAD)
os_net_close((os_socket_t)s->sock); os_net_close((os_socket_t)s->sock);
s->sock = (intptr_t)OS_SOCKET_BAD; 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_hdr(os_socket_t s, const cst_peer_sync_bundle_t *b, static int send_sync_bundle(cst_peer_session_t *sess,
int timeout_ms) { const cst_peer_sync_bundle_t *b, int timeout_ms) {
cJSON *o = cJSON_CreateObject(); cJSON *o = cJSON_CreateObject();
if (!o) if (!o)
return -1; return -1;
@@ -352,30 +523,25 @@ static int send_sync_hdr(os_socket_t s, const cst_peer_sync_bundle_t *b,
cJSON_AddStringToObject(o, "parliament_id", b->parliament_id); cJSON_AddStringToObject(o, "parliament_id", b->parliament_id);
cJSON_AddStringToObject(o, "project_id", b->project_id); cJSON_AddStringToObject(o, "project_id", b->project_id);
cJSON_AddStringToObject(o, "chair", b->chair_instance); cJSON_AddStringToObject(o, "chair", b->chair_instance);
size_t clen = b->context ? strlen(b->context) : 0; cJSON_AddStringToObject(o, "context", b->context ? b->context : "");
cJSON_AddNumberToObject(o, "context_len", (double)clen); char *txt = cJSON_PrintUnformatted(o);
char *line = cJSON_PrintUnformatted(o);
cJSON_Delete(o); cJSON_Delete(o);
if (!line) if (!txt)
return -1; return -1;
int rc = send_line(s, line, timeout_ms); int rc = send_sealed(sess, txt, strlen(txt), timeout_ms);
free(line); free(txt);
if (rc != 0) return rc;
return -1;
if (clen > 0 &&
os_net_send_all(s, b->context, clen, timeout_ms) != 0)
return -1;
return 0;
} }
static int read_sync_hdr(os_socket_t s, cst_peer_sync_bundle_t *b, static int read_sync_bundle(cst_peer_session_t *sess, cst_peer_sync_bundle_t *b,
int timeout_ms) { int timeout_ms) {
memset(b, 0, sizeof(*b)); memset(b, 0, sizeof(*b));
char *line = recv_line(s, timeout_ms); uint8_t *plain = NULL;
if (!line) size_t n = 0;
if (recv_sealed(sess, &plain, &n, timeout_ms) != 0)
return -1; return -1;
cJSON *o = cJSON_Parse(line); cJSON *o = cJSON_ParseWithLength((char *)plain, n);
free(line); free(plain);
if (!o) if (!o)
return -1; return -1;
const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op"); const cJSON *op = cJSON_GetObjectItemCaseSensitive(o, "op");
@@ -397,51 +563,31 @@ static int read_sync_hdr(os_socket_t s, cst_peer_sync_bundle_t *b,
if (cJSON_IsString(v)) if (cJSON_IsString(v))
snprintf(b->chair_instance, sizeof(b->chair_instance), "%s", snprintf(b->chair_instance, sizeof(b->chair_instance), "%s",
v->valuestring); v->valuestring);
size_t clen = 0; v = cJSON_GetObjectItemCaseSensitive(o, "context");
v = cJSON_GetObjectItemCaseSensitive(o, "context_len"); if (cJSON_IsString(v))
if (cJSON_IsNumber(v) && v->valuedouble >= 0) b->context = strdup(v->valuestring);
clen = (size_t)v->valuedouble; else
cJSON_Delete(o);
if (clen > 2 * 1024 * 1024)
return -1;
if (clen == 0) {
b->context = strdup(""); b->context = strdup("");
return b->context ? 0 : -1; cJSON_Delete(o);
} return b->context ? 0 : -1;
char *ctx = malloc(clen + 1);
if (!ctx)
return -1;
size_t got = 0;
while (got < clen) {
long r = os_net_recv(s, ctx + got, clen - got, timeout_ms);
if (r <= 0) {
free(ctx);
return -1;
}
got += (size_t)r;
}
ctx[clen] = 0;
b->context = ctx;
return 0;
} }
int cst_peer_sync(cst_peer_session_t *s, cst_peer_sync_bundle_t *local, int cst_peer_sync(cst_peer_session_t *s, cst_peer_sync_bundle_t *local,
int timeout_ms, char *err, size_t err_sz) { int timeout_ms, char *err, size_t err_sz) {
if (err && err_sz) if (err && err_sz)
err[0] = 0; err[0] = 0;
if (!s || !local || s->sock == (intptr_t)OS_SOCKET_BAD) { if (!s || !local || s->sock == (intptr_t)OS_SOCKET_BAD || !s->sealed) {
if (err && err_sz) if (err && err_sz)
snprintf(err, err_sz, "bad peer session"); snprintf(err, err_sz, "bad peer session");
return -1; return -1;
} }
os_socket_t sock = (os_socket_t)s->sock; if (send_sync_bundle(s, local, timeout_ms) != 0) {
if (send_sync_hdr(sock, local, timeout_ms) != 0) {
if (err && err_sz) if (err && err_sz)
snprintf(err, err_sz, "sync send failed"); snprintf(err, err_sz, "sync send failed");
return -1; return -1;
} }
cst_peer_sync_bundle_t remote; cst_peer_sync_bundle_t remote;
if (read_sync_hdr(sock, &remote, timeout_ms) != 0) { if (read_sync_bundle(s, &remote, timeout_ms) != 0) {
if (err && err_sz) if (err && err_sz)
snprintf(err, err_sz, "sync recv failed"); snprintf(err, err_sz, "sync recv failed");
return -1; return -1;
+29
View File
@@ -7,6 +7,7 @@
#include <assert.h> #include <assert.h>
#include <pthread.h> #include <pthread.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
typedef struct { typedef struct {
@@ -16,6 +17,8 @@ typedef struct {
uint8_t realm_pk[32]; uint8_t realm_pk[32];
int ok; int ok;
char remote_id[64]; char remote_id[64];
unsigned long got_rev;
char got_ctx[128];
} srv_t; } srv_t;
static void *server_main(void *p) { static void *server_main(void *p) {
@@ -26,6 +29,18 @@ static void *server_main(void *p) {
5000, &sess, err, sizeof(err)) == 0; 5000, &sess, err, sizeof(err)) == 0;
if (s->ok) { if (s->ok) {
snprintf(s->remote_id, sizeof(s->remote_id), "%s", sess.remote.instance_id); snprintf(s->remote_id, sizeof(s->remote_id), "%s", sess.remote.instance_id);
assert(sess.sealed);
cst_peer_sync_bundle_t local = {0};
local.revision = 1;
snprintf(local.parliament_id, sizeof(local.parliament_id), "parl-s");
snprintf(local.project_id, sizeof(local.project_id), "proj");
snprintf(local.chair_instance, sizeof(local.chair_instance), "peer-b");
local.context = strdup("server-old");
assert(cst_peer_sync(&sess, &local, 5000, err, sizeof(err)) == 0);
s->got_rev = local.revision;
snprintf(s->got_ctx, sizeof(s->got_ctx), "%s",
local.context ? local.context : "");
free(local.context);
cst_peer_close(&sess); cst_peer_close(&sess);
} }
return NULL; return NULL;
@@ -57,10 +72,24 @@ int main(void) {
assert(cst_peer_dial("127.0.0.1", port, &ca, ska, realm.pk, 5000, &sess, err, assert(cst_peer_dial("127.0.0.1", port, &ca, ska, realm.pk, 5000, &sess, err,
sizeof(err)) == 0); sizeof(err)) == 0);
assert(!strcmp(sess.remote.instance_id, "peer-b")); assert(!strcmp(sess.remote.instance_id, "peer-b"));
assert(sess.sealed);
cst_peer_sync_bundle_t local = {0};
local.revision = 9;
snprintf(local.parliament_id, sizeof(local.parliament_id), "parl-c");
snprintf(local.project_id, sizeof(local.project_id), "proj");
snprintf(local.chair_instance, sizeof(local.chair_instance), "peer-a");
local.context = strdup("client-wins-secret");
assert(cst_peer_sync(&sess, &local, 5000, err, sizeof(err)) == 0);
/* Client already has higher rev; stays. */
assert(local.revision == 9);
assert(strstr(local.context, "client-wins-secret"));
free(local.context);
cst_peer_close(&sess); cst_peer_close(&sess);
pthread_join(th, NULL); pthread_join(th, NULL);
assert(srv.ok); assert(srv.ok);
assert(!strcmp(srv.remote_id, "peer-a")); assert(!strcmp(srv.remote_id, "peer-a"));
assert(srv.got_rev == 9);
assert(strstr(srv.got_ctx, "client-wins-secret"));
/* Foreign realm must fail. */ /* Foreign realm must fail. */
cst_realm_keys_t other; cst_realm_keys_t other;