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