diff --git a/docs/parliament.md b/docs/parliament.md index e686c03..e2a4365 100644 --- a/docs/parliament.md +++ b/docs/parliament.md @@ -53,13 +53,18 @@ cst-agent --new-project my-app 清空共享上下文、撤销主席与登录票,并停止本机服务。Realm 密钥保留。 +## 传输安全 + +Peer 在 Realm 证书互鉴后做 **临时 X25519 密钥交换**,后续议会上下文同步走 +**XChaCha20-Poly1305**(Monocypher,无需 OpenSSL)。握手前的发现/证书交换仍是明文元数据。 + ## Docker 冒烟 ```bash bash scripts/docker_parliament_test.sh ``` -Alpine 内构建,chair↔member 握手。 +Alpine 内构建,chair↔member 握手 + 密封通道。 ## 相关标志 diff --git a/include/cst_peer.h b/include/cst_peer.h index 0f7e7f1..812bbb5 100644 --- a/include/cst_peer.h +++ b/include/cst_peer.h @@ -8,17 +8,22 @@ 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. */ +/* Mutual Realm certificate challenge over TCP, then ephemeral X25519 + + * XChaCha20-Poly1305 (Monocypher) for the session. Same Realm public key + * required on both sides. */ typedef struct { cst_realm_cert_t remote; char host[64]; int port; 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; -/* 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. */ 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], @@ -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); -/* After handshake: exchange parliament revision/context. Higher revision wins - * and is written locally via the callbacks. Returns 0 on success. */ +/* After handshake: exchange parliament revision/context over the sealed + * channel. Higher revision wins. Returns 0 on success. */ typedef struct { unsigned long revision; char parliament_id[64]; diff --git a/include/cst_version.h b/include/cst_version.h index a4165ed..bc65884 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -3,7 +3,7 @@ #define CST_VERSION_MAJOR 0 #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) CST_VERSION_STR_(x) diff --git a/src/peer_auth.c b/src/peer_auth.c index 9fe03e8..d6bab2b 100644 --- a/src/peer_auth.c +++ b/src/peer_auth.c @@ -223,6 +223,160 @@ static int read_ok(os_socket_t s, int timeout_ms) { 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], 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); out->port = port; 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; } @@ -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)); out->port = 0; 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; } @@ -339,10 +507,13 @@ void cst_peer_close(cst_peer_session_t *s) { 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_hdr(os_socket_t s, const cst_peer_sync_bundle_t *b, - int timeout_ms) { +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; @@ -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, "project_id", b->project_id); cJSON_AddStringToObject(o, "chair", b->chair_instance); - size_t clen = b->context ? strlen(b->context) : 0; - cJSON_AddNumberToObject(o, "context_len", (double)clen); - char *line = cJSON_PrintUnformatted(o); + cJSON_AddStringToObject(o, "context", b->context ? b->context : ""); + char *txt = cJSON_PrintUnformatted(o); cJSON_Delete(o); - if (!line) + if (!txt) return -1; - int rc = send_line(s, line, timeout_ms); - free(line); - if (rc != 0) - return -1; - if (clen > 0 && - os_net_send_all(s, b->context, clen, timeout_ms) != 0) - return -1; - return 0; + int rc = send_sealed(sess, txt, strlen(txt), timeout_ms); + free(txt); + return rc; } -static int read_sync_hdr(os_socket_t s, cst_peer_sync_bundle_t *b, - int timeout_ms) { +static int read_sync_bundle(cst_peer_session_t *sess, cst_peer_sync_bundle_t *b, + int timeout_ms) { memset(b, 0, sizeof(*b)); - char *line = recv_line(s, timeout_ms); - if (!line) + uint8_t *plain = NULL; + size_t n = 0; + if (recv_sealed(sess, &plain, &n, timeout_ms) != 0) return -1; - cJSON *o = cJSON_Parse(line); - free(line); + cJSON *o = cJSON_ParseWithLength((char *)plain, n); + free(plain); if (!o) return -1; 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)) snprintf(b->chair_instance, sizeof(b->chair_instance), "%s", v->valuestring); - size_t clen = 0; - v = cJSON_GetObjectItemCaseSensitive(o, "context_len"); - if (cJSON_IsNumber(v) && v->valuedouble >= 0) - clen = (size_t)v->valuedouble; - cJSON_Delete(o); - if (clen > 2 * 1024 * 1024) - return -1; - if (clen == 0) { + v = cJSON_GetObjectItemCaseSensitive(o, "context"); + if (cJSON_IsString(v)) + b->context = strdup(v->valuestring); + else b->context = strdup(""); - 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; + 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) { + 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; } - os_socket_t sock = (os_socket_t)s->sock; - if (send_sync_hdr(sock, local, timeout_ms) != 0) { + 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_hdr(sock, &remote, timeout_ms) != 0) { + if (read_sync_bundle(s, &remote, timeout_ms) != 0) { if (err && err_sz) snprintf(err, err_sz, "sync recv failed"); return -1; diff --git a/tests/test_peer_auth.c b/tests/test_peer_auth.c index 9df03bd..dfcae6a 100644 --- a/tests/test_peer_auth.c +++ b/tests/test_peer_auth.c @@ -7,6 +7,7 @@ #include #include #include +#include #include typedef struct { @@ -16,6 +17,8 @@ typedef struct { uint8_t realm_pk[32]; int ok; char remote_id[64]; + unsigned long got_rev; + char got_ctx[128]; } srv_t; static void *server_main(void *p) { @@ -26,6 +29,18 @@ static void *server_main(void *p) { 5000, &sess, err, sizeof(err)) == 0; if (s->ok) { 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); } 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, sizeof(err)) == 0); 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); pthread_join(th, NULL); assert(srv.ok); assert(!strcmp(srv.remote_id, "peer-a")); + assert(srv.got_rev == 9); + assert(strstr(srv.got_ctx, "client-wins-secret")); /* Foreign realm must fail. */ cst_realm_keys_t other;