diff options
| author | eunhhu <[email protected]> | 2026-09-27 19:17:02 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 19:17:02 +0100 |
| commit | 6d276be51ba89b04a60bb16b5179c015ad202c7c (patch) | |
| tree | 1d30ae7bbb2d1db9e9a1b627d0fb76dc26eaf074 /serve | |
| parent | 313c4fa1720a0019c87083d3523b6643606c5aa6 (diff) | |
| download | pure-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')
| -rw-r--r-- | serve/crypto-openssl.c | 89 | ||||
| -rw-r--r-- | serve/crypto.c | 26 | ||||
| -rw-r--r-- | serve/crypto.h | 16 | ||||
| -rw-r--r-- | serve/deliver.c | 94 | ||||
| -rw-r--r-- | serve/deliver.h | 9 | ||||
| -rw-r--r-- | serve/seal.h | 53 | ||||
| -rw-r--r-- | serve/serve.c | 145 |
7 files changed, 323 insertions, 109 deletions
diff --git a/serve/crypto-openssl.c b/serve/crypto-openssl.c index 38b4628..127ae38 100644 --- a/serve/crypto-openssl.c +++ b/serve/crypto-openssl.c @@ -56,11 +56,14 @@ static int tls_read(PureTls *tls, void *bytes, int size) { return SSL_read((SSL static int tls_write(PureTls *tls, const void *bytes, int size) { return SSL_write((SSL *)tls, bytes, size); } static void tls_close(PureTls *tls) { SSL_free((SSL *)tls); } -/* ---- Sealing: HPKE base mode (RFC 9180), X25519 and HKDF-SHA256, with the - * AEAD the suite names. */ +/* ---- Sealing: HPKE (RFC 9180), X25519 and HKDF-SHA256, with the AEAD the + * suite names; auth mode when the sender seals with a key of its own. A + * session is one encapsulation, its seals numbered by the context. */ struct PureSealKey { EVP_PKEY *key; }; +struct PureSealer { OSSL_HPKE_CTX *context; }; +struct PureOpener { OSSL_HPKE_CTX *context; }; -static const unsigned char info[] = "pure sealed send 1"; +static const unsigned char info[] = "pure sealed send 2"; static bool suite_named(const char *name, OSSL_HPKE_SUITE *out) { OSSL_HPKE_SUITE suite = {OSSL_HPKE_KEM_ID_X25519, OSSL_HPKE_KDF_ID_HKDF_SHA256, 0}; @@ -120,34 +123,68 @@ static void seal_key_free(PureSealKey *key) { free(key); } -static bool seal(const char *suite_name, const unsigned char peer[PURE_SEAL_KEY], const char *aad, - const unsigned char *plain, size_t size, unsigned char *out) { +static PureSealer *sealer(const char *suite_name, const unsigned char peer[PURE_SEAL_KEY], const PureSealKey *own, + unsigned char enc[PURE_SEAL_KEY]) { OSSL_HPKE_SUITE suite; - if (!suite_named(suite_name, &suite)) return false; - OSSL_HPKE_CTX *context = OSSL_HPKE_CTX_new(OSSL_HPKE_MODE_BASE, suite, OSSL_HPKE_ROLE_SENDER, NULL, NULL); - size_t encapsulated = PURE_SEAL_KEY, sealed = size + 16; - bool ok = context && OSSL_HPKE_encap(context, out, &encapsulated, peer, PURE_SEAL_KEY, info, sizeof(info) - 1) == 1 && - encapsulated == PURE_SEAL_KEY && - OSSL_HPKE_seal(context, out + PURE_SEAL_KEY, &sealed, (const unsigned char *)aad, strlen(aad), plain, size) == 1 && - sealed == size + 16; - OSSL_HPKE_CTX_free(context); - return ok; + PureSealer *made = suite_named(suite_name, &suite) ? calloc(1, sizeof(*made)) : NULL; + if (!made) return NULL; + made->context = OSSL_HPKE_CTX_new(own ? OSSL_HPKE_MODE_AUTH : OSSL_HPKE_MODE_BASE, suite, OSSL_HPKE_ROLE_SENDER, + NULL, NULL); + size_t length = PURE_SEAL_KEY; + if (!made->context || (own && OSSL_HPKE_CTX_set1_authpriv(made->context, own->key) != 1) || + OSSL_HPKE_encap(made->context, enc, &length, peer, PURE_SEAL_KEY, info, sizeof(info) - 1) != 1 || + length != PURE_SEAL_KEY) { + OSSL_HPKE_CTX_free(made->context); + free(made); + return NULL; + } + return made; +} + +static bool seal(PureSealer *s, const char *aad, const unsigned char *plain, size_t size, unsigned char *out, + uint64_t *n) { + size_t sealed = size + PURE_SEAL_OVERHEAD; + return OSSL_HPKE_CTX_get_seq(s->context, n) == 1 && + OSSL_HPKE_seal(s->context, out, &sealed, (const unsigned char *)aad, strlen(aad), plain, size) == 1 && + sealed == size + PURE_SEAL_OVERHEAD; +} + +static void sealer_free(PureSealer *s) { + if (!s) return; + OSSL_HPKE_CTX_free(s->context); + free(s); } -static bool unseal(const PureSealKey *key, const char *suite_name, const char *aad, const unsigned char *sealed, - size_t size, unsigned char *plain) { +static PureOpener *opener(const PureSealKey *key, const char *suite_name, const unsigned char enc[PURE_SEAL_KEY], + const unsigned char *from) { OSSL_HPKE_SUITE suite; - if (!key || size < PURE_SEAL_OVERHEAD || !suite_named(suite_name, &suite)) return false; - OSSL_HPKE_CTX *context = OSSL_HPKE_CTX_new(OSSL_HPKE_MODE_BASE, suite, OSSL_HPKE_ROLE_RECEIVER, NULL, NULL); + PureOpener *made = key && suite_named(suite_name, &suite) ? calloc(1, sizeof(*made)) : NULL; + if (!made) return NULL; + made->context = OSSL_HPKE_CTX_new(from ? OSSL_HPKE_MODE_AUTH : OSSL_HPKE_MODE_BASE, suite, OSSL_HPKE_ROLE_RECEIVER, + NULL, NULL); + if (!made->context || (from && OSSL_HPKE_CTX_set1_authpub(made->context, from, PURE_SEAL_KEY) != 1) || + OSSL_HPKE_decap(made->context, enc, PURE_SEAL_KEY, key->key, info, sizeof(info) - 1) != 1) { + OSSL_HPKE_CTX_free(made->context); + free(made); + return NULL; + } + return made; +} + +/* A receiver may set the number (a sender may not: it would reuse a nonce). */ +static bool unseal(PureOpener *o, uint64_t n, const char *aad, const unsigned char *sealed, size_t size, + unsigned char *plain) { size_t opened = size - PURE_SEAL_OVERHEAD; - bool ok = context && - OSSL_HPKE_decap(context, sealed, PURE_SEAL_KEY, key->key, info, sizeof(info) - 1) == 1 && - OSSL_HPKE_open(context, plain, &opened, (const unsigned char *)aad, strlen(aad), sealed + PURE_SEAL_KEY, - size - PURE_SEAL_KEY) == 1 && - opened == size - PURE_SEAL_OVERHEAD; - OSSL_HPKE_CTX_free(context); - return ok; + return size >= PURE_SEAL_OVERHEAD && OSSL_HPKE_CTX_set_seq(o->context, n) == 1 && + OSSL_HPKE_open(o->context, plain, &opened, (const unsigned char *)aad, strlen(aad), sealed, size) == 1 && + opened == size - PURE_SEAL_OVERHEAD; +} + +static void opener_free(PureOpener *o) { + if (!o) return; + OSSL_HPKE_CTX_free(o->context); + free(o); } const PureCrypto pure_crypto = {PURE_CRYPTO_VERSION, tls_start, tls_open, tls_read, tls_write, tls_close, - seal_key, seal_key_free, seal, unseal}; + seal_key, seal_key_free, sealer, seal, sealer_free, opener, unseal, opener_free}; diff --git a/serve/crypto.c b/serve/crypto.c index 8806ad2..8eae6d1 100644 --- a/serve/crypto.c +++ b/serve/crypto.c @@ -106,16 +106,30 @@ PureSealKey *pure_seal_key(const char *path, char public_hex[2 * PURE_SEAL_KEY + return c ? c->seal_key(path, public_hex, error, size) : NULL; } -/* A key exists only once the table is loaded. */ +/* A key or a session exists only once the table is loaded. */ void pure_seal_key_free(PureSealKey *key) { if (key) crypto->seal_key_free(key); } -bool pure_seal(const unsigned char peer[PURE_SEAL_KEY], const char *aad, const unsigned char *plain, size_t size, - unsigned char *out) { +PureSealer *pure_sealer(const unsigned char peer[PURE_SEAL_KEY], const PureSealKey *own, + unsigned char enc[PURE_SEAL_KEY]) { const PureCrypto *c = loaded(NULL, 0); - return c && c->seal(pure_seal_suite(), peer, aad, plain, size, out); + return c ? c->sealer(pure_seal_suite(), peer, own, enc) : NULL; } -bool pure_unseal(const PureSealKey *key, const char *suite, const char *aad, const unsigned char *sealed, size_t size, +bool pure_seal(PureSealer *s, const char *aad, const unsigned char *plain, size_t size, unsigned char *out, + uint64_t *n) { + return crypto->seal(s, aad, plain, size, out, n); +} + +void pure_sealer_free(PureSealer *s) { if (s) crypto->sealer_free(s); } + +PureOpener *pure_opener(const PureSealKey *key, const char *suite, const unsigned char enc[PURE_SEAL_KEY], + const unsigned char *from) { + return key ? crypto->opener(key, suite, enc, from) : NULL; +} + +bool pure_unseal(PureOpener *o, uint64_t n, const char *aad, const unsigned char *sealed, size_t size, unsigned char *plain) { - return key && crypto->unseal(key, suite, aad, sealed, size, plain); + return crypto->unseal(o, n, aad, sealed, size, plain); } + +void pure_opener_free(PureOpener *o) { if (o) crypto->opener_free(o); } diff --git a/serve/crypto.h b/serve/crypto.h index 5083b23..4919c49 100644 --- a/serve/crypto.h +++ b/serve/crypto.h @@ -8,7 +8,7 @@ * first time a server seals, opens a seal or connects to an https receiver * (serve/crypto.c): one that does none of these maps no OpenSSL, 2.3 MiB * less resident and 0.6 MiB less of its own. */ -enum { PURE_CRYPTO_VERSION = 1 }; +enum { PURE_CRYPTO_VERSION = 2 }; typedef struct { unsigned version; @@ -20,12 +20,18 @@ typedef struct { int (*tls_read)(PureTls *, void *bytes, int size); int (*tls_write)(PureTls *, const void *bytes, int size); void (*tls_close)(PureTls *); - /* Sealing, as serve/seal.h says, in the suite named. */ + /* Sealing, as serve/seal.h says; a sender's sessions in the suite named. */ PureSealKey *(*seal_key)(const char *path, char public_hex[2 * PURE_SEAL_KEY + 1], char *error, size_t size); void (*seal_key_free)(PureSealKey *); - bool (*seal)(const char *suite, const unsigned char peer[PURE_SEAL_KEY], const char *aad, - const unsigned char *plain, size_t size, unsigned char *out); - bool (*unseal)(const PureSealKey *, const char *suite, const char *aad, const unsigned char *sealed, size_t size, + PureSealer *(*sealer)(const char *suite, const unsigned char peer[PURE_SEAL_KEY], const PureSealKey *own, + unsigned char enc[PURE_SEAL_KEY]); + bool (*seal)(PureSealer *, const char *aad, const unsigned char *plain, size_t size, unsigned char *out, + uint64_t *n); + void (*sealer_free)(PureSealer *); + PureOpener *(*opener)(const PureSealKey *, const char *suite, const unsigned char enc[PURE_SEAL_KEY], + const unsigned char *from); + bool (*unseal)(PureOpener *, uint64_t n, const char *aad, const unsigned char *sealed, size_t size, unsigned char *plain); + void (*opener_free)(PureOpener *); } PureCrypto; #endif 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); diff --git a/serve/deliver.h b/serve/deliver.h index 70b03b9..f7427c5 100644 --- a/serve/deliver.h +++ b/serve/deliver.h @@ -1,6 +1,7 @@ #ifndef PURE_DELIVER_H #define PURE_DELIVER_H #include "module.h" +#include "seal.h" /* Delivering an app's saved sends over HTTP (docs/research/083-delivery-over-http.md). * A wire takes one output's sends in order: each goes as a JSON object of @@ -23,10 +24,12 @@ void pure_wire_trust(const char *ca_file); PureWire *pure_wire_open(const PureModule *m, const char *output, const char *url, const char *source, char *error, size_t size); uint32_t pure_wire_output(const PureWire *); -/* Seal the wire's sends to the receiver's public key (serve/seal.h): each +/* Seal the wire's sends to the receiver's public key (serve/seal.h), + * authenticated by own (whose public half is from) when given: each * request's body, bound to its source, first number and count, goes with - * X-Pure-Seal naming the suite. Before pure_wire_start. */ -void pure_wire_seal(PureWire *, const unsigned char peer[32]); + * X-Pure-Seal naming the suite, the session, the seal's number and the + * sender's key. Before pure_wire_start. */ +void pure_wire_seal(PureWire *, const unsigned char peer[32], const PureSealKey *own, const unsigned char *from); /* The owner's side: queue a saved send numbered after the last queued one * (it is encoded now); false when it cannot be (a bytes field that is not * UTF-8 has no JSON form), and the wire then stops at it. */ diff --git a/serve/seal.h b/serve/seal.h index 663d860..a020e0d 100644 --- a/serve/seal.h +++ b/serve/seal.h @@ -2,34 +2,55 @@ #define PURE_SEAL_H #include <stdbool.h> #include <stddef.h> +#include <stdint.h> -/* Sends sealed end to end (docs/research/086-sealed-sends.md): a receiving - * node's key opens what a sender sealed to its public key, and nothing - * between them (a relay, a proxy, a mailbox) can read or change it. HPKE - * (RFC 9180) base mode: X25519, HKDF-SHA256, and AES-128-GCM where the CPU - * has AES instructions, else ChaCha20-Poly1305. A private key lives in +/* Sends sealed end to end (docs/research/086-sealed-sends.md, 088): a + * receiving node's key opens what a sender sealed to its public key, and + * nothing between them (a relay, a proxy, a mailbox) can read or change it. + * HPKE (RFC 9180): X25519, HKDF-SHA256, and AES-128-GCM where the CPU has AES + * instructions, else ChaCha20-Poly1305; auth mode when the sender has a key + * of its own, so the receiver knows who sealed. A sender keeps one session + * (one encapsulation) and numbers its seals in it; a receiver opens a + * session once and then each seal by its number. A private key lives in * OpenSSL's secure heap: locked in memory, out of core dumps, cleared when * freed. OpenSSL comes with pure-crypto.so (serve/crypto.c), mapped the - * first time a key is read or a send sealed. */ -enum { PURE_SEAL_KEY = 32, PURE_SEAL_OVERHEAD = 32 + 16 }; /* the encapsulated key, then the tag */ + * first time a key is read or a session made. */ +enum { PURE_SEAL_KEY = 32, PURE_SEAL_OVERHEAD = 16 }; /* a public key; a seal's tag */ typedef struct PureSealKey PureSealKey; +typedef struct PureSealer PureSealer; /* a sender's session */ +typedef struct PureOpener PureOpener; /* a receiver's session */ + +/* Whether this machine can seal (pure-crypto.so maps), with why not in error. */ +bool pure_seal_ready(char *error, size_t size); /* A node's key: read from path, or made and written there (0600) when there * is none; its public half in hex. NULL with why in error. */ PureSealKey *pure_seal_key(const char *path, char public_hex[2 * PURE_SEAL_KEY + 1], char *error, size_t size); void pure_seal_key_free(PureSealKey *); -/* Whether this machine can seal (pure-crypto.so maps), with why not in error. */ -bool pure_seal_ready(char *error, size_t size); /* A public key from its hex. */ bool pure_seal_peer(const char *hex, unsigned char out[PURE_SEAL_KEY]); /* The suite this machine seals with, as the X-Pure-Seal header names it. */ const char *pure_seal_suite(void); -/* plain sealed to peer, bound to aad: size + PURE_SEAL_OVERHEAD bytes into out. */ -bool pure_seal(const unsigned char peer[PURE_SEAL_KEY], const char *aad, const unsigned char *plain, size_t size, - unsigned char *out); -/* Opened with key under suite (either this machine's or the other known one): - * size - PURE_SEAL_OVERHEAD bytes into plain; false when it does not open - * (another key, another aad, or changed on the way). */ -bool pure_unseal(const PureSealKey *, const char *suite, const char *aad, const unsigned char *sealed, size_t size, + +/* A session sealing to peer, authenticated by own when given: its + * encapsulated key into enc (what a receiver opens the session with). */ +PureSealer *pure_sealer(const unsigned char peer[PURE_SEAL_KEY], const PureSealKey *own, + unsigned char enc[PURE_SEAL_KEY]); +/* plain sealed as the session's next seal, bound to aad: size + + * PURE_SEAL_OVERHEAD bytes into out, and its number into n. */ +bool pure_seal(PureSealer *, const char *aad, const unsigned char *plain, size_t size, unsigned char *out, + uint64_t *n); +void pure_sealer_free(PureSealer *); + +/* A session a sender made to key's public half, in suite (this machine's or + * the other known one), from its enc; from is the sender's public key when + * it sealed with its own (NULL otherwise). NULL when it does not open. */ +PureOpener *pure_opener(const PureSealKey *key, const char *suite, const unsigned char enc[PURE_SEAL_KEY], + const unsigned char *from); +/* The session's seal number n: size - PURE_SEAL_OVERHEAD bytes into plain; + * false when it does not open (another session, another aad, or changed on + * the way). */ +bool pure_unseal(PureOpener *, uint64_t n, const char *aad, const unsigned char *sealed, size_t size, unsigned char *plain); +void pure_opener_free(PureOpener *); #endif diff --git a/serve/serve.c b/serve/serve.c index 5088d8a..0e60e2a 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -71,9 +71,27 @@ typedef enum { ROUTE_NONE, ROUTE_OBJECT, ROUTE_VIEW, ROUTE_INPUT } RouteKind; typedef struct { RouteKind kind; uint32_t target; size_t from, count; } Route; /* This node's key, for deliveries sealed to it (--key), and whether it - * takes only those (--sealed-only). */ + * takes only those (--sealed-only), or only those from the senders named, + * each by the key it seals with (--sender NAME=KEY). */ static PureSealKey *seal_key; static bool sealed_only; +static struct Sender { char name[PURE_SOURCE + 1]; unsigned char key[PURE_SEAL_KEY]; } senders[PURE_MODULE_INPUTS]; +static unsigned sender_count; + +/* A delivery's X-Pure-Seal: the suite, the sender's session (its + * encapsulated key), the seal's number in it and, when the sender sealed + * with a key of its own, that key. */ +typedef struct { + char suite[32]; /* empty: not sealed */ + unsigned char enc[PURE_SEAL_KEY], from[PURE_SEAL_KEY]; + uint64_t n; + bool authenticated; +} Seal; + +/* Sessions a connection thread has opened, the least used replaced first: + * a sender's key agreement is made once a session, not once a request. */ +enum { SESSIONS = 16 }; +typedef struct { Seal seal; PureOpener *opener; uint64_t used; } Session; /* Allocated when accepted and reused after closing: memory follows the * connections that are open, not the configured maximum. */ @@ -101,10 +119,11 @@ typedef struct Connection { char source[PURE_SOURCE + 1]; /* a delivered message's sender (X-Pure-Source), or empty */ uint64_t sequence; /* and its number (X-Pure-Sequence) */ unsigned batch; /* how many numbered messages the body holds (X-Pure-Count) */ - /* A sealed delivery: the suite it names (X-Pure-Seal), what the seal is - * bound to (source, number, count), and how many plain bytes the body - * holds once opened, cleared when its ticket goes. */ - char seal[40], aad[PURE_SOURCE + 48]; + /* A sealed delivery: its seal (X-Pure-Seal), what the seal is bound to + * (source, number, count), and how many plain bytes the body holds once + * opened, cleared when its ticket goes. */ + Seal seal; + char aad[PURE_SOURCE + 48]; size_t opened; int64_t expected; size_t body_size, received, in_used; @@ -207,6 +226,8 @@ struct Host { uint64_t timeout_ms; uint64_t now; /* the monotonic clock in ms, read once a round: deadlines count from it */ uint64_t accepted, requests, expired; + Session sessions[SESSIONS]; + uint64_t session_uses; /* What the owner hands back to answer (swapped for answering), and the * eventfd that says so; pending counts requests at the owner. */ pthread_mutex_t lock; @@ -416,6 +437,34 @@ static void answer_app(Connection *c, PureOutcome result, bool view, PureFault f if (!result.body.size) release_ticket(c); } +/* "SUITE ENC N[ FROM]": the keys as 64 hex digits, the number in decimal. */ +static bool read_seal(const char *value, Seal *seal) { + char copy[256], *rest, *suite = NULL, *enc = NULL, *n = NULL, *from = NULL, *extra = NULL; + if (strlen(value) >= sizeof(copy)) return false; + strcpy(copy, value); + suite = strtok_r(copy, " ", &rest); + if (suite) enc = strtok_r(NULL, " ", &rest); + if (enc) n = strtok_r(NULL, " ", &rest); + if (n) from = strtok_r(NULL, " ", &rest); + if (from) extra = strtok_r(NULL, " ", &rest); + uint64_t sequence; + if (!n || extra || strlen(suite) >= sizeof(seal->suite) || !pure_seal_peer(enc, seal->enc) || + !number(n, UINT64_MAX, &sequence) || (from && !pure_seal_peer(from, seal->from))) + return false; + strcpy(seal->suite, suite); + seal->n = sequence; + seal->authenticated = from != NULL; + return true; +} + +/* A sender this node names, sealing with the key it was named with. */ +static bool known_sender(const char *source, const Seal *seal) { + for (unsigned i = 0; i < sender_count; ++i) + if (!strcmp(senders[i].name, source)) + return seal->authenticated && !memcmp(senders[i].key, seal->from, PURE_SEAL_KEY); + return false; +} + /* Parse one request head of `size` bytes (through the blank line): the same * bounded HTTP/1.1 subset as server.c, plus Connection, POST and the app * routes. A query stays in head for start_request; an unknown path is @@ -464,7 +513,7 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { c->source[0] = 0; c->sequence = 0; c->batch = 1; - c->seal[0] = 0; + c->seal.suite[0] = 0; bool counted = false; c->body_size = 0; c->json = false; @@ -517,9 +566,8 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { if (c->sequence || !number(value, UINT64_MAX, &n) || !n) return false; c->sequence = n; } else if (!strcasecmp(line, "X-Pure-Seal")) { - /* A delivery sealed to this node's key, in the suite named. */ - if (*c->seal || strlen(value) >= sizeof(c->seal)) return false; - strcpy(c->seal, value); + /* A delivery sealed to this node's key: suite, session, number[, sender]. */ + if (*c->seal.suite || !read_seal(value, &c->seal)) return false; } else if (!strcasecmp(line, "X-Pure-Count")) { /* Numbered messages from X-Pure-Sequence on, as a JSON array. */ if (counted || !number(value, BATCH, &n) || !n) return false; @@ -656,13 +704,35 @@ static const char *convert(Connection *c) { return NULL; } +/* The session a seal was made in: one this thread opened before, or opened + * now in place of the least used; NULL when it does not open. */ +static PureOpener *session(Host *host, const Seal *seal) { + Session *least = &host->sessions[0]; + for (unsigned i = 0; i < SESSIONS; ++i) { + Session *k = &host->sessions[i]; + if (k->opener && !memcmp(k->seal.enc, seal->enc, PURE_SEAL_KEY) && !strcmp(k->seal.suite, seal->suite) && + k->seal.authenticated == seal->authenticated && + (!seal->authenticated || !memcmp(k->seal.from, seal->from, PURE_SEAL_KEY))) { + k->used = ++host->session_uses; + return k->opener; + } + if (k->used < least->used) least = k; + } + PureOpener *opener = pure_opener(seal_key, seal->suite, seal->enc, seal->authenticated ? seal->from : NULL); + if (!opener) return NULL; + pure_opener_free(least->opener); + *least = (Session){*seal, opener, ++host->session_uses}; + return opener; +} + /* A sealed body opened where it lies, or false (another key, another - * delivery's binding, or changed on the way). */ + * session or sender, another delivery's binding, or changed on the way). */ static bool open_seal(Connection *c) { if (c->body_size < PURE_SEAL_OVERHEAD) return false; + PureOpener *opener = session(c->host, &c->seal); size_t size = c->body_size - PURE_SEAL_OVERHEAD; - unsigned char *plain = malloc(size ? size : 1); - bool opened = plain && pure_unseal(seal_key, c->seal, c->aad, c->body, c->body_size, plain); + unsigned char *plain = opener ? malloc(size ? size : 1) : NULL; + bool opened = plain && pure_unseal(opener, c->seal.n, c->aad, c->body, c->body_size, plain); if (opened) { memcpy(c->body, plain, size); c->body_size = size; @@ -676,7 +746,7 @@ static bool open_seal(Connection *c) { /* A request's body is in: an app input's JSON becomes its payload, and the * request is ready, or it is answered 400 and never runs. */ static void body_done(Connection *c) { - if (*c->seal && !open_seal(c)) { + if (*c->seal.suite && !open_seal(c)) { release_ticket(c); /* never made ready: it never runs */ respond(c, 400, "Bad Request", "seal", 0, (PureSpan){0}); return; @@ -701,11 +771,13 @@ static void start_request(Connection *c, size_t head_size) { memmove(c->in, c->in + head_size, extra); c->in_used = extra; if (!parse_head(c, head, head_size, &query)) { refuse(c, 400, "Bad Request", "request"); return; } - /* A node that keeps its deliveries sealed refuses the others; a seal - * needs this node's key, and binds the delivery's source, number and count. */ - if (*c->source && sealed_only && !*c->seal) { refuse(c, 403, "Forbidden", "unsealed"); return; } - if (*c->seal) { + /* A node that keeps its deliveries sealed refuses the others, and one + * that names its senders refuses any other sender; a seal needs this + * node's key, and binds the delivery's source, number and count. */ + if (*c->source && (sealed_only || sender_count) && !*c->seal.suite) { refuse(c, 403, "Forbidden", "unsealed"); return; } + if (*c->seal.suite) { if (!*c->source || !seal_key || !c->json) { refuse(c, 400, "Bad Request", "seal"); return; } + if (sender_count && !known_sender(c->source, &c->seal)) { refuse(c, 403, "Forbidden", "sender"); return; } snprintf(c->aad, sizeof(c->aad), "%s %llu %u", c->source, (unsigned long long)c->sequence, c->batch); } if (c->body_size > PURE_OBJECT_LIMIT) { refuse(c, 413, "Content Too Large", "limit"); return; } @@ -1697,6 +1769,7 @@ static void stop_shard(Host *host) { if (host->listener >= 0) close(host->listener); if (host->epoll >= 0) close(host->epoll); if (host->wake >= 0) close(host->wake); + for (unsigned i = 0; i < SESSIONS; ++i) pure_opener_free(host->sessions[i].opener); while (host->closed) { Connection *c = host->closed; host->closed = c->next; @@ -1753,7 +1826,8 @@ static bool open_wires(Executor *ex, const PureModule *m, const char *const *sen } /* --seal OUTPUT=KEY: that output's sends sealed to the receiver's public key (hex). */ -static bool wire_seals(Executor *ex, const PureModule *m, const char *const *seals, unsigned count) { +static bool wire_seals(Executor *ex, const PureModule *m, const char *const *seals, unsigned count, + const unsigned char *own_public) { char error[512]; if (count && !pure_seal_ready(error, sizeof(error))) { fprintf(stderr, "--seal: %s\n", error); @@ -1775,7 +1849,25 @@ static bool wire_seals(Executor *ex, const PureModule *m, const char *const *sea fprintf(stderr, "--seal takes OUTPUT=KEY for an output given to --send, the key as 64 hex digits: %s\n", seals[i]); return false; } - pure_wire_seal(wire, peer); + pure_wire_seal(wire, peer, seal_key, own_public); /* sealed with this node's key when it has one */ + } + return true; +} + +/* --sender NAME=KEY: deliveries from source NAME are taken only sealed with + * that key (hex); with any, only named senders are taken. */ +static bool name_senders(const char *const *named, unsigned count) { + for (unsigned i = 0; i < count; ++i) { + const char *equals = strchr(named[i], '='); + size_t length = equals ? (size_t)(equals - named[i]) : 0; + struct Sender *sender = &senders[sender_count]; + if (!length || length > PURE_SOURCE || !pure_seal_peer(equals + 1, sender->key)) { + fprintf(stderr, "--sender takes NAME=KEY, the key as 64 hex digits: %s\n", named[i]); + return false; + } + memcpy(sender->name, named[i], length); + sender->name[length] = 0; + ++sender_count; } return true; } @@ -1818,8 +1910,8 @@ int main(int argc, char **argv) { static PureModule module; static Owner owner; const char *module_path = NULL, *store = NULL, *name = NULL, *sends[PURE_MODULE_INPUTS], *replies[PURE_MODULE_INPUTS]; - const char *seals[PURE_MODULE_INPUTS], *key_path = NULL; - unsigned send_count = 0, reply_count = 0, seal_count = 0; + const char *seals[PURE_MODULE_INPUTS], *named[PURE_MODULE_INPUTS], *key_path = NULL; + unsigned send_count = 0, reply_count = 0, seal_count = 0, named_count = 0; unsigned port = 8767, threads = 1; size_t budget = 64u << 20, capacity = 1024; uint64_t timeout_ms = 10000; @@ -1835,6 +1927,7 @@ int main(int argc, char **argv) { else if (!strcmp(argv[i], "--name")) name = argv[i + 1]; else if (!strcmp(argv[i], "--send-ca")) pure_wire_trust(argv[i + 1]); else if (!strcmp(argv[i], "--reply") && reply_count < PURE_MODULE_INPUTS) replies[reply_count++] = argv[i + 1]; + else if (!strcmp(argv[i], "--sender") && named_count < PURE_MODULE_INPUTS) named[named_count++] = argv[i + 1]; else if (!strcmp(argv[i], "--port") && number(argv[i + 1], 65535, &value)) port = (unsigned)value; else if (!strcmp(argv[i], "--budget") && number(argv[i + 1], SIZE_MAX, &value) && value) budget = (size_t)value; else if (!strcmp(argv[i], "--connections") && number(argv[i + 1], 1u << 20, &value) && value) capacity = (size_t)value; @@ -1852,7 +1945,10 @@ int main(int argc, char **argv) { char error[256]; if (!(seal_key = pure_seal_key(key_path, public_key, error, sizeof(error)))) { fprintf(stderr, "--key %s\n", error); return 2; } } - if (sealed_only && !seal_key) { fputs("--sealed-only needs --key\n", stderr); return 2; } + if ((sealed_only || named_count) && !seal_key) { fputs("--sealed-only and --sender need --key\n", stderr); return 2; } + if (!name_senders(named, named_count)) return 2; + unsigned char own_public[PURE_SEAL_KEY]; + if (seal_key && !pure_seal_peer(public_key, own_public)) return 2; if (pure_store_init(store) != PURE_OK) { fputs("Cannot initialize the store directory\n", stderr); return 1; } signal(SIGPIPE, SIG_IGN); struct sigaction stop_action = {.sa_handler = stop}; @@ -1884,7 +1980,7 @@ int main(int argc, char **argv) { owner.executor.publishes = true; if (!open_wires(&owner.executor, &module, sends, send_count, name) || !wire_replies(&owner.executor, &module, replies, reply_count) || - !wire_seals(&owner.executor, &module, seals, seal_count)) return 2; + !wire_seals(&owner.executor, &module, seals, seal_count, seal_key ? own_public : NULL)) return 2; after_save(&owner.executor); /* the snapshot, and the sends stored before */ for (unsigned i = 0; i < owner.executor.wire_count; ++i) if (!pure_wire_start(owner.executor.wires[i], wire_delivered, &owner)) { fputs("Cannot start a wire\n", stderr); return 1; } @@ -1955,6 +2051,7 @@ int main(int argc, char **argv) { usage: fputs("pure-serve --module FILE.pmod --store DIRECTORY [--port 8767] [--threads 1|N|auto] [--budget BYTES] " "[--connections 1024] [--timeout-ms 10000] [--send OUTPUT=http[s]://host:port/path ...] [--name NAME] " - "[--send-ca FILE] [--reply OUTPUT=INPUT ...] [--seal OUTPUT=KEY ...] [--key FILE] [--sealed-only]\n", stderr); + "[--send-ca FILE] [--reply OUTPUT=INPUT ...] [--seal OUTPUT=KEY ...] [--key FILE] [--sealed-only] " + "[--sender NAME=KEY ...]\n", stderr); return 2; } |
