aboutsummaryrefslogtreecommitdiffstats
path: root/serve/crypto-openssl.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 19:03:34 +0100
committereunhhu <[email protected]>2026-09-27 19:03:34 +0100
commit8019260926dcebe4f4be99cb5f92f84fbc9cd6d3 (patch)
tree06e407f4ba8606741df98bb1c6dcf3b7eed8fa12 /serve/crypto-openssl.c
parent944ab4fe95596127ec3c517037bad8fe6102dfe4 (diff)
downloadpure-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.
Diffstat (limited to 'serve/crypto-openssl.c')
-rw-r--r--serve/crypto-openssl.c153
1 files changed, 153 insertions, 0 deletions
diff --git a/serve/crypto-openssl.c b/serve/crypto-openssl.c
new file mode 100644
index 0000000..38b4628
--- /dev/null
+++ b/serve/crypto-openssl.c
@@ -0,0 +1,153 @@
+#define _GNU_SOURCE
+/* 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>
+
+/* ---- 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 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;
+}
+
+/* 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, "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;
+}
+
+static pthread_once_t secure_once = PTHREAD_ONCE_INIT;
+static void secure_heap(void) { CRYPTO_secure_malloc_init(16384, 32); }
+
+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));
+ EVP_PKEY *key = NULL;
+ size_t length = PURE_SEAL_KEY;
+ if (!raw || !made) { snprintf(error, size, "out of memory"); goto fail; }
+ int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
+ struct stat info_;
+ if (fd >= 0) {
+ bool ok = !fstat(fd, &info_) && S_ISREG(info_.st_mode) && !(info_.st_mode & 077) &&
+ read(fd, raw, PURE_SEAL_KEY) == PURE_SEAL_KEY && info_.st_size == PURE_SEAL_KEY;
+ close(fd);
+ if (!ok) { snprintf(error, size, "%s: a key is 32 bytes that only its owner may read (0600)", path); goto fail; }
+ key = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, raw, PURE_SEAL_KEY);
+ } else if (errno == ENOENT) {
+ /* A new key: made, written whole and synced before it is used. */
+ key = EVP_PKEY_Q_keygen(NULL, NULL, "X25519");
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
+ bool ok = key && fd >= 0 && EVP_PKEY_get_raw_private_key(key, raw, &length) == 1 && length == PURE_SEAL_KEY &&
+ write(fd, raw, PURE_SEAL_KEY) == PURE_SEAL_KEY && !fsync(fd);
+ if (fd >= 0 && close(fd)) ok = false;
+ if (!ok) { snprintf(error, size, "%s: cannot make a key there", path); unlink(path); goto fail; }
+ } else { snprintf(error, size, "%s: %s", path, strerror(errno)); goto fail; }
+ length = PURE_SEAL_KEY;
+ if (!key || EVP_PKEY_get_raw_public_key(key, public_key, &length) != 1 || length != PURE_SEAL_KEY) {
+ snprintf(error, size, "%s: not an X25519 key", path);
+ goto fail;
+ }
+ for (size_t i = 0; i < PURE_SEAL_KEY; ++i) snprintf(public_hex + 2 * i, 3, "%02x", public_key[i]);
+ OPENSSL_secure_clear_free(raw, PURE_SEAL_KEY);
+ made->key = key;
+ return made;
+fail:
+ EVP_PKEY_free(key);
+ if (raw) OPENSSL_secure_clear_free(raw, PURE_SEAL_KEY);
+ free(made);
+ return NULL;
+}
+
+static void seal_key_free(PureSealKey *key) {
+ if (!key) return;
+ EVP_PKEY_free(key->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) {
+ 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;
+}
+
+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);
+ 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;
+}
+
+const PureCrypto pure_crypto = {PURE_CRYPTO_VERSION, tls_start, tls_open, tls_read, tls_write, tls_close,
+ seal_key, seal_key_free, seal, unseal};