aboutsummaryrefslogtreecommitdiffstats
path: root/serve/deliver.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 19:17:02 +0100
committereunhhu <[email protected]>2026-09-27 19:17:02 +0100
commit6d276be51ba89b04a60bb16b5179c015ad202c7c (patch)
tree1d30ae7bbb2d1db9e9a1b627d0fb76dc26eaf074 /serve/deliver.c
parent313c4fa1720a0019c87083d3523b6643606c5aa6 (diff)
downloadpure-6d276be51ba89b04a60bb16b5179c015ad202c7c.tar.gz
pure-6d276be51ba89b04a60bb16b5179c015ad202c7c.zip
Seal in one session a wire, authenticated by the sender's key (088)
A sealed request cost three X25519 operations (about 0.5 ms), so sealed sends drained at half the plain rate. A wire now keeps one HPKE context, encapsulated at its first seal, and seals each request as the context's next number (a failed seal or a restart starts a new session); the header carries the session's encapsulated key and the number, so a receiver opens a session once per connection thread (16 remembered, the least used replaced) and each seal by its number, also after it restarts, unasked. A sender with a key of its own seals in HPKE auth mode and names that key in the header; a receiver started with --sender NAME=KEY takes deliveries only from the senders it names, each sealed with its key (403 sender otherwise). The body is now the ciphertext and its tag. Sealed sends, sender authenticated, drain at 461-464k a second against 462-470k plain and 207-234k sealed a request at a time (Raspberry Pi 5). The check adds: a wire's requests share one session numbered from 0; a changed number or sender key does not open; a receiver restarted naming its sender opens the same session again; another node claiming that sender's name with its own key is refused and applies nothing.
Diffstat (limited to 'serve/deliver.c')
-rw-r--r--serve/deliver.c94
1 files changed, 65 insertions, 29 deletions
diff --git a/serve/deliver.c b/serve/deliver.c
index 99f2f53..36b39f4 100644
--- a/serve/deliver.c
+++ b/serve/deliver.c
@@ -49,8 +49,13 @@ struct PureWire {
size_t answer_head, answer_count, answer_capacity;
Answer taken; /* the one the owner took last, kept until it takes the next */
uint32_t placed[2 * PURE_MODULE_FIELDS];
- bool sealed; /* the sends go sealed to peer's key */
- unsigned char peer[PURE_SEAL_KEY];
+ /* Sealed sends: to peer's key, authenticated by the node's own key when
+ * it has one (its public half is from), in one session made at the first
+ * seal (and again after one fails); enc opens it. */
+ bool sealed;
+ unsigned char peer[PURE_SEAL_KEY], from[PURE_SEAL_KEY], enc[PURE_SEAL_KEY];
+ const PureSealKey *own;
+ PureSealer *sealer;
};
/* TLS verifies a receiver's certificate against the system's CAs or the
@@ -518,11 +523,57 @@ static bool form_answer(const PureWire *w, int status, const Text *body, const P
static int send_request(PureWire *w, const char *request, size_t size, const Pending *p, Answer *answer);
-void pure_wire_seal(PureWire *w, const unsigned char peer[PURE_SEAL_KEY]) {
+void pure_wire_seal(PureWire *w, const unsigned char peer[PURE_SEAL_KEY], const PureSealKey *own,
+ const unsigned char *from) {
memcpy(w->peer, peer, PURE_SEAL_KEY);
+ w->own = own;
+ if (own) memcpy(w->from, from, PURE_SEAL_KEY);
w->sealed = true;
}
+/* count sends from p as one body: one object, or an array of them. */
+static void put_body(unsigned char *at, const Pending *p, unsigned count) {
+ for (unsigned i = 0; i < count; ++i) {
+ if (count > 1) *at++ = i ? ',' : '[';
+ memcpy(at, p[i].body, p[i].size);
+ at += p[i].size;
+ }
+ if (count > 1) *at = ']';
+}
+
+static void hex(const unsigned char *bytes, size_t size, char *out) {
+ static const char digits[] = "0123456789abcdef";
+ for (size_t i = 0; i < size; ++i) out[2 * i] = digits[bytes[i] >> 4], out[2 * i + 1] = digits[bytes[i] & 15];
+ out[2 * size] = 0;
+}
+
+/* The body sealed whole in the wire's session, bound to its source, first
+ * number and count; the X-Pure-Seal line that opens it (suite, session,
+ * number, and the sender's key when it sealed with its own) into line. */
+static unsigned char *seal_body(PureWire *w, const Pending *p, unsigned count, size_t body, char *line, size_t size) {
+ unsigned char *plain = malloc(body), *sealed = malloc(body + PURE_SEAL_OVERHEAD);
+ char aad[PURE_SOURCE + 48];
+ snprintf(aad, sizeof(aad), "%s %llu %u", w->source, (unsigned long long)p->seq, count);
+ uint64_t n = 0;
+ if (plain && sealed) {
+ put_body(plain, p, count);
+ if (!w->sealer) w->sealer = pure_sealer(w->peer, w->own, w->enc);
+ }
+ bool ok = plain && sealed && w->sealer && pure_seal(w->sealer, aad, plain, body, sealed, &n);
+ free(plain);
+ if (!ok) {
+ pure_sealer_free(w->sealer); /* the next try makes a new session */
+ w->sealer = NULL;
+ free(sealed);
+ return NULL;
+ }
+ char enc[2 * PURE_SEAL_KEY + 1], from[2 * PURE_SEAL_KEY + 2] = "";
+ hex(w->enc, PURE_SEAL_KEY, enc);
+ if (w->own) from[0] = ' ', hex(w->from, PURE_SEAL_KEY, from + 1);
+ snprintf(line, size, "X-Pure-Seal: %s %s %llu%s\r\n", pure_seal_suite(), enc, (unsigned long long)n, from);
+ return sealed;
+}
+
/* One request for count sends from p (numbered in a row): one object, or
* an array of them for a receiver that takes batches. A final answer to a
* send whose answers are wired is made into *answer. */
@@ -530,19 +581,17 @@ static int post(PureWire *w, const Pending *p, unsigned count, Answer *answer) {
size_t body = count > 1 ? count + 1 : 0;
for (unsigned i = 0; i < count; ++i) body += p[i].size;
size_t length = body + (w->sealed ? PURE_SEAL_OVERHEAD : 0);
- char batch[32] = "", seal[48] = "";
+ char batch[32] = "", seal[256] = "";
if (count > 1) snprintf(batch, sizeof(batch), "X-Pure-Count: %u\r\n", count);
- if (w->sealed) snprintf(seal, sizeof(seal), "X-Pure-Seal: %s\r\n", pure_seal_suite());
+ unsigned char *sealed = w->sealed ? seal_body(w, p, count, body, seal, sizeof(seal)) : NULL;
+ if (w->sealed && !sealed) return AGAIN;
/* The request in one piece, its head and then a batch in its brackets:
* one write where there were two a send (a batch drained at 91k sends a
- * second, and at 470k in one piece). Sealed, the body is sealed whole,
- * bound to its source, first number and count. */
- enum { HEAD = 2048 }; /* the longest path, host and source leave room to spare */
+ * second, and at 470k in one piece). */
+ enum { HEAD = 2048 }; /* the longest path, host, source and seal leave room to spare */
char *request = malloc(HEAD + length);
- unsigned char *plain = w->sealed ? malloc(body) : NULL;
- if (!request || (w->sealed && !plain)) {
- free(request);
- free(plain);
+ if (!request) {
+ free(sealed);
return AGAIN;
}
int head = snprintf(request, HEAD,
@@ -550,23 +599,9 @@ static int post(PureWire *w, const Pending *p, unsigned count, Answer *answer) {
"X-Pure-Source: %s\r\nX-Pure-Sequence: %llu\r\n%s%sIdempotency-Key: %s-%llu\r\n\r\n",
w->path, w->host, w->port, length, w->source, (unsigned long long)p->seq, batch, seal, w->source,
(unsigned long long)p->seq);
- unsigned char *at = plain ? plain : (unsigned char *)request + head;
- for (unsigned i = 0; i < count; ++i) {
- if (count > 1) *at++ = i ? ',' : '[';
- memcpy(at, p[i].body, p[i].size);
- at += p[i].size;
- }
- if (count > 1) *at++ = ']';
- if (plain) {
- char aad[PURE_SOURCE + 48];
- snprintf(aad, sizeof(aad), "%s %llu %u", w->source, (unsigned long long)p->seq, count);
- bool sealed = pure_seal(w->peer, aad, plain, body, (unsigned char *)request + head);
- free(plain);
- if (!sealed) {
- free(request);
- return AGAIN;
- }
- }
+ if (sealed) memcpy(request + head, sealed, length);
+ else put_body((unsigned char *)request + head, p, count);
+ free(sealed);
int result = send_request(w, request, (size_t)head + length, p, answer);
free(request);
return result;
@@ -706,6 +741,7 @@ void pure_wire_close(PureWire *w) {
pthread_mutex_unlock(&w->lock);
if (w->started) pthread_join(w->thread, NULL);
pure_tls_close(w->session);
+ pure_sealer_free(w->sealer);
if (w->fd >= 0) close(w->fd);
for (size_t i = 0; i < w->count; ++i) {
free(w->queue[w->head + i].body);