diff options
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; } |
