diff options
| author | eunhhu <[email protected]> | 2026-09-27 19:03:34 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 19:03:34 +0100 |
| commit | 8019260926dcebe4f4be99cb5f92f84fbc9cd6d3 (patch) | |
| tree | 06e407f4ba8606741df98bb1c6dcf3b7eed8fa12 | |
| parent | 944ab4fe95596127ec3c517037bad8fe6102dfe4 (diff) | |
| download | pure-8019260926dcebe4f4be99cb5f92f84fbc9cd6d3.tar.gz pure-8019260926dcebe4f4be99cb5f92f84fbc9cd6d3.zip | |
Map OpenSSL only in a server that uses it
Linking OpenSSL cost every pure-serve 2.3 MiB of resident memory, 0.66
MiB of it the process's own (relocated data), though most servers never
reach an https receiver or seal a send. The code that uses OpenSSL (TLS
to https receivers, sealing) is now built as pure-crypto.so beside
pure-serve and exports one table; pure-serve maps it the first time a
wire to an https URL opens or a key is read or a seal wired
(serve/crypto.c), and a missing library is reported at startup. What
needs no OpenSSL, a key's hex and the suite this CPU seals with, stays in
pure-serve.
An idle server: RSS 4,496 -> 2,320 kB, PSS 1,639 -> 881 kB, private
dirty 1,296 -> 864 kB. A static OpenSSL was measured too and is worse
(RSS 3.8 MiB, but 2.3 MiB private: all of its tables relocated). The
checks, https webhooks and sealed sends included, pass unchanged.
| -rw-r--r-- | Makefile | 14 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | docs/design/009-layout.md | 8 | ||||
| -rw-r--r-- | research/experiments/013-parallel-syncs/variants.py | 5 | ||||
| -rw-r--r-- | serve/crypto-openssl.c (renamed from serve/seal.c) | 107 | ||||
| -rw-r--r-- | serve/crypto.c | 121 | ||||
| -rw-r--r-- | serve/crypto.h | 31 | ||||
| -rw-r--r-- | serve/deliver.c | 72 | ||||
| -rw-r--r-- | serve/seal.h | 5 | ||||
| -rw-r--r-- | serve/serve.c | 5 | ||||
| -rw-r--r-- | serve/tls.h | 18 |
11 files changed, 276 insertions, 112 deletions
@@ -10,7 +10,9 @@ BUILD ?= build # states as drafts; state.h and store.h are what it asks of storage. # store/ the tree store (one file; commits are one contiguous write and one # sync; formats PURETRE3 and PUREREC1). -# serve/ pure-serve: the HTTP host, outbound delivery, JSON in place. +# serve/ pure-serve: the HTTP host, outbound delivery, JSON in place; with +# pure-crypto.so beside it, OpenSSL's part (TLS to https receivers, +# sealed sends), mapped only by a server that uses one. # cli/ command-line hosts. # libpure.a is the app model with the tree store: what a host embeds (Vega's # surfaces link it). prototypes/ keeps the earlier document runtime (000-077). @@ -19,12 +21,12 @@ BUILD ?= build INCLUDES = -Icore MODEL = core/module.c core/module-list.c core/module-save.c core/module-text.c core/module-store.c core/flow.c STORE = store/store-tree.c -SERVE = serve/serve.c serve/deliver.c serve/message-json.c serve/seal.c +SERVE = serve/serve.c serve/deliver.c serve/message-json.c serve/crypto.c HEADERS = $(wildcard core/*.h) $(wildcard serve/*.h) OBJECTS = $(patsubst %.c,$(BUILD)/obj/%.o,$(MODEL) $(STORE)) .PHONY: all check -all: $(BUILD)/libpure.a $(BUILD)/pure-serve $(BUILD)/pure-run $(BUILD)/pure-edit-module +all: $(BUILD)/libpure.a $(BUILD)/pure-serve $(BUILD)/pure-crypto.so $(BUILD)/pure-run $(BUILD)/pure-edit-module $(BUILD)/obj/%.o: %.c $(HEADERS) @mkdir -p $(dir $@) @@ -38,7 +40,11 @@ $(BUILD)/libpure.a: $(OBJECTS) # It links after the library, last as it always was, so code keeps its place. $(BUILD)/pure-serve: $(SERVE) core/runtime.c $(BUILD)/libpure.a $(HEADERS) $(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) -Iserve -DPURE_ASYNC_EFFECTS $(SERVE) $(BUILD)/libpure.a core/runtime.c \ - $(LDFLAGS) -lssl -lcrypto -lm -pthread $(LDLIBS) -o $@ + $(LDFLAGS) -lm -pthread $(LDLIBS) -o $@ + +$(BUILD)/pure-crypto.so: serve/crypto-openssl.c $(HEADERS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) -Iserve -fPIC -shared serve/crypto-openssl.c $(LDFLAGS) -lssl -lcrypto \ + $(LDLIBS) -o $@ $(BUILD)/pure-run: cli/pure-run.c $(BUILD)/libpure.a $(HEADERS) $(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) cli/pure-run.c $(BUILD)/libpure.a $(LDFLAGS) -lm $(LDLIBS) -o $@ @@ -22,7 +22,7 @@ The source follows the principle below, a small core with the rest selected arou |---|---|---| | `core/` | execution, ownership, limits and completion (`runtime.c`); the app model: modules verified before they run, events as transactions, states as drafts (`module*.c`); what the core asks of storage (`state.h`, `store.h`) | `build/libpure.a` (with the store) | | `store/` | the tree store: one file, a commit is one contiguous write and one sync (formats `PURETRE3`, `PUREREC1`) | in `libpure.a` | -| `serve/` | `pure-serve`: the HTTP host, outbound delivery, JSON read in place | `build/pure-serve` | +| `serve/` | `pure-serve`: the HTTP host, outbound delivery, JSON read in place; OpenSSL's part (TLS to https receivers, sealed sends) apart, mapped only by a server that uses one | `build/pure-serve`, `build/pure-crypto.so` | | `cli/` | command-line hosts | `build/pure-run`, `build/pure-edit-module` | | `prototypes/` | the earlier document runtime (000-077), built against `core/` and `store/` | `make -C prototypes/connections` | | `tests/` | the hosts checked end to end, with apps built by aup (the sibling checkout) | `make check` | diff --git a/docs/design/009-layout.md b/docs/design/009-layout.md index bcaf64e..d985003 100644 --- a/docs/design/009-layout.md +++ b/docs/design/009-layout.md @@ -29,10 +29,16 @@ |---|---|---| | `core/` | 실행·소유권·한도·완료(`runtime.c`, `flow.c`), 앱 모델(`module*.c`: 실행 전에 검증한 모듈, 트랜잭션인 이벤트, draft인 상태), 코어가 저장소에 요구하는 것(`state.h`, `store.h`) | `build/libpure.a`(저장소와 함께) | | `store/` | 트리 저장소. 한 파일, 커밋은 한 연속 쓰기와 동기화 한 번 | `libpure.a` 안 | -| `serve/` | `pure-serve`: HTTP 호스트, 나가는 전달, 제자리 JSON | `build/pure-serve` | +| `serve/` | `pure-serve`: HTTP 호스트, 나가는 전달, 제자리 JSON. OpenSSL을 쓰는 부분(https 수신자로의 TLS, 봉인한 전달)은 따로 두어 쓰는 서버만 매핑한다 | `build/pure-serve`, `build/pure-crypto.so` | | `cli/` | 명령행 호스트 | `build/pure-run`, `build/pure-edit-module` | | `prototypes/` | 이전 문서 런타임. `core/`와 `store/`를 가져다 빌드한다 | `make -C prototypes/connections` | +OpenSSL은 pure-serve가 쓰는 것 중 가장 크다. 링크만 해도 아무 일도 하지 않는 프로세스의 상주 메모리가 2.3 MiB 늘고, +그중 0.66 MiB는 그 프로세스만의 것(재배치한 데이터)이다. 그래서 OpenSSL을 쓰는 코드(`serve/crypto-openssl.c`)는 +`pure-crypto.so`로 따로 빌드하고, pure-serve는 https 수신자나 봉인을 처음 쓸 때 실행 파일 옆의 그것을 연다 +(`serve/crypto.c`). 쓰지 않는 서버는 유휴 RSS가 4.5 → 2.3 MiB, 자기만의 메모리가 1.3 → 0.86 MiB다. 정적 링크는 RSS가 조금 +줄지만(3.8 MiB) 자기만의 메모리가 2.3 MiB로 는다. OpenSSL의 표를 모두 재배치하기 때문이다. + 검사도 부품을 따른다. 부품의 단위 검사는 그 옆에(`core/test-module.c`, `store/test-tree-store.c`), 저장소 전체를 끝에서 끝까지 보는 검사는 `tests/check.py`에 둔다. aup와 Vega도 같다(aup `compiler/`, Vega `lib/`·`surfaces/`·`symbols/`, 둘 다 `tests/check.py`). pure-serve의 HTTP 검사는 전에 aup의 검사 안에 있었다. 호스트를 바꾸면 aup에서 확인해야 했던 것이다. diff --git a/research/experiments/013-parallel-syncs/variants.py b/research/experiments/013-parallel-syncs/variants.py index ee49d2a..0b07243 100644 --- a/research/experiments/013-parallel-syncs/variants.py +++ b/research/experiments/013-parallel-syncs/variants.py @@ -41,10 +41,11 @@ def build(work, name, patches): store.write_text(source) binary = work / f'pure-serve-{len(name)}' subprocess.run(['cc', '-O2', '-std=c17', f'-I{ROOT / "core"}', f'-I{ROOT / "serve"}', '-DPURE_ASYNC_EFFECTS', '-pthread', - *[str(ROOT / s) for s in ('serve/serve.c', 'serve/deliver.c', 'serve/message-json.c', 'core/module.c', + *[str(ROOT / s) for s in ('serve/serve.c', 'serve/deliver.c', 'serve/message-json.c', 'serve/crypto.c', + 'core/module.c', 'core/module-list.c', 'core/module-save.c', 'core/module-text.c', 'core/module-store.c')], str(store), str(ROOT / 'core/runtime.c'), - '-lssl', '-lcrypto', '-lm', '-o', str(binary)], check=True) + '-lm', '-o', str(binary)], check=True) return binary diff --git a/serve/seal.c b/serve/crypto-openssl.c index c1cc55b..38b4628 100644 --- a/serve/seal.c +++ b/serve/crypto-openssl.c @@ -1,72 +1,80 @@ #define _GNU_SOURCE -#include "seal.h" +/* pure-crypto.so: what pure-serve asks of OpenSSL (serve/crypto.h), mapped + * only when a server seals, opens a seal or reaches an https receiver. It + * exports one table, pure_crypto. */ +#include "crypto.h" +#include <arpa/inet.h> #include <errno.h> #include <fcntl.h> #include <openssl/crypto.h> #include <openssl/evp.h> #include <openssl/hpke.h> +#include <openssl/ssl.h> +#include <openssl/x509v3.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <unistd.h> -#if defined(__aarch64__) && defined(__linux__) -#include <asm/hwcap.h> -#include <sys/auxv.h> -#endif -struct PureSealKey { EVP_PKEY *key; }; +/* ---- TLS: one client context, verifying the receiver's certificate against + * the system's CAs or the file given, and its name. */ +static SSL_CTX *tls_context; -static const unsigned char info[] = "pure sealed send 1"; -static const char aes_name[] = "x25519-sha256-aes128gcm", chacha_name[] = "x25519-sha256-chacha20poly1305"; +static bool tls_start(const char *ca_file) { + SSL_CTX *context = SSL_CTX_new(TLS_client_method()); + if (!context) return false; + if (!SSL_CTX_set_min_proto_version(context, TLS1_2_VERSION) || + !(ca_file ? SSL_CTX_load_verify_locations(context, ca_file, NULL) : SSL_CTX_set_default_verify_paths(context))) { + SSL_CTX_free(context); + return false; + } + SSL_CTX_set_verify(context, SSL_VERIFY_PEER, NULL); + SSL_CTX_set_mode(context, SSL_MODE_AUTO_RETRY); + tls_context = context; + return true; +} -/* AES-GCM where AES is in the CPU (two to three times ChaCha20-Poly1305 from - * a kilobyte up on a Cortex-A76, research 086); ChaCha20-Poly1305, fast in - * plain code, elsewhere. */ -static bool has_aes(void) { -#if defined(__aarch64__) && defined(__linux__) - return (getauxval(AT_HWCAP) & HWCAP_AES) && (getauxval(AT_HWCAP) & HWCAP_PMULL); -#elif defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__)) - return __builtin_cpu_supports("aes") && __builtin_cpu_supports("pclmul"); -#else - return false; -#endif +/* TLS over a connected socket: the receiver's certificate must name the host + * (a DNS name, or an IP address written as one). */ +static PureTls *tls_open(const char *host, int fd) { + SSL *ssl = SSL_new(tls_context); + if (!ssl) return NULL; + unsigned char address[16]; + bool literal = inet_pton(AF_INET, host, address) == 1 || inet_pton(AF_INET6, host, address) == 1; + bool named = literal ? X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl), host) == 1 : + SSL_set_tlsext_host_name(ssl, host) == 1 && SSL_set1_host(ssl, host) == 1; + if (!named || SSL_set_fd(ssl, fd) != 1 || SSL_connect(ssl) != 1 || SSL_get_verify_result(ssl) != X509_V_OK) { + SSL_free(ssl); + return NULL; + } + return (PureTls *)ssl; } +static int tls_read(PureTls *tls, void *bytes, int size) { return SSL_read((SSL *)tls, bytes, size); } +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. */ +struct PureSealKey { EVP_PKEY *key; }; + +static const unsigned char info[] = "pure sealed send 1"; + 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}; - if (!strcmp(name, aes_name)) suite.aead_id = OSSL_HPKE_AEAD_ID_AES_GCM_128; - else if (!strcmp(name, chacha_name)) suite.aead_id = OSSL_HPKE_AEAD_ID_CHACHA_POLY1305; + if (!strcmp(name, "x25519-sha256-aes128gcm")) suite.aead_id = OSSL_HPKE_AEAD_ID_AES_GCM_128; + else if (!strcmp(name, "x25519-sha256-chacha20poly1305")) suite.aead_id = OSSL_HPKE_AEAD_ID_CHACHA_POLY1305; else return false; *out = suite; return true; } -const char *pure_seal_suite(void) { return has_aes() ? aes_name : chacha_name; } - static pthread_once_t secure_once = PTHREAD_ONCE_INIT; static void secure_heap(void) { CRYPTO_secure_malloc_init(16384, 32); } -static bool hex_value(char c, unsigned *v) { - if (c >= '0' && c <= '9') *v = (unsigned)(c - '0'); - else if (c >= 'a' && c <= 'f') *v = (unsigned)(c - 'a' + 10); - else if (c >= 'A' && c <= 'F') *v = (unsigned)(c - 'A' + 10); - else return false; - return true; -} - -bool pure_seal_peer(const char *hex, unsigned char out[PURE_SEAL_KEY]) { - if (strlen(hex) != 2 * PURE_SEAL_KEY) return false; - for (size_t i = 0; i < PURE_SEAL_KEY; ++i) { - unsigned high, low; - if (!hex_value(hex[2 * i], &high) || !hex_value(hex[2 * i + 1], &low)) return false; - out[i] = (unsigned char)(high << 4 | low); - } - return true; -} - -PureSealKey *pure_seal_key(const char *path, char public_hex[2 * PURE_SEAL_KEY + 1], char *error, size_t size) { +static PureSealKey *seal_key(const char *path, char public_hex[2 * PURE_SEAL_KEY + 1], char *error, size_t size) { pthread_once(&secure_once, secure_heap); unsigned char *raw = OPENSSL_secure_malloc(PURE_SEAL_KEY), public_key[PURE_SEAL_KEY]; PureSealKey *made = calloc(1, sizeof(*made)); @@ -106,16 +114,16 @@ fail: return NULL; } -void pure_seal_key_free(PureSealKey *key) { +static void seal_key_free(PureSealKey *key) { if (!key) return; EVP_PKEY_free(key->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) { +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) { OSSL_HPKE_SUITE suite; - if (!suite_named(pure_seal_suite(), &suite)) return false; + 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 && @@ -126,8 +134,8 @@ bool pure_seal(const unsigned char peer[PURE_SEAL_KEY], const char *aad, const u return ok; } -bool pure_unseal(const PureSealKey *key, const char *suite_name, const char *aad, const unsigned char *sealed, size_t size, - unsigned char *plain) { +static bool unseal(const PureSealKey *key, const char *suite_name, const char *aad, const unsigned char *sealed, + size_t size, unsigned char *plain) { 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); @@ -140,3 +148,6 @@ bool pure_unseal(const PureSealKey *key, const char *suite_name, const char *aad OSSL_HPKE_CTX_free(context); return ok; } + +const PureCrypto pure_crypto = {PURE_CRYPTO_VERSION, tls_start, tls_open, tls_read, tls_write, tls_close, + seal_key, seal_key_free, seal, unseal}; diff --git a/serve/crypto.c b/serve/crypto.c new file mode 100644 index 0000000..8806ad2 --- /dev/null +++ b/serve/crypto.c @@ -0,0 +1,121 @@ +#define _GNU_SOURCE +/* Sealing and TLS for pure-serve (serve/seal.h, serve/tls.h), by way of + * pure-crypto.so (serve/crypto.h): mapped, and OpenSSL with it, the first + * time a server needs it. What needs no OpenSSL (a key's hex, the suite + * this machine seals with) is here. */ +#include "crypto.h" +#include <dlfcn.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(__aarch64__) && defined(__linux__) +#include <asm/hwcap.h> +#include <sys/auxv.h> +#endif + +static pthread_once_t once = PTHREAD_ONCE_INIT; +static const PureCrypto *crypto; +static char why[512]; + +/* pure-crypto.so beside the executable, or where PURE_CRYPTO names it. */ +static void load(void) { + char path[4096]; + const char *named = getenv("PURE_CRYPTO"); + if (named) snprintf(path, sizeof(path), "%s", named); + else { + ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 32); + char *slash = length > 0 ? memrchr(path, '/', (size_t)length) : NULL; + if (!slash) { snprintf(why, sizeof(why), "pure-crypto.so: cannot find pure-serve's directory"); return; } + strcpy(slash + 1, "pure-crypto.so"); + } + void *library = dlopen(path, RTLD_NOW | RTLD_LOCAL); + const PureCrypto *table = library ? dlsym(library, "pure_crypto") : NULL; + if (!table || table->version != PURE_CRYPTO_VERSION) { + snprintf(why, sizeof(why), "%s", !library ? dlerror() : "pure-crypto.so is not this pure-serve's"); + return; + } + crypto = table; +} + +static const PureCrypto *loaded(char *error, size_t size) { + pthread_once(&once, load); + if (!crypto && error) snprintf(error, size, "%s", why); + return crypto; +} + +/* ---- TLS */ + +bool pure_tls_start(const char *ca_file, char *error, size_t size) { + static int started; /* 1 made, -1 failed; by the thread that opens wires */ + const PureCrypto *c = loaded(error, size); + if (!c) return false; + if (!started) started = c->tls_start(ca_file) ? 1 : -1; + if (started < 0) snprintf(error, size, "no TLS context: %s cannot be read", ca_file ? ca_file : "the system's CAs"); + return started > 0; +} + +/* A session exists only once the table is loaded. */ +PureTls *pure_tls_open(const char *host, int fd) { return crypto ? crypto->tls_open(host, fd) : NULL; } +int pure_tls_read(PureTls *tls, void *bytes, int size) { return crypto->tls_read(tls, bytes, size); } +int pure_tls_write(PureTls *tls, const void *bytes, int size) { return crypto->tls_write(tls, bytes, size); } +void pure_tls_close(PureTls *tls) { if (tls) crypto->tls_close(tls); } + +/* ---- Sealing */ + +static const char aes_name[] = "x25519-sha256-aes128gcm", chacha_name[] = "x25519-sha256-chacha20poly1305"; + +/* AES-GCM where AES is in the CPU (two to three times ChaCha20-Poly1305 from + * a kilobyte up on a Cortex-A76, research 086); ChaCha20-Poly1305, fast in + * plain code, elsewhere. */ +static bool has_aes(void) { +#if defined(__aarch64__) && defined(__linux__) + return (getauxval(AT_HWCAP) & HWCAP_AES) && (getauxval(AT_HWCAP) & HWCAP_PMULL); +#elif defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__)) + return __builtin_cpu_supports("aes") && __builtin_cpu_supports("pclmul"); +#else + return false; +#endif +} + +const char *pure_seal_suite(void) { return has_aes() ? aes_name : chacha_name; } + +static bool hex_value(char c, unsigned *v) { + if (c >= '0' && c <= '9') *v = (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') *v = (unsigned)(c - 'a' + 10); + else if (c >= 'A' && c <= 'F') *v = (unsigned)(c - 'A' + 10); + else return false; + return true; +} + +bool pure_seal_peer(const char *hex, unsigned char out[PURE_SEAL_KEY]) { + if (strlen(hex) != 2 * PURE_SEAL_KEY) return false; + for (size_t i = 0; i < PURE_SEAL_KEY; ++i) { + unsigned high, low; + if (!hex_value(hex[2 * i], &high) || !hex_value(hex[2 * i + 1], &low)) return false; + out[i] = (unsigned char)(high << 4 | low); + } + return true; +} + +bool pure_seal_ready(char *error, size_t size) { return loaded(error, size) != NULL; } + +PureSealKey *pure_seal_key(const char *path, char public_hex[2 * PURE_SEAL_KEY + 1], char *error, size_t size) { + const PureCrypto *c = loaded(error, size); + return c ? c->seal_key(path, public_hex, error, size) : NULL; +} + +/* A key 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) { + const PureCrypto *c = loaded(NULL, 0); + return c && c->seal(pure_seal_suite(), peer, aad, plain, size, out); +} + +bool pure_unseal(const PureSealKey *key, const char *suite, const char *aad, const unsigned char *sealed, size_t size, + unsigned char *plain) { + return key && crypto->unseal(key, suite, aad, sealed, size, plain); +} diff --git a/serve/crypto.h b/serve/crypto.h new file mode 100644 index 0000000..5083b23 --- /dev/null +++ b/serve/crypto.h @@ -0,0 +1,31 @@ +#ifndef PURE_CRYPTO_H +#define PURE_CRYPTO_H +#include "seal.h" +#include "tls.h" + +/* What pure-serve asks of OpenSSL, as the table pure-crypto.so exports + * (serve/crypto-openssl.c). pure-serve maps it, and OpenSSL with it, the + * 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 }; + +typedef struct { + unsigned version; + /* TLS client: the context (verifying against ca_file, or the system's + * CAs when NULL), sessions over connected sockets, their reads and + * writes (as SSL_read and SSL_write: bytes, or <= 0), and their end. */ + bool (*tls_start)(const char *ca_file); + PureTls *(*tls_open)(const char *host, int fd); + 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. */ + 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, + unsigned char *plain); +} PureCrypto; +#endif diff --git a/serve/deliver.c b/serve/deliver.c index 391e0db..99f2f53 100644 --- a/serve/deliver.c +++ b/serve/deliver.c @@ -7,9 +7,7 @@ #include <netdb.h> #include <netinet/in.h> #include <netinet/tcp.h> -#include <openssl/ssl.h> -#include <openssl/x509v3.h> -#include <arpa/inet.h> +#include "tls.h" #include <pthread.h> #include <stdio.h> #include <stdlib.h> @@ -44,8 +42,8 @@ struct PureWire { void (*notify)(void *); void *context; int fd; /* the connection kept for the next send, or -1; changed under the lock */ - bool tls; /* https: the connection is TLS */ - SSL *ssl; /* its TLS session */ + bool tls; /* https: the connection is TLS */ + PureTls *session; /* its TLS session */ int answer_input; /* the input answers become events of, or -1 */ Answer *answers; /* made, for the owner to take in order */ size_t answer_head, answer_count, answer_capacity; @@ -55,27 +53,12 @@ struct PureWire { unsigned char peer[PURE_SEAL_KEY]; }; -/* ---- TLS: one client context, verifying the receiver's certificate against - * the system's CAs or the file pure_wire_trust named, and its name. */ -static pthread_once_t tls_once = PTHREAD_ONCE_INIT; -static SSL_CTX *tls_context; +/* TLS verifies a receiver's certificate against the system's CAs or the + * file pure_wire_trust named (serve/tls.h). */ static const char *tls_ca; void pure_wire_trust(const char *ca_file) { tls_ca = ca_file; } -static void make_tls_context(void) { - SSL_CTX *context = SSL_CTX_new(TLS_client_method()); - if (!context) return; - if (!SSL_CTX_set_min_proto_version(context, TLS1_2_VERSION) || - !(tls_ca ? SSL_CTX_load_verify_locations(context, tls_ca, NULL) : SSL_CTX_set_default_verify_paths(context))) { - SSL_CTX_free(context); - return; - } - SSL_CTX_set_verify(context, SSL_VERIFY_PEER, NULL); - SSL_CTX_set_mode(context, SSL_MODE_AUTO_RETRY); - tls_context = context; -} - /* ---- A send as JSON: its fields in order, a group's fields as an object. */ typedef struct { char *bytes; size_t size, capacity; bool failed; } Text; @@ -213,13 +196,7 @@ PureWire *pure_wire_open(const PureModule *m, const char *output, const char *ur snprintf(error, size, "a URL is http[s]://host[:port]/path"); return NULL; } - if (tls) { - pthread_once(&tls_once, make_tls_context); - if (!tls_context) { - snprintf(error, size, "no TLS context: %s cannot be read", tls_ca ? tls_ca : "the system's CAs"); - return NULL; - } - } + if (tls && !pure_tls_start(tls_ca, error, size)) return NULL; const char *host = url + (tls ? 8 : 7), *slash = strchr(host, '/'), *colon = memchr(host, ':', slash ? (size_t)(slash - host) : strlen(host)); const char *host_end = colon ? colon : slash ? slash : host + strlen(host); const char *port = colon ? colon + 1 : tls ? "443" : "80"; @@ -341,7 +318,7 @@ static bool fill(Reader *r) { if (r->end == sizeof(r->bytes)) return false; PureWire *w = r->wire; ssize_t n; - if (w->ssl) n = SSL_read(w->ssl, r->bytes + r->end, (int)(sizeof(r->bytes) - r->end)); + if (w->session) n = pure_tls_read(w->session, r->bytes + r->end, (int)(sizeof(r->bytes) - r->end)); else do n = recv(w->fd, r->bytes + r->end, sizeof(r->bytes) - r->end, 0); while (n < 0 && errno == EINTR); if (n <= 0) return false; #ifdef TCP_QUICKACK @@ -392,9 +369,9 @@ static bool take(Reader *r, uint64_t size, Text *into) { static bool send_all(PureWire *w, const char *bytes, size_t size) { while (size) { - ssize_t n = w->ssl ? SSL_write(w->ssl, bytes, (int)(size < INT32_MAX ? size : INT32_MAX)) : - send(w->fd, bytes, size, MSG_NOSIGNAL); - if (n < 0 && !w->ssl && errno == EINTR) continue; + ssize_t n = w->session ? pure_tls_write(w->session, bytes, (int)(size < INT32_MAX ? size : INT32_MAX)) : + send(w->fd, bytes, size, MSG_NOSIGNAL); + if (n < 0 && !w->session && errno == EINTR) continue; if (n <= 0) return false; bytes += n; size -= (size_t)n; @@ -402,22 +379,6 @@ static bool send_all(PureWire *w, const char *bytes, size_t size) { return true; } -/* TLS over a connected socket: the receiver's certificate must name the host - * (a DNS name, or an IP address written as one). */ -static SSL *start_tls(const PureWire *w, int fd) { - SSL *ssl = SSL_new(tls_context); - if (!ssl) return NULL; - unsigned char address[16]; - bool literal = inet_pton(AF_INET, w->host, address) == 1 || inet_pton(AF_INET6, w->host, address) == 1; - bool named = literal ? X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl), w->host) == 1 : - SSL_set_tlsext_host_name(ssl, w->host) == 1 && SSL_set1_host(ssl, w->host) == 1; - if (!named || SSL_set_fd(ssl, fd) != 1 || SSL_connect(ssl) != 1 || SSL_get_verify_result(ssl) != X509_V_OK) { - SSL_free(ssl); - return NULL; - } - return ssl; -} - static int connect_to(const PureWire *w) { struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM}, *list; if (getaddrinfo(w->host, w->port, &hints, &list)) return -1; @@ -441,9 +402,9 @@ static int connect_to(const PureWire *w) { static void drop_connection(PureWire *w) { pthread_mutex_lock(&w->lock); - if (w->ssl) SSL_free(w->ssl); + pure_tls_close(w->session); if (w->fd >= 0) close(w->fd); - w->ssl = NULL; + w->session = NULL; w->fd = -1; pthread_mutex_unlock(&w->lock); } @@ -623,13 +584,14 @@ static int send_request(PureWire *w, const char *request, size_t size, const Pen bool stopping = w->stopping; pthread_mutex_unlock(&w->lock); if (stopping) return AGAIN; - SSL *ssl = w->tls ? start_tls(w, fd) : NULL; - if (w->tls && !ssl) { + /* The receiver's certificate must name the host. */ + PureTls *session = w->tls ? pure_tls_open(w->host, fd) : NULL; + if (w->tls && !session) { drop_connection(w); return TLS_FAILED; } pthread_mutex_lock(&w->lock); - w->ssl = ssl; + w->session = session; pthread_mutex_unlock(&w->lock); } int status; @@ -743,7 +705,7 @@ void pure_wire_close(PureWire *w) { pthread_cond_broadcast(&w->changed); pthread_mutex_unlock(&w->lock); if (w->started) pthread_join(w->thread, NULL); - if (w->ssl) SSL_free(w->ssl); + pure_tls_close(w->session); 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/seal.h b/serve/seal.h index 4ce3844..663d860 100644 --- a/serve/seal.h +++ b/serve/seal.h @@ -9,7 +9,8 @@ * (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 * OpenSSL's secure heap: locked in memory, out of core dumps, cleared when - * freed. */ + * 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 */ typedef struct PureSealKey PureSealKey; @@ -17,6 +18,8 @@ typedef struct PureSealKey PureSealKey; * 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. */ diff --git a/serve/serve.c b/serve/serve.c index 4f633ed..5088d8a 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -1754,6 +1754,11 @@ 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) { + char error[512]; + if (count && !pure_seal_ready(error, sizeof(error))) { + fprintf(stderr, "--seal: %s\n", error); + return false; + } for (unsigned i = 0; i < count; ++i) { const char *equals = strchr(seals[i], '='); unsigned char peer[PURE_SEAL_KEY]; diff --git a/serve/tls.h b/serve/tls.h new file mode 100644 index 0000000..704924d --- /dev/null +++ b/serve/tls.h @@ -0,0 +1,18 @@ +#ifndef PURE_TLS_H +#define PURE_TLS_H +#include <stdbool.h> +#include <stddef.h> + +/* TLS to https receivers (serve/crypto.c): one client context, verifying a + * receiver's certificate against the system's CAs or ca_file, and its name. */ +typedef struct PureTls PureTls; +/* The context, made once; false with why in error when it cannot be. */ +bool pure_tls_start(const char *ca_file, char *error, size_t size); +/* A session over a connected socket to host (a DNS name, or an IP address + * written as one), its certificate verified; NULL when it fails. */ +PureTls *pure_tls_open(const char *host, int fd); +/* As SSL_read and SSL_write: the bytes moved, or <= 0. */ +int pure_tls_read(PureTls *, void *bytes, int size); +int pure_tls_write(PureTls *, const void *bytes, int size); +void pure_tls_close(PureTls *); +#endif |
