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:
+195
-49
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user