From c635e41d8576f5587d47e51a98dfd4a8920315f0 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 27 Sep 2026 18:26:33 +0100 Subject: Seal delivered sends to the receiver's key (086) --seal OUTPUT=KEY seals each request's body whole to the receiving node's public key with HPKE (RFC 9180, base mode: X25519, HKDF-SHA256, AES-128-GCM where the CPU has AES, else ChaCha20-Poly1305), bound to the request's source, first number and count; X-Pure-Seal names the suite. A receiver's --key is a raw 32-byte file, made 0600 when missing, held in OpenSSL's secure heap; it opens the body where it lies (400 seal when it does not open) and zeroes the plain text when the request's ticket goes. --sealed-only refuses unsealed deliveries (403 unsealed). The check puts a recording relay between two apps: every order applies once, no body shows an item or a field name, a flipped bit is refused, a replayed request applies nothing twice, and a seal to another key does not open. Its hosts choose their own ports (--port 0). Sealed sends drain at 233-235k a second against 469-470k plain (Raspberry Pi 5): three X25519 operations a request, 164 us each. The plain request path measured the same as before. --- serve/seal.c | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 serve/seal.c (limited to 'serve/seal.c') diff --git a/serve/seal.c b/serve/seal.c new file mode 100644 index 0000000..c1cc55b --- /dev/null +++ b/serve/seal.c @@ -0,0 +1,142 @@ +#define _GNU_SOURCE +#include "seal.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__aarch64__) && defined(__linux__) +#include +#include +#endif + +struct PureSealKey { EVP_PKEY *key; }; + +static const unsigned char info[] = "pure sealed send 1"; +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 +} + +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; + 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) { + 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; +} + +void pure_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) { + OSSL_HPKE_SUITE suite; + if (!suite_named(pure_seal_suite(), &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; +} + +bool pure_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; +} -- cgit v1.2.3