aboutsummaryrefslogtreecommitdiffstats
path: root/serve
diff options
context:
space:
mode:
Diffstat (limited to 'serve')
-rw-r--r--serve/crypto-openssl.c89
-rw-r--r--serve/crypto.c26
-rw-r--r--serve/crypto.h16
-rw-r--r--serve/deliver.c94
-rw-r--r--serve/deliver.h9
-rw-r--r--serve/seal.h53
-rw-r--r--serve/serve.c145
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;
}