summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/config/global.cmake12
-rw-r--r--cmake/config/irmd.cmake2
-rw-r--r--cmake/config/lib.cmake38
-rw-r--r--cmake/dependencies.cmake1
-rw-r--r--cmake/dependencies/system/liburcu.cmake45
-rw-r--r--include/ouroboros/crypt.h47
-rw-r--r--include/ouroboros/rcu.h110
-rw-r--r--src/irmd/main.c15
-rw-r--r--src/lib/CMakeLists.txt8
-rw-r--r--src/lib/config.h.in10
-rw-r--r--src/lib/crypt.c338
-rw-r--r--src/lib/crypt/keyrot.c741
-rw-r--r--src/lib/crypt/keyrot.h74
-rw-r--r--src/lib/crypt/openssl.c504
-rw-r--r--src/lib/crypt/openssl.h42
-rw-r--r--src/lib/dev.c26
-rw-r--r--src/lib/tests/CMakeLists.txt4
-rw-r--r--src/lib/tests/crypt_test.c430
-rw-r--r--src/lib/tests/keyrot_test.c1083
19 files changed, 2996 insertions, 534 deletions
diff --git a/cmake/config/global.cmake b/cmake/config/global.cmake
index 0ac256bb..1e172724 100644
--- a/cmake/config/global.cmake
+++ b/cmake/config/global.cmake
@@ -25,8 +25,16 @@ set(SHM_LOCKFILE_NAME "/${SHM_PREFIX}.lockfile" CACHE INTERNAL
# Secure memory configuration
set(IRMD_SECMEM_MAX 1048576 CACHE STRING "IRMd secure heap size")
-set(PROC_SECMEM_MAX 1048576 CACHE STRING "Process secure heap size")
-set(SECMEM_GUARD 32 CACHE STRING "Secure heap min size")
+# ~8 KiB secure heap per encrypted flow (cur+prev node slabs); the total
+# is rounded up to a power of two for the OpenSSL secure-heap allocator.
+set(PROC_SECMEM_FLOWS 512 CACHE STRING
+ "Max concurrent encrypted flows the per-process secure heap is sized for")
+math(EXPR PROC_SECMEM_NEED "${PROC_SECMEM_FLOWS} * 8192")
+set(PROC_SECMEM_MAX 4096)
+while(PROC_SECMEM_MAX LESS PROC_SECMEM_NEED)
+ math(EXPR PROC_SECMEM_MAX "${PROC_SECMEM_MAX} * 2")
+endwhile()
+set(SECMEM_MINSIZE 32 CACHE STRING "Secure heap min alloc size")
# Container/deployment options
set(BUILD_CONTAINER FALSE CACHE BOOL
diff --git a/cmake/config/irmd.cmake b/cmake/config/irmd.cmake
index 2f5e7f02..b6b2dc40 100644
--- a/cmake/config/irmd.cmake
+++ b/cmake/config/irmd.cmake
@@ -23,7 +23,7 @@ set(OAP_REPLAY_TIMER 20 CACHE STRING
set(OAP_REPLAY_MAX 4096 CACHE STRING
"Maximum entries in the OAP replay cache (bounds memory/CPU under flood)")
set(OAP_CLIENT_AUTH_DEFAULT TRUE CACHE BOOL
- "Client requires the server to authenticate by default (FALSE for testing)")
+ "Client requires the server to authenticate by default")
set(DEBUG_PROTO_OAP FALSE CACHE BOOL
"Add Flow allocation protocol message output to IRMd debug logging")
diff --git a/cmake/config/lib.cmake b/cmake/config/lib.cmake
index 25130519..2c01b311 100644
--- a/cmake/config/lib.cmake
+++ b/cmake/config/lib.cmake
@@ -87,8 +87,42 @@ set(TPM_DEBUG_ABORT_TIMEOUT 0 CACHE STRING
"TPM abort process after a thread reaches this timeout (s), 0 disables")
# Encryption
-set(KEY_ROTATION_BIT 20 CACHE STRING
- "Bit position in packet counter that triggers key rotation (default 20 = every 2^20 packets)")
+set(KEY_LEAF_BITS 20 CACHE STRING
+ "Packets per leaf key as a power of two (2^20 = AEAD-safe default)")
+set(KEY_NODE_BITS 6 CACHE STRING
+ "Leaf keys per node key, power of two (2^6 = 64; leak compartment)")
+set(KEY_NODE_COUNT 128 CACHE STRING
+ "Node keys per batch (N); <= 4096, the 12-bit on-wire node index")
+set(KEY_REKEY_WATERMARK 4 CACHE STRING
+ "Re-key when this many node keys remain; 0 disables the count trigger")
+set(KEY_REPLAY_WINDOW 2048 CACHE STRING
+ "RX replay window in packets; power of two, >= 128")
+if(NOT KEY_REPLAY_WINDOW MATCHES "^[0-9]+$")
+ message(FATAL_ERROR "KEY_REPLAY_WINDOW must be a positive integer")
+endif()
+math(EXPR _krw_p2 "${KEY_REPLAY_WINDOW} & (${KEY_REPLAY_WINDOW} - 1)")
+if(KEY_REPLAY_WINDOW LESS 128 OR NOT _krw_p2 EQUAL 0)
+ message(FATAL_ERROR "KEY_REPLAY_WINDOW must be a power of two >= 128")
+endif()
+
+# Re-key must finish within its lead window - KEY_REKEY_WATERMARK node keys
+# worth of packets - before the batch exhausts and TX fails closed. dev.c only
+# evaluates the watermark once per FLOW_WM_CHECK writes, so a lead below ~2x
+# that leaves a high-rate flow no room to complete the exchange. Production
+# defaults are vast; this guards under-sized (test) geometries.
+if(KEY_REKEY_WATERMARK GREATER 0)
+ set(_rk_wm_check 65536) # FLOW_WM_CHECK in src/lib/dev.c (2^16)
+ math(EXPR _rk_lead
+ "${KEY_REKEY_WATERMARK} << (${KEY_LEAF_BITS} + ${KEY_NODE_BITS})")
+ math(EXPR _rk_min "2 * ${_rk_wm_check}")
+ if(_rk_lead LESS _rk_min)
+ message(WARNING
+ "Re-key lead is ${_rk_lead} packets vs the watermark check interval "
+ "${_rk_wm_check}; a high-rate flow may exhaust its key batch before the "
+ "re-key completes (TX fails closed until it does). Raise KEY_LEAF_BITS, "
+ "KEY_NODE_BITS, or KEY_REKEY_WATERMARK.")
+ endif()
+endif()
# Flow statistics (requires FUSE)
if(HAVE_FUSE)
diff --git a/cmake/dependencies.cmake b/cmake/dependencies.cmake
index 109fe1d6..ff44ad68 100644
--- a/cmake/dependencies.cmake
+++ b/cmake/dependencies.cmake
@@ -7,6 +7,7 @@ include(dependencies/system/libraries)
include(dependencies/system/explicit_bzero)
include(dependencies/system/robustmutex)
include(dependencies/system/fuse)
+include(dependencies/system/liburcu)
include(dependencies/system/sysrandom)
# Cryptography
diff --git a/cmake/dependencies/system/liburcu.cmake b/cmake/dependencies/system/liburcu.cmake
new file mode 100644
index 00000000..89a7ab12
--- /dev/null
+++ b/cmake/dependencies/system/liburcu.cmake
@@ -0,0 +1,45 @@
+# Userspace RCU (liburcu) - optional. Enables lock-free data-plane key
+# rotation; absent => per-flow rwlock fallback. The "bulletproof" flavour
+# (urcu-bp) auto-registers reader threads, so application threads need no
+# RCU lifecycle plumbing.
+if(PkgConfig_FOUND)
+ pkg_check_modules(URCU_PKG QUIET IMPORTED_TARGET liburcu-bp)
+ if(URCU_PKG_FOUND AND NOT TARGET Urcu::Urcu)
+ add_library(Urcu::Urcu ALIAS PkgConfig::URCU_PKG)
+ endif()
+endif()
+
+if(NOT URCU_PKG_FOUND)
+ find_library(URCU_BP_LIBRARY urcu-bp QUIET)
+ find_library(URCU_COMMON_LIBRARY urcu-common QUIET)
+ find_path(URCU_INCLUDE_DIR urcu-bp.h QUIET)
+ if(URCU_BP_LIBRARY AND URCU_COMMON_LIBRARY AND URCU_INCLUDE_DIR)
+ set(URCU_PKG_FOUND TRUE)
+ if(NOT TARGET Urcu::Urcu)
+ add_library(Urcu::Urcu INTERFACE IMPORTED)
+ set_target_properties(Urcu::Urcu PROPERTIES
+ INTERFACE_LINK_LIBRARIES "${URCU_BP_LIBRARY};${URCU_COMMON_LIBRARY}"
+ INTERFACE_INCLUDE_DIRECTORIES "${URCU_INCLUDE_DIR}")
+ endif()
+ endif()
+endif()
+
+if(URCU_PKG_FOUND)
+ set(DISABLE_LIBURCU FALSE CACHE BOOL "Disable liburcu (RCU) support")
+ if(NOT DISABLE_LIBURCU)
+ if(URCU_PKG_VERSION)
+ message(STATUS "liburcu (RCU) support enabled (version ${URCU_PKG_VERSION})")
+ else()
+ message(STATUS "liburcu (RCU) support enabled")
+ endif()
+ set(HAVE_LIBURCU TRUE CACHE INTERNAL "Userspace RCU (liburcu) available")
+ else()
+ message(STATUS "liburcu (RCU) support disabled by user")
+ unset(HAVE_LIBURCU CACHE)
+ endif()
+else()
+ message(STATUS "Install liburcu (urcu-bp) for lock-free data-plane re-keying")
+ unset(HAVE_LIBURCU CACHE)
+endif()
+
+mark_as_advanced(URCU_BP_LIBRARY URCU_COMMON_LIBRARY URCU_INCLUDE_DIR)
diff --git a/include/ouroboros/crypt.h b/include/ouroboros/crypt.h
index 543facaa..ce765158 100644
--- a/include/ouroboros/crypt.h
+++ b/include/ouroboros/crypt.h
@@ -28,7 +28,7 @@
#include <assert.h>
-#define IVSZ 16
+#define NONCESZ 16
#define SYMMKEYSZ 32
#define MAX_HASH_SIZE 64 /* SHA-512/BLAKE2b max */
#define KEX_ALGO_BUFSZ 32
@@ -102,11 +102,15 @@
#define IS_KEX_ALGO_SET(cfg) ((cfg)->x.nid != NID_undef)
#define IS_KEX_CIPHER_SET(cfg) ((cfg)->c.nid != NID_undef)
+/* Flow role: forks the per-direction keys so each end's TX = peer's RX. */
+#define CRYPT_ROLE_INIT 0 /* flow allocator / OAP client */
+#define CRYPT_ROLE_RESP 1 /* flow acceptor / OAP server */
struct crypt_sk {
int nid;
uint8_t * key;
- uint8_t rot_bit; /* Rotation bit to control epoch */
+ uint8_t epoch; /* installed batch epoch */
+ uint8_t role; /* CRYPT_ROLE_INIT / _RESP */
};
struct sec_config {
@@ -308,12 +312,16 @@ const char * md_nid_to_str(uint16_t nid);
uint16_t md_str_to_nid(const char * kdf);
-ssize_t md_digest(int md_nid,
- buffer_t in,
- uint8_t * out);
+ssize_t md_digest(int md_nid,
+ buffer_t in,
+ uint8_t * out);
ssize_t md_len(int md_nid);
+int crypt_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out);
+
int crypt_encrypt(struct crypt_ctx * ctx,
buffer_t in,
buffer_t * out);
@@ -322,10 +330,37 @@ int crypt_decrypt(struct crypt_ctx * ctx,
buffer_t in,
buffer_t * out);
-int crypt_get_ivsz(struct crypt_ctx * ctx);
+/* One-shot AEAD over an explicit key/nonce. out = ciphertext ‖ tag. */
+int crypt_oneshot_seal(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out);
+
+int crypt_oneshot_open(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out);
+
+int crypt_get_headsz(struct crypt_ctx * ctx);
int crypt_get_tagsz(struct crypt_ctx * ctx);
+int crypt_rekey(struct crypt_ctx * ctx,
+ struct crypt_sk * sk);
+
+/* Nodes remaining in the TX batch (re-key watermark). */
+int crypt_nodes_left(struct crypt_ctx * ctx);
+
+/* 1 once the peer has been observed on the current generation. */
+int crypt_peer_synced(struct crypt_ctx * ctx);
+
+/* Switch TX to the installed (new) batch (after peer synced/grace). */
+void crypt_tx_promote(struct crypt_ctx * ctx);
+
int crypt_load_crt_file(const char * path,
void ** crt);
diff --git a/include/ouroboros/rcu.h b/include/ouroboros/rcu.h
new file mode 100644
index 00000000..b4e7d27c
--- /dev/null
+++ b/include/ouroboros/rcu.h
@@ -0,0 +1,110 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Read-mostly pointer publication (RCU, with a locked fallback)
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1 as published by the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_LIB_RCU_H
+#define OUROBOROS_LIB_RCU_H
+
+/*
+ * Lock-free reads of published pointers via liburcu (urcu-bp) when
+ * available; a per-object rwlock fallback otherwise.
+ * Include config.h before this header so HAVE_LIBURCU is defined.
+ *
+ * Embed a struct rcu_guard in the object. A reader brackets its access
+ * with rcu_rdlock/rcu_rdunlock and reads published pointers via rcu_deref.
+ * A writer serialises with rcu_wrlock/rcu_wrunlock and publishes via
+ * rcu_assign; after unlock it reclaims a now-unreachable object with
+ * rcu_reclaim (waits out live readers) before freeing it. rcu_drain waits
+ * out all readers at teardown.
+ */
+
+#include <ouroboros/pthread.h>
+
+#ifdef HAVE_LIBURCU
+
+#include <urcu-bp.h>
+
+struct rcu_guard {
+ pthread_mutex_t w; /* serialises writers; readers use RCU */
+};
+
+#define rcu_guard_init(g) pthread_mutex_init(&(g)->w, NULL)
+#define rcu_guard_fini(g) pthread_mutex_destroy(&(g)->w)
+#define rcu_rdlock(g) ((void) (g), rcu_read_lock())
+#define rcu_rdunlock(g) ((void) (g), rcu_read_unlock())
+#define rcu_wrlock(g) pthread_mutex_lock(&(g)->w)
+#define rcu_wrunlock(g) pthread_mutex_unlock(&(g)->w)
+#define rcu_deref(p) rcu_dereference(p)
+#define rcu_assign(p, v) rcu_assign_pointer(p, v)
+#define rcu_reclaim(g) ((void) (g), synchronize_rcu())
+#define rcu_drain(g) ((void) (g), synchronize_rcu())
+
+/* TSan can miss the publish/consume barrier under urcu. */
+#if defined(__SANITIZE_THREAD__)
+#define RCU_TSAN_ANNOTATE
+#endif
+#if defined(__has_feature)
+#if __has_feature(thread_sanitizer)
+#define RCU_TSAN_ANNOTATE
+#endif
+#endif
+
+/*
+ * Publish/consume annotations re-expose liburcu's rcu_assign/rcu_deref edge to
+ * TSan, which cannot see liburcu's barriers. Call rcu_publish(p) before
+ * publishing p with rcu_assign, and rcu_consume(p) after reading it with
+ * rcu_deref. No-op without liburcu (the rwlock fallback already gives TSan the
+ * edge) or without TSan.
+ */
+#ifdef RCU_TSAN_ANNOTATE
+#include <sanitizer/tsan_interface.h>
+#define rcu_publish(p) __tsan_release(p)
+#define rcu_consume(p) __tsan_acquire(p)
+#else
+#define rcu_publish(p) ((void) (p))
+#define rcu_consume(p) ((void) (p))
+#endif
+
+#else /* !HAVE_LIBURCU : per-object rwlock fallback */
+
+struct rcu_guard {
+ pthread_rwlock_t rw; /* readers rd, writers wr */
+};
+
+#define rcu_guard_init(g) pthread_rwlock_init(&(g)->rw, NULL)
+#define rcu_guard_fini(g) pthread_rwlock_destroy(&(g)->rw)
+#define rcu_rdlock(g) pthread_rwlock_rdlock(&(g)->rw)
+#define rcu_rdunlock(g) pthread_rwlock_unlock(&(g)->rw)
+#define rcu_wrlock(g) pthread_rwlock_wrlock(&(g)->rw)
+#define rcu_wrunlock(g) pthread_rwlock_unlock(&(g)->rw)
+#define rcu_deref(p) (p)
+#define rcu_assign(p, v) ((p) = (v))
+#define rcu_reclaim(g) ((void) (g)) /* wrlock already excluded readers */
+#define rcu_drain(g) (pthread_rwlock_wrlock(&(g)->rw), \
+ pthread_rwlock_unlock(&(g)->rw))
+
+/* rwlock already gives TSan the publish/consume edge; no annotation. */
+#define rcu_publish(p) ((void) (p))
+#define rcu_consume(p) ((void) (p))
+
+#endif /* HAVE_LIBURCU */
+
+#endif /* OUROBOROS_LIB_RCU_H */
diff --git a/src/irmd/main.c b/src/irmd/main.c
index 66f341eb..484a265a 100644
--- a/src/irmd/main.c
+++ b/src/irmd/main.c
@@ -1717,6 +1717,13 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
return ret_msg;
}
+/* Wipe the session key from a reply before its buffers are freed. */
+static void clear_msg_key(irm_msg_t * msg)
+{
+ if (msg != NULL && msg->has_sym_key)
+ crypt_secure_clear(msg->sym_key.data, msg->sym_key.len);
+}
+
static void * mainloop(void * o)
{
int sfd;
@@ -1728,6 +1735,7 @@ static void * mainloop(void * o)
while (true) {
irm_msg_t * ret_msg;
struct cmd * cmd;
+ bool had_key;
pthread_mutex_lock(&irmd.cmd_lock);
@@ -1791,6 +1799,9 @@ static void * mainloop(void * o)
irm_msg__pack(ret_msg, buffer.data);
+ had_key = ret_msg->has_sym_key;
+ clear_msg_key(ret_msg);
+
irm_msg__free_unpacked(ret_msg, NULL);
pthread_cleanup_push(__cleanup_close_ptr, &sfd);
@@ -1805,6 +1816,9 @@ static void * mainloop(void * o)
strerror(errno));
}
+ if (had_key)
+ crypt_secure_clear(buffer.data, buffer.len);
+
pthread_cleanup_pop(true);
pthread_cleanup_pop(true);
@@ -1812,6 +1826,7 @@ static void * mainloop(void * o)
continue;
fail:
+ clear_msg_key(ret_msg);
irm_msg__free_unpacked(ret_msg, NULL);
fail_msg:
close(sfd);
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index 6cd3a8a4..3abf39d0 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -22,6 +22,7 @@ set(SOURCE_FILES_COMMON
crc/crc32.c
crc/crc64.c
crypt.c
+ crypt/keyrot.c
hash.c
lockfile.c
logs.c
@@ -92,6 +93,13 @@ if(HAVE_FUSE)
target_link_libraries(ouroboros-common PRIVATE Fuse::Fuse)
endif()
+if(HAVE_LIBURCU)
+ target_link_libraries(ouroboros-common PRIVATE Urcu::Urcu)
+ # urcu headers require C99; override the global -std=c89 for this TU only.
+ set_source_files_properties(crypt/keyrot.c PROPERTIES
+ COMPILE_OPTIONS "-std=gnu99")
+endif()
+
install(TARGETS ouroboros-common
EXPORT OuroborosTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
diff --git a/src/lib/config.h.in b/src/lib/config.h.in
index 7124a974..06c5e23f 100644
--- a/src/lib/config.h.in
+++ b/src/lib/config.h.in
@@ -37,7 +37,7 @@
#cmakedefine HAVE_OPENSSL_ML_DSA
#cmakedefine HAVE_OPENSSL_SLH_DSA
#define HAVE_ENCRYPTION
-#define SECMEM_GUARD @SECMEM_GUARD@
+#define SECMEM_MINSIZE @SECMEM_MINSIZE@
#endif
#define PROC_SECMEM_MAX @PROC_SECMEM_MAX@
@@ -70,6 +70,8 @@
#cmakedefine PROC_FLOW_STATS
#endif
+#cmakedefine HAVE_LIBURCU
+
#cmakedefine FRCT_DEBUG_STDOUT
#define PTHREAD_COND_CLOCK @PTHREAD_COND_CLOCK@
@@ -100,4 +102,8 @@
#define ACKQ_SLOTS (@ACK_WHEEL_SLOTS@)
#define ACKQ_RES (@ACK_WHEEL_RESOLUTION@) /* 2^N ns */
-#define KEY_ROTATION_BIT (@KEY_ROTATION_BIT@) /* Bit for key rotation */
+#define KEY_LEAF_BITS (@KEY_LEAF_BITS@) /* pkts/leaf-key = 2^n */
+#define KEY_NODE_BITS (@KEY_NODE_BITS@) /* leaf-keys/node = 2^n */
+#define KEY_NODE_COUNT (@KEY_NODE_COUNT@) /* node keys/batch N */
+#define KEY_REKEY_WATERMARK (@KEY_REKEY_WATERMARK@) /* node-keys-left trig */
+#define KEY_REPLAY_WINDOW (@KEY_REPLAY_WINDOW@) /* rx replay win pkts */
diff --git a/src/lib/crypt.c b/src/lib/crypt.c
index 9728ac8c..e4b65cf0 100644
--- a/src/lib/crypt.c
+++ b/src/lib/crypt.c
@@ -27,10 +27,14 @@
#include <config.h>
#include <ouroboros/errno.h>
+#include <ouroboros/pthread.h>
#include <ouroboros/random.h>
#include <ouroboros/crypt.h>
+#include "crypt/keyrot.h"
+
#ifdef HAVE_OPENSSL
+#include <openssl/crypto.h>
#include <openssl/evp.h>
#include "crypt/openssl.h"
#endif
@@ -50,18 +54,12 @@ static const struct nid_map cipher_nid_map[] = {
{NID_aes_192_gcm, "aes-192-gcm"},
{NID_aes_256_gcm, "aes-256-gcm"},
{NID_chacha20_poly1305, "chacha20-poly1305"},
- {NID_aes_128_ctr, "aes-128-ctr"},
- {NID_aes_192_ctr, "aes-192-ctr"},
- {NID_aes_256_ctr, "aes-256-ctr"},
{NID_undef, NULL}
};
/* Ordered in strength preference, lowest first */
const uint16_t crypt_supported_nids[] = {
#ifdef HAVE_OPENSSL
- NID_aes_128_ctr,
- NID_aes_192_ctr,
- NID_aes_256_ctr,
NID_aes_128_gcm,
NID_aes_192_gcm,
NID_aes_256_gcm,
@@ -87,23 +85,23 @@ static const struct nid_map kex_nid_map[] = {
{NID_undef, NULL}
};
-/* Ordered in strength preference, lowest first */
+/* Ordered in strength preference, lowest first (NIST SP 800-57 levels) */
const uint16_t kex_supported_nids[] = {
#ifdef HAVE_OPENSSL
- NID_ffdhe2048,
- NID_X9_62_prime256v1,
- NID_X25519,
- NID_ffdhe3072,
- NID_secp384r1,
- NID_ffdhe4096,
- NID_X448,
- NID_secp521r1,
+ NID_ffdhe2048, /* FFDHE-2048, ~112-bit */
+ NID_X9_62_prime256v1, /* ECDH P-256, 128-bit */
+ NID_X25519, /* ECDH X25519, 128-bit */
+ NID_ffdhe3072, /* FFDHE-3072, ~128-bit */
+ NID_ffdhe4096, /* FFDHE-4096, ~152-bit */
+ NID_secp384r1, /* ECDH P-384, 192-bit */
+ NID_X448, /* ECDH X448, 224-bit */
+ NID_secp521r1, /* ECDH P-521, 256-bit */
#ifdef HAVE_OPENSSL_ML_KEM
- NID_MLKEM512,
- NID_MLKEM768,
- NID_MLKEM1024,
- NID_X25519MLKEM768,
- NID_X448MLKEM1024,
+ NID_MLKEM512, /* ML-KEM-512, PQC L1 (~AES-128) */
+ NID_MLKEM768, /* ML-KEM-768, PQC L3 (~AES-192) */
+ NID_MLKEM1024, /* ML-KEM-1024, PQC L5 (~AES-256) */
+ NID_X25519MLKEM768, /* X25519 + ML-KEM-768, PQC L3 */
+ NID_X448MLKEM1024, /* X448 + ML-KEM-1024, PQC L5 */
#endif
#endif
NID_undef
@@ -137,7 +135,8 @@ const uint16_t md_supported_nids[] = {
};
struct crypt_ctx {
- void * ctx; /* Encryption context */
+ struct keyrot * kr; /* backend-independent key rotation */
+ void * cipher; /* backend AEAD cipher context */
};
struct auth_ctx {
@@ -623,19 +622,71 @@ int crypt_kex_rank(int nid)
return -1;
}
-/* Hash length now returned by md_digest() */
+/* AEAD primitive: 1:1 backend wrappers used by the data path below. */
+static int crypt_seal(void * cipher,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ uint8_t * out,
+ uint8_t * tag)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_seal(cipher, key, nonce, aad, in, out, tag);
+#else
+ (void) cipher;
+ (void) key;
+ (void) nonce;
+ (void) aad;
+ (void) in;
+ (void) out;
+ (void) tag;
-int crypt_encrypt(struct crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+ return -ECRYPT;
+#endif
+}
+
+static int crypt_open(void * cipher,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ const uint8_t * tag,
+ buffer_t * out)
{
- assert(ctx != NULL);
- assert(ctx->ctx != NULL);
+#ifdef HAVE_OPENSSL
+ return openssl_open(cipher, key, nonce, aad, in, tag, out);
+#else
+ (void) cipher;
+ (void) key;
+ (void) nonce;
+ (void) aad;
+ (void) in;
+ (void) tag;
+ (void) out;
+
+ return -ECRYPT;
+#endif
+}
+
+int crypt_oneshot_seal(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out)
+{
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
#ifdef HAVE_OPENSSL
- return openssl_encrypt(ctx->ctx, in, out);
+ return openssl_oneshot_seal(nid, key, nonce, aad, in, out);
#else
- (void) ctx;
+ (void) nid;
+ (void) key;
+ (void) nonce;
+ (void) aad;
(void) in;
(void) out;
@@ -643,17 +694,24 @@ int crypt_encrypt(struct crypt_ctx * ctx,
#endif
}
-int crypt_decrypt(struct crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+int crypt_oneshot_open(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out)
{
- assert(ctx != NULL);
- assert(ctx->ctx != NULL);
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
#ifdef HAVE_OPENSSL
- return openssl_decrypt(ctx->ctx, in, out);
+ return openssl_oneshot_open(nid, key, nonce, aad, in, out);
#else
- (void) ctx;
+ (void) nid;
+ (void) key;
+ (void) nonce;
+ (void) aad;
(void) in;
(void) out;
@@ -661,6 +719,115 @@ int crypt_decrypt(struct crypt_ctx * ctx,
#endif
}
+/*
+ * Data-path encrypt: rotate the key, frame selector ‖ ct ‖ tag, seal.
+ * Backend-agnostic: composed from keyrot_*, crypt_seal and crypt_get_tagsz.
+ */
+int crypt_encrypt(struct crypt_ctx * ctx,
+ buffer_t in,
+ buffer_t * out)
+{
+ uint8_t nonce[KR_NONCE_LEN];
+ const uint8_t * key;
+ uint8_t * ct;
+ buffer_t aad;
+ int tagsz;
+ int out_sz;
+
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ tagsz = crypt_get_tagsz(ctx);
+ if (tagsz < 0)
+ return -ECRYPT;
+
+ out->data = malloc(KR_SELECTOR_LEN + in.len + (size_t) tagsz);
+ if (out->data == NULL)
+ goto fail_malloc;
+
+ ct = out->data + KR_SELECTOR_LEN;
+
+ /* keyrot writes the selector into the wire header (== AAD). */
+ if (keyrot_tx_next(ctx->kr, out->data, &key, nonce) != 0)
+ goto fail_encrypt;
+
+ aad.data = out->data;
+ aad.len = KR_SELECTOR_LEN;
+
+ out_sz = crypt_seal(ctx->cipher, key, nonce, aad, in, ct, ct + in.len);
+ if (out_sz < 0)
+ goto fail_encrypt;
+
+ out->len = KR_SELECTOR_LEN + (size_t) out_sz + (size_t) tagsz;
+
+ return 0;
+ fail_encrypt:
+ free(out->data);
+ fail_malloc:
+ clrbuf(*out);
+ return -ECRYPT;
+}
+
+/*
+ * Data-path decrypt: look up the rotated key from the selector, open, and
+ * commit the replay window only after the tag verifies.
+ */
+int crypt_decrypt(struct crypt_ctx * ctx,
+ buffer_t in,
+ buffer_t * out)
+{
+ uint8_t nonce[KR_NONCE_LEN];
+ const uint8_t * key;
+ const uint8_t * tag;
+ struct kr_rx rx;
+ buffer_t aad;
+ buffer_t ct;
+ int tagsz;
+ int in_sz;
+
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ tagsz = crypt_get_tagsz(ctx);
+ if (tagsz < 0)
+ return -ECRYPT;
+
+ if (in.len < (size_t) (KR_SELECTOR_LEN + tagsz))
+ return -ECRYPT;
+
+ if (keyrot_rx_lookup(ctx->kr, in.data, &key, nonce, &rx) != 0)
+ return -ECRYPT;
+
+ in_sz = (int) in.len - KR_SELECTOR_LEN - tagsz;
+
+ /* +1 keeps malloc(0) defined for an empty (zero-length) frame. */
+ out->data = malloc((size_t) in_sz + 1);
+ if (out->data == NULL)
+ goto fail_malloc;
+
+ aad.data = in.data;
+ aad.len = KR_SELECTOR_LEN;
+
+ ct.data = in.data + KR_SELECTOR_LEN;
+ ct.len = (size_t) in_sz;
+
+ tag = in.data + KR_SELECTOR_LEN + in_sz;
+
+ if (crypt_open(ctx->cipher, key, nonce, aad, ct, tag, out) < 0)
+ goto fail_decrypt;
+
+ /* Commit replay state only after the tag verifies. */
+ if (keyrot_rx_commit(ctx->kr, &rx) != 0)
+ goto fail_decrypt;
+
+ return 0;
+ fail_decrypt:
+ free(out->data);
+ fail_malloc:
+ clrbuf(*out);
+ return -ECRYPT;
+}
+
struct crypt_ctx * crypt_create_ctx(struct crypt_sk * sk)
{
struct crypt_ctx * crypt;
@@ -674,16 +841,23 @@ struct crypt_ctx * crypt_create_ctx(struct crypt_sk * sk)
memset(crypt, 0, sizeof(*crypt));
+ crypt->kr = keyrot_create(sk->key, sk->epoch, sk->role);
+ if (crypt->kr == NULL)
+ goto fail_kr;
+
#ifdef HAVE_OPENSSL
- crypt->ctx = openssl_crypt_create_ctx(sk);
- if (crypt->ctx == NULL)
- goto fail_ctx;
+ crypt->cipher = openssl_crypt_create_ctx(sk);
+ if (crypt->cipher == NULL)
+ goto fail_cipher;
#endif
return crypt;
+
#ifdef HAVE_OPENSSL
- fail_ctx:
- free(crypt);
+ fail_cipher:
+ keyrot_destroy(crypt->kr);
#endif
+ fail_kr:
+ free(crypt);
fail_crypt:
return NULL;
}
@@ -693,43 +867,70 @@ void crypt_destroy_ctx(struct crypt_ctx * crypt)
if (crypt == NULL)
return;
+ keyrot_destroy(crypt->kr);
#ifdef HAVE_OPENSSL
- assert(crypt->ctx != NULL);
- openssl_crypt_destroy_ctx(crypt->ctx);
-#else
- assert(crypt->ctx == NULL);
+ openssl_crypt_destroy_ctx(crypt->cipher);
#endif
free(crypt);
}
-int crypt_get_ivsz(struct crypt_ctx * ctx)
+int crypt_get_headsz(struct crypt_ctx * ctx)
{
- if (ctx == NULL)
- return -EINVAL;
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
-#ifdef HAVE_OPENSSL
- assert(ctx->ctx != NULL);
- return openssl_crypt_get_ivsz(ctx->ctx);
-#else
- assert(ctx->ctx == NULL);
- return -ENOTSUP;
-#endif
+ (void) ctx; /* validated only; header size is a constant */
+
+ return KR_SELECTOR_LEN;
+}
+
+int crypt_rekey(struct crypt_ctx * ctx,
+ struct crypt_sk * sk)
+{
+ assert(ctx != NULL);
+ assert(sk != NULL);
+ assert(ctx->kr != NULL);
+
+ return keyrot_rekey(ctx->kr, sk->key, sk->epoch) == 0 ? 0 : -ECRYPT;
}
int crypt_get_tagsz(struct crypt_ctx * ctx)
{
- if (ctx == NULL)
- return -EINVAL;
+ assert(ctx != NULL);
+ assert(ctx->cipher != NULL);
#ifdef HAVE_OPENSSL
- assert(ctx->ctx != NULL);
- return openssl_crypt_get_tagsz(ctx->ctx);
+ return openssl_crypt_get_tagsz(ctx->cipher);
#else
- assert(ctx->ctx == NULL);
+ (void) ctx;
return -ENOTSUP;
#endif
}
+int crypt_nodes_left(struct crypt_ctx * ctx)
+{
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ return (int) keyrot_tx_nodes_left(ctx->kr);
+}
+
+int crypt_peer_synced(struct crypt_ctx * ctx)
+{
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ return keyrot_peer_switched(ctx->kr) ? 1 : 0;
+}
+
+void crypt_tx_promote(struct crypt_ctx * ctx)
+{
+ assert(ctx != NULL);
+ assert(ctx->kr != NULL);
+
+ keyrot_tx_promote(ctx->kr);
+}
+
int crypt_load_privkey_file(const char * path,
void ** key)
{
@@ -1157,10 +1358,25 @@ ssize_t md_len(int md_nid)
#endif
}
+int crypt_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out)
+{
+#ifdef HAVE_OPENSSL
+ return openssl_hkdf_expand(key, info, out) == 0 ? 0 : -ECRYPT;
+#else
+ (void) key;
+ (void) info;
+ (void) out;
+
+ return -ECRYPT;
+#endif
+}
+
int crypt_secure_malloc_init(size_t max)
{
#ifdef HAVE_OPENSSL
- return openssl_secure_malloc_init(max, SECMEM_GUARD);
+ return openssl_secure_malloc_init(max, SECMEM_MINSIZE);
#else
(void) max;
return 0;
diff --git a/src/lib/crypt/keyrot.c b/src/lib/crypt/keyrot.c
new file mode 100644
index 00000000..8b0d9429
--- /dev/null
+++ b/src/lib/crypt/keyrot.c
@@ -0,0 +1,741 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Data-plane key-rotation schedule (node/leaf keys, selector)
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1 as published by the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <config.h>
+
+#include <ouroboros/atomics.h>
+#include <ouroboros/crypt.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/rcu.h>
+
+#include "crypt/keyrot.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * Per-flow keys are addressed by (epoch, node, leaf) and derived as:
+ * root = per-batch HKDF PRK from the OAP exchange, wiped once expanded
+ * nodes = HKDF-Expand(root, "o7s-keyrot-node") -> KEY_NODE_COUNT keys
+ * leaf = HKDF-Expand(node, "o7s-keyrot-leaf"|dir|leaf) -> AEAD key
+ * The epoch is a small wrapping counter, carried in the selector, that picks
+ * the live batch; a Tier-2 OAP re-key advances it. The "dir" byte forks the
+ * leaf keys per direction.
+ *
+ * Concurrency: cur/prev batch pointers are published by a re-key and read on
+ * the data path under an rcu_guard (lock-free RCU with liburcu, else a per-
+ * keyrot rwlock). The per-batch TX counter is atomic, so the (epoch, counter)
+ * nonce is unique without serialising TX. Leaf caches are THREAD-LOCAL (an app
+ * writer and the FRCT retransmit timer never share cache state), keyed on a
+ * global batch id and direct-mapped.
+ */
+
+#define KR_WITHIN_BITS (KEY_LEAF_BITS + KEY_NODE_BITS)
+#define KR_WITHIN_MASK (((uint64_t) 1 << KR_WITHIN_BITS) - 1)
+#define KR_N (KEY_NODE_COUNT)
+#define KR_LEAVES (1u << KEY_NODE_BITS)
+#define KR_BATCH_MAX ((uint64_t) KR_N << KR_WITHIN_BITS)
+#define KR_NODES_SZ ((size_t) KR_N * SYMMKEYSZ)
+#define KR_TCACHE_WAYS 16 /* per-thread cache slots per direction (pow2) */
+#define KR_EPOCHS 16 /* 4-bit wire epoch: gens before wrap */
+
+#define KR_RP_WORDS (KEY_REPLAY_WINDOW / 64) /* pow2; RFC 6479 bitmap */
+#define KR_RP_SHIFT 6
+#define KR_RP_MASK 63
+#define KR_RP_WINDOW (KEY_REPLAY_WINDOW - 64) /* reserve 1 slack word */
+
+static const char kr_node_label[] = "o7s-keyrot-node";
+static const char kr_leaf_label[] = "o7s-keyrot-leaf";
+
+struct kr_batch {
+ uint64_t id; /* process-global, unique; cache key (no ABA) */
+ uint8_t epoch; /* 4-bit wire selector */
+ uint8_t * nodes; /* KR_NODES_SZ in secure heap; NULL if empty */
+ uint64_t tx_ctr; /* atomic; per-batch so nonces never collide */
+
+ struct { /* RFC 6479-like anti-replay window */
+ uint64_t last; /* highest accepted ctr + 1 */
+ uint64_t bits[KR_RP_WORDS];
+ pthread_mutex_t mtx;
+ } rp;
+};
+
+struct kr_keycache {
+ uint8_t * key; /* SYMMKEYSZ, points into the per-thread slab */
+ uint64_t id; /* batch the cached key belongs to */
+ uint16_t node;
+ uint8_t leaf;
+ uint8_t dir;
+ bool valid;
+};
+
+struct keyrot {
+ struct kr_batch * cur; /* published; read on data path */
+ struct kr_batch * prev; /* NULL = none */
+ struct rcu_guard guard; /* re-key vs readers */
+ uint8_t role;
+ uint8_t tx_epoch; /* epoch TX currently stamps */
+ bool peer_switched; /* peer is on the cur epoch */
+};
+
+/* Per-thread leaf-key caches, freed by the thread-exit destructor. */
+struct kr_tcache {
+ struct kr_keycache tx[KR_TCACHE_WAYS];
+ struct kr_keycache rx[KR_TCACHE_WAYS];
+ uint8_t * slab; /* 2*KR_TCACHE_WAYS*SYMMKEYSZ secure heap */
+};
+
+static struct {
+ uint64_t next_id; /* batch-id allocator (atomic) */
+ pthread_key_t tcache_key; /* per-thread leaf-key caches */
+ pthread_once_t tcache_once;
+} kr_g = { 0, 0, PTHREAD_ONCE_INIT };
+
+static void kr_tcache_free(void * p)
+{
+ struct kr_tcache * t = p;
+
+ if (t == NULL)
+ return;
+
+ crypt_secure_free(t->slab, 2 * KR_TCACHE_WAYS * SYMMKEYSZ);
+ free(t);
+}
+
+static void kr_tcache_init(void)
+{
+ pthread_key_create(&kr_g.tcache_key, kr_tcache_free);
+}
+
+static struct kr_tcache * kr_tcache_get(void)
+{
+ struct kr_tcache * t;
+ size_t i;
+
+ pthread_once(&kr_g.tcache_once, kr_tcache_init);
+
+ t = pthread_getspecific(kr_g.tcache_key);
+ if (t != NULL)
+ return t;
+
+ t = malloc(sizeof(*t));
+ if (t == NULL)
+ goto fail_alloc;
+
+ memset(t, 0, sizeof(*t));
+
+ t->slab = crypt_secure_malloc(2 * KR_TCACHE_WAYS * SYMMKEYSZ);
+ if (t->slab == NULL)
+ goto fail_slab;
+
+ for (i = 0; i < KR_TCACHE_WAYS; i++) {
+ t->tx[i].key = t->slab + i * SYMMKEYSZ;
+ t->rx[i].key = t->slab + (KR_TCACHE_WAYS + i) * SYMMKEYSZ;
+ }
+
+ if (pthread_setspecific(kr_g.tcache_key, t) != 0)
+ goto fail_set;
+
+ return t;
+
+ fail_set:
+ crypt_secure_free(t->slab, 2 * KR_TCACHE_WAYS * SYMMKEYSZ);
+ fail_slab:
+ free(t);
+ fail_alloc:
+ return NULL;
+}
+
+static uint8_t * kr_expand_nodes(const uint8_t * root)
+{
+ uint8_t * nodes;
+ buffer_t prk;
+ buffer_t info;
+ buffer_t okm;
+
+ nodes = crypt_secure_malloc(KR_NODES_SZ);
+ if (nodes == NULL)
+ return NULL;
+
+ prk.len = SYMMKEYSZ;
+ prk.data = (uint8_t *) root;
+ info.len = sizeof(kr_node_label) - 1;
+ info.data = (uint8_t *) kr_node_label;
+ okm.len = KR_NODES_SZ;
+ okm.data = nodes;
+
+ if (crypt_hkdf_expand(prk, info, okm) != 0)
+ goto fail_expand;
+
+ return nodes;
+
+ fail_expand:
+ crypt_secure_free(nodes, KR_NODES_SZ);
+ return NULL;
+}
+
+static int kr_leaf_key(const uint8_t * node,
+ uint8_t leaf,
+ uint8_t dir,
+ uint8_t * out)
+{
+ uint8_t info_buf[sizeof(kr_leaf_label) - 1 + 2];
+ buffer_t prk;
+ buffer_t info;
+ buffer_t okm;
+ size_t n = sizeof(kr_leaf_label) - 1;
+
+ memcpy(info_buf, kr_leaf_label, n);
+ info_buf[n] = dir;
+ info_buf[n + 1] = leaf;
+
+ prk.len = SYMMKEYSZ;
+ prk.data = (uint8_t *) node;
+ info.len = n + 2;
+ info.data = info_buf;
+ okm.len = SYMMKEYSZ;
+ okm.data = out;
+
+ return crypt_hkdf_expand(prk, info, okm);
+}
+
+static __inline__ bool kr_kc_hit(const struct kr_keycache * kc,
+ const struct kr_batch * b,
+ uint16_t node,
+ uint8_t leaf,
+ uint8_t dir)
+{
+ if (!kc->valid)
+ return false;
+
+ if (kc->id != b->id)
+ return false;
+
+ if (kc->node != node)
+ return false;
+
+ if (kc->leaf != leaf)
+ return false;
+
+ return kc->dir == dir;
+}
+
+/* Fetch the leaf key; derive into the (direct-mapped) slot on a miss. */
+static const uint8_t * kr_kc_get(struct kr_keycache * cache,
+ const struct kr_batch * b,
+ uint16_t node,
+ uint8_t leaf,
+ uint8_t dir)
+{
+ struct kr_keycache * kc;
+ uint8_t * nkey;
+
+ kc = &cache[b->id & (KR_TCACHE_WAYS - 1)];
+
+ if (kr_kc_hit(kc, b, node, leaf, dir))
+ return kc->key;
+
+ nkey = b->nodes + (size_t) node * SYMMKEYSZ;
+ if (kr_leaf_key(nkey, leaf, dir, kc->key) != 0)
+ return NULL;
+
+ kc->valid = true;
+ kc->id = b->id;
+ kc->node = node;
+ kc->leaf = leaf;
+ kc->dir = dir;
+
+ return kc->key;
+}
+
+static void kr_sel_enc(uint8_t epoch,
+ uint16_t node,
+ uint32_t seq,
+ uint8_t sel[KR_SELECTOR_LEN])
+{
+ sel[0] = (uint8_t) ((epoch << 4) | ((node >> 8) & 0x0F));
+ sel[1] = (uint8_t) (node & 0xFF);
+ sel[2] = (uint8_t) (seq >> 24);
+ sel[3] = (uint8_t) (seq >> 16);
+ sel[4] = (uint8_t) (seq >> 8);
+ sel[5] = (uint8_t) (seq);
+}
+
+static void kr_sel_dec(const uint8_t sel[KR_SELECTOR_LEN],
+ uint8_t * epoch,
+ uint16_t * node,
+ uint32_t * seq)
+{
+ *epoch = (uint8_t) (sel[0] >> 4);
+ *node = (uint16_t) (((sel[0] & 0x0F) << 8) | sel[1]);
+ *seq = ((uint32_t) sel[2] << 24) | ((uint32_t) sel[3] << 16) |
+ ((uint32_t) sel[4] << 8) | (uint32_t) sel[5];
+}
+
+static uint64_t kr_ctr(uint16_t node,
+ uint32_t seq)
+{
+ return ((uint64_t) node << KR_WITHIN_BITS) |
+ ((uint64_t) seq & KR_WITHIN_MASK);
+}
+
+static void kr_nonce(uint64_t ctr,
+ uint8_t * nonce)
+{
+ size_t i;
+
+ memset(nonce, 0, KR_NONCE_LEN);
+
+ /* ctr big-endian in the low 8 bytes; high bytes stay zero */
+ for (i = 0; i < 8; i++)
+ nonce[i] = (uint8_t) (ctr >> (56 - 8 * i));
+}
+
+static struct kr_batch * kr_batch_create(uint8_t epoch,
+ const uint8_t * root)
+{
+ struct kr_batch * b;
+
+ b = malloc(sizeof(*b));
+ if (b == NULL)
+ goto fail_alloc;
+
+ b->nodes = kr_expand_nodes(root);
+ if (b->nodes == NULL)
+ goto fail_nodes;
+
+ b->id = FETCH_ADD_RELAXED(&kr_g.next_id, 1);
+ b->epoch = epoch;
+ b->tx_ctr = 0;
+ if (pthread_mutex_init(&b->rp.mtx, NULL) != 0)
+ goto fail_lock;
+
+ b->rp.last = 0;
+ memset(b->rp.bits, 0, sizeof(b->rp.bits));
+
+ return b;
+
+ fail_lock:
+ crypt_secure_free(b->nodes, KR_NODES_SZ);
+ free(b);
+ return NULL;
+ fail_nodes:
+ free(b);
+ fail_alloc:
+ return NULL;
+}
+
+static void kr_batch_free(struct kr_batch * b)
+{
+ if (b == NULL)
+ return;
+
+ pthread_mutex_destroy(&b->rp.mtx);
+ crypt_secure_free(b->nodes, KR_NODES_SZ);
+ free(b);
+}
+
+/*
+ * RFC 6479 anti-replay window keyed on the per-batch counter, with
+ * seq = ctr + 1 so 0 means "nothing accepted yet". Returns 0 if the
+ * packet is fresh (and records it), -1 on a replay or a too-old ctr.
+ */
+static int kr_rp_commit(struct kr_batch * b,
+ uint64_t ctr)
+{
+ uint64_t seq;
+ uint64_t idx;
+ uint64_t cur;
+ uint64_t diff;
+
+ seq = ctr + 1;
+
+ pthread_mutex_lock(&b->rp.mtx);
+
+ if (seq > b->rp.last) {
+ idx = seq >> KR_RP_SHIFT;
+ cur = b->rp.last >> KR_RP_SHIFT;
+ diff = idx - cur;
+ if (diff > KR_RP_WORDS)
+ diff = KR_RP_WORDS;
+
+ while (diff-- > 0) {
+ cur++;
+ b->rp.bits[cur & (KR_RP_WORDS - 1)] = 0;
+ }
+
+ b->rp.bits[idx & (KR_RP_WORDS - 1)] |=
+ (uint64_t) 1 << (seq & KR_RP_MASK);
+ b->rp.last = seq;
+ goto finish;
+ }
+
+ if (b->rp.last - seq >= KR_RP_WINDOW)
+ goto fail;
+
+ idx = seq >> KR_RP_SHIFT;
+ if (b->rp.bits[idx & (KR_RP_WORDS - 1)]
+ & ((uint64_t) 1 << (seq & KR_RP_MASK)))
+ goto fail;
+
+ b->rp.bits[idx & (KR_RP_WORDS - 1)] |=
+ (uint64_t) 1 << (seq & KR_RP_MASK);
+ finish:
+ pthread_mutex_unlock(&b->rp.mtx);
+
+ return 0;
+ fail:
+ pthread_mutex_unlock(&b->rp.mtx);
+
+ return -1;
+}
+
+struct keyrot * keyrot_create(const uint8_t * root,
+ uint8_t epoch,
+ uint8_t role)
+{
+ struct keyrot * kr;
+
+ assert(root != NULL);
+ assert(role <= 1);
+
+ if (epoch >= KR_EPOCHS)
+ goto fail_kr;
+
+ kr = malloc(sizeof(*kr));
+ if (kr == NULL)
+ goto fail_kr;
+
+ memset(kr, 0, sizeof(*kr));
+
+ kr->role = role;
+ kr->tx_epoch = epoch;
+ kr->peer_switched = true;
+ kr->prev = NULL;
+
+ kr->cur = kr_batch_create(epoch, root);
+ if (kr->cur == NULL)
+ goto fail_cur;
+
+ if (rcu_guard_init(&kr->guard))
+ goto fail_guard;
+
+ return kr;
+
+ fail_guard:
+ kr_batch_free(kr->cur);
+ fail_cur:
+ free(kr);
+ fail_kr:
+ return NULL;
+}
+
+void keyrot_destroy(struct keyrot * kr)
+{
+ if (kr == NULL)
+ return;
+
+ /* Wait out any in-flight reader before freeing batches. */
+ rcu_drain(&kr->guard);
+
+ kr_batch_free(kr->cur);
+ kr_batch_free(kr->prev);
+
+ rcu_guard_fini(&kr->guard);
+
+ free(kr);
+}
+
+int keyrot_rekey(struct keyrot * kr,
+ const uint8_t * root,
+ uint8_t epoch)
+{
+ struct kr_batch * nb;
+ struct kr_batch * old_prev;
+
+ assert(kr != NULL);
+ assert(root != NULL);
+
+ if (epoch >= KR_EPOCHS)
+ return -1;
+
+ nb = kr_batch_create(epoch, root);
+ if (nb == NULL)
+ return -1;
+
+ rcu_wrlock(&kr->guard);
+
+ old_prev = kr->prev;
+ rcu_assign(kr->prev, kr->cur);
+ rcu_publish(nb);
+ rcu_assign(kr->cur, nb);
+
+ /* TX keeps the old epoch until the peer is seen on the new one. */
+ STORE_RELEASE(&kr->peer_switched, false);
+
+ rcu_wrunlock(&kr->guard);
+
+ /* old_prev is unreachable now; reclaim past any live reader. */
+ rcu_reclaim(&kr->guard);
+ kr_batch_free(old_prev);
+
+ return 0;
+}
+
+void keyrot_tx_promote(struct keyrot * kr)
+{
+ assert(kr != NULL);
+
+ /* Serialise with keyrot_rekey so tx_epoch tracks a consistent cur. */
+ rcu_wrlock(&kr->guard);
+ STORE_RELAXED(&kr->tx_epoch, rcu_deref(kr->cur)->epoch);
+ rcu_wrunlock(&kr->guard);
+}
+
+int keyrot_tx_next(struct keyrot * kr,
+ uint8_t sel[KR_SELECTOR_LEN],
+ const uint8_t ** key,
+ uint8_t nonce[KR_NONCE_LEN])
+{
+ struct kr_tcache * tc;
+ struct kr_batch * cur;
+ struct kr_batch * prev;
+ struct kr_batch * b;
+ uint64_t ctr;
+ uint16_t node;
+ uint8_t leaf;
+ uint8_t txe;
+ uint8_t epoch;
+ uint32_t seq;
+ const uint8_t * k;
+
+ assert(kr != NULL);
+ assert(key != NULL);
+
+ tc = kr_tcache_get();
+ if (tc == NULL)
+ return -1;
+
+ rcu_rdlock(&kr->guard);
+
+ cur = rcu_deref(kr->cur);
+ prev = rcu_deref(kr->prev);
+ rcu_consume(cur);
+ rcu_consume(prev);
+ txe = LOAD_RELAXED(&kr->tx_epoch);
+
+ if (cur->epoch == txe)
+ b = cur;
+ else if (prev != NULL && prev->epoch == txe)
+ b = prev;
+ else
+ b = NULL;
+
+ if (b == NULL) {
+ rcu_rdunlock(&kr->guard);
+ return -1; /* tx_epoch batch gone; next promote resyncs */
+ }
+
+ /* Slot reserved even if exhausted; tx_nodes_left clamps the count. */
+ ctr = FETCH_ADD_RELAXED(&b->tx_ctr, 1);
+ if (ctr >= KR_BATCH_MAX) {
+ rcu_rdunlock(&kr->guard);
+ return -1; /* batch exhausted */
+ }
+
+ node = (uint16_t) (ctr >> KR_WITHIN_BITS);
+ leaf = (uint8_t) ((ctr >> KEY_LEAF_BITS) & (KR_LEAVES - 1));
+ seq = (uint32_t) (ctr & KR_WITHIN_MASK);
+ epoch = b->epoch;
+
+ k = kr_kc_get(tc->tx, b, node, leaf, kr->role);
+
+ rcu_rdunlock(&kr->guard);
+
+ if (k == NULL)
+ return -1;
+
+ kr_sel_enc(epoch, node, seq, sel);
+ kr_nonce(ctr, nonce);
+
+ *key = k;
+
+ return 0;
+}
+
+int keyrot_rx_lookup(struct keyrot * kr,
+ const uint8_t sel[KR_SELECTOR_LEN],
+ const uint8_t ** key,
+ uint8_t nonce[KR_NONCE_LEN],
+ struct kr_rx * rx)
+{
+ struct kr_tcache * tc;
+ struct kr_batch * cur;
+ struct kr_batch * prev;
+ struct kr_batch * b;
+ uint8_t epoch;
+ uint16_t node;
+ uint32_t seq;
+ uint64_t ctr;
+ uint8_t leaf;
+ const uint8_t * k;
+
+ assert(kr != NULL);
+ assert(key != NULL);
+
+ kr_sel_dec(sel, &epoch, &node, &seq);
+
+ if (node >= KR_N)
+ return -1;
+
+ tc = kr_tcache_get();
+ if (tc == NULL)
+ return -1;
+
+ rcu_rdlock(&kr->guard);
+
+ cur = rcu_deref(kr->cur);
+ prev = rcu_deref(kr->prev);
+ rcu_consume(cur);
+ rcu_consume(prev);
+
+ if (epoch == cur->epoch) {
+ b = cur;
+ } else if (prev != NULL && epoch == prev->epoch) {
+ b = prev;
+ } else {
+ rcu_rdunlock(&kr->guard);
+ return -1; /* unknown epoch */
+ }
+
+ ctr = kr_ctr(node, seq);
+ leaf = (uint8_t) ((ctr >> KEY_LEAF_BITS) & (KR_LEAVES - 1));
+
+ /* peer's tx direction */
+ k = kr_kc_get(tc->rx, b, node, leaf, (uint8_t) (kr->role ^ 1));
+
+ rx->id = b->id;
+ rx->ctr = ctr;
+
+ rcu_rdunlock(&kr->guard);
+
+ if (k == NULL)
+ return -1;
+
+ kr_nonce(ctr, nonce);
+
+ *key = k;
+
+ return 0;
+}
+
+/*
+ * Commit a packet that authenticated under the batch keyrot_rx_lookup
+ * selected. Re-finds that batch by id (epoch may have advanced) and,
+ * if still resident, advances the replay window and records that the
+ * peer is on the current batch. Runs only post-AEAD so a forged or
+ * replayed packet can mutate no receiver state. Returns -1 on replay.
+ */
+int keyrot_rx_commit(struct keyrot * kr,
+ const struct kr_rx * rx)
+{
+ struct kr_batch * cur;
+ struct kr_batch * prev;
+ struct kr_batch * b;
+ int rc;
+
+ assert(kr != NULL);
+ assert(rx != NULL);
+
+ rcu_rdlock(&kr->guard);
+
+ cur = rcu_deref(kr->cur);
+ prev = rcu_deref(kr->prev);
+ rcu_consume(cur);
+ rcu_consume(prev);
+
+ if (cur->id == rx->id)
+ b = cur;
+ else if (prev != NULL && prev->id == rx->id)
+ b = prev;
+ else
+ b = NULL;
+
+ if (b == NULL) {
+ rcu_rdunlock(&kr->guard);
+ return 0; /* batch evicted post-auth; nothing to protect */
+ }
+
+ rc = kr_rp_commit(b, rx->ctr);
+ if (rc == 0 && b == cur)
+ STORE_RELEASE(&kr->peer_switched, true);
+
+ rcu_rdunlock(&kr->guard);
+
+ return rc;
+}
+
+bool keyrot_peer_switched(const struct keyrot * kr)
+{
+ assert(kr != NULL);
+
+ return LOAD_ACQUIRE(&kr->peer_switched);
+}
+
+unsigned keyrot_tx_nodes_left(struct keyrot * kr)
+{
+ struct kr_batch * cur;
+ struct kr_batch * prev;
+ struct kr_batch * b;
+ uint64_t ctr;
+ unsigned used;
+ uint8_t txe;
+
+ assert(kr != NULL);
+
+ rcu_rdlock(&kr->guard);
+ cur = rcu_deref(kr->cur);
+ prev = rcu_deref(kr->prev);
+ rcu_consume(cur);
+ rcu_consume(prev);
+ txe = LOAD_RELAXED(&kr->tx_epoch);
+
+ if (cur->epoch == txe)
+ b = cur;
+ else if (prev != NULL && prev->epoch == txe)
+ b = prev;
+ else
+ b = NULL;
+
+ ctr = b != NULL ? LOAD_RELAXED(&b->tx_ctr) : KR_BATCH_MAX;
+ rcu_rdunlock(&kr->guard);
+
+ used = (unsigned) (ctr >> KR_WITHIN_BITS);
+ if (used >= KR_N)
+ return 0;
+
+ return KR_N - used;
+}
diff --git a/src/lib/crypt/keyrot.h b/src/lib/crypt/keyrot.h
new file mode 100644
index 00000000..6a598f76
--- /dev/null
+++ b/src/lib/crypt/keyrot.h
@@ -0,0 +1,74 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Data-plane key-rotation schedule (node/leaf keys, selector)
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1 as published by the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_LIB_CRYPT_KEYROT_H
+#define OUROBOROS_LIB_CRYPT_KEYROT_H
+
+#include <ouroboros/crypt.h> /* SYMMKEYSZ, NONCESZ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define KR_SELECTOR_LEN 6
+#define KR_NONCE_LEN NONCESZ
+
+struct keyrot;
+
+struct kr_rx {
+ uint64_t id; /* batch id of the matched epoch */
+ uint64_t ctr; /* packet counter for replay check */
+};
+
+struct keyrot * keyrot_create(const uint8_t * root,
+ uint8_t epoch,
+ uint8_t role);
+
+void keyrot_destroy(struct keyrot * kr);
+
+int keyrot_rekey(struct keyrot * kr,
+ const uint8_t * root,
+ uint8_t epoch);
+
+/* Promote TX to the installed (new) batch once the peer is on it. */
+void keyrot_tx_promote(struct keyrot * kr);
+
+int keyrot_tx_next(struct keyrot * kr,
+ uint8_t sel[KR_SELECTOR_LEN],
+ const uint8_t ** key,
+ uint8_t nonce[KR_NONCE_LEN]);
+
+int keyrot_rx_lookup(struct keyrot * kr,
+ const uint8_t sel[KR_SELECTOR_LEN],
+ const uint8_t ** key,
+ uint8_t nonce[KR_NONCE_LEN],
+ struct kr_rx * rx);
+
+/* Commit an authenticated packet: replay window + peer-switched. */
+int keyrot_rx_commit(struct keyrot * kr,
+ const struct kr_rx * rx);
+
+/* True once an RX packet under the current batch has been observed. */
+bool keyrot_peer_switched(const struct keyrot * kr);
+
+unsigned keyrot_tx_nodes_left(struct keyrot * kr);
+
+#endif /* OUROBOROS_LIB_CRYPT_KEYROT_H */
diff --git a/src/lib/crypt/openssl.c b/src/lib/crypt/openssl.c
index d4ffc00b..7a4abec9 100644
--- a/src/lib/crypt/openssl.c
+++ b/src/lib/crypt/openssl.c
@@ -53,27 +53,14 @@
#define HKDF_INFO_DHE "o7s-ossl-dhe"
#define HKDF_INFO_ENCAP "o7s-ossl-encap"
-#define HKDF_INFO_ROTATION "o7s-key-rotation"
#define HKDF_SALT_LEN 32 /* SHA-256 output size */
+#define AEAD_NONCE_LEN 12 /* 96-bit deterministic IV (SP 800-38D) */
+#define AEAD_TAG_LEN 16 /* 128-bit AEAD authentication tag */
struct ossl_crypt_ctx {
EVP_CIPHER_CTX * evp_ctx;
const EVP_CIPHER * cipher;
- int ivsz;
int tagsz;
-
- struct {
- uint8_t * cur; /* current key */
- uint8_t * prv; /* rotated key */
- } keys;
-
- struct {
- uint32_t cntr; /* counter */
- uint32_t mask; /* phase mask */
- uint32_t age; /* counter within epoch */
- uint8_t phase; /* current key phase */
- uint8_t salt[HKDF_SALT_LEN];
- } rot; /* rotation logic */
};
struct kdf_info {
@@ -84,17 +71,6 @@ struct kdf_info {
buffer_t key;
};
-/* Key rotation macros */
-#define HAS_PHASE_BIT_TOGGLED(ctx) \
- (((ctx)->rot.cntr & (ctx)->rot.mask) != \
- (((ctx)->rot.cntr - 1) & (ctx)->rot.mask))
-
-#define HAS_GRACE_EXPIRED(ctx) \
- ((ctx)->rot.age >= ((ctx)->rot.mask >> 1))
-
-#define ROTATION_TOO_RECENT(ctx) \
- ((ctx)->rot.age < ((ctx)->rot.mask - ((ctx)->rot.mask >> 2)))
-
/* Convert hash NID to OpenSSL digest name string for HKDF */
static const char * hash_nid_to_digest_name(int nid)
{
@@ -145,21 +121,20 @@ static int get_pk_bytes_from_key(EVP_PKEY * key,
}
/* Derive salt from public key bytes by hashing them */
-static int derive_salt_from_pk_bytes(buffer_t pk,
- uint8_t * salt,
- size_t salt_len)
+static int derive_salt_from_pk_bytes(buffer_t pk,
+ buffer_t salt)
{
uint8_t hash[EVP_MAX_MD_SIZE];
unsigned hash_len;
assert(pk.data != NULL);
- assert(salt != NULL);
+ assert(salt.data != NULL);
if (EVP_Digest(pk.data, pk.len, hash, &hash_len,
EVP_sha256(), NULL) != 1)
goto fail_digest;
- memcpy(salt, hash, salt_len < hash_len ? salt_len : hash_len);
+ memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len);
return 0;
fail_digest:
@@ -167,10 +142,9 @@ static int derive_salt_from_pk_bytes(buffer_t pk,
}
/* Derive salt from two public key byte buffers (DHE) in canonical order */
-static int derive_salt_from_pk_bytes_dhe(buffer_t local,
- buffer_t remote,
- uint8_t * salt,
- size_t salt_len)
+static int derive_salt_from_pk_bytes_dhe(buffer_t local,
+ buffer_t remote,
+ buffer_t salt)
{
uint8_t * concat;
size_t concat_len;
@@ -181,7 +155,7 @@ static int derive_salt_from_pk_bytes_dhe(buffer_t local,
assert(local.data != NULL);
assert(remote.data != NULL);
- assert(salt != NULL);
+ assert(salt.data != NULL);
concat_len = local.len + remote.len;
concat = OPENSSL_malloc(concat_len);
@@ -205,7 +179,7 @@ static int derive_salt_from_pk_bytes_dhe(buffer_t local,
OPENSSL_free(concat);
- memcpy(salt, hash, salt_len < hash_len ? salt_len : hash_len);
+ memcpy(salt.data, hash, salt.len < hash_len ? salt.len : hash_len);
return 0;
fail_digest:
@@ -259,117 +233,144 @@ static int derive_key_hkdf(struct kdf_info * ki)
return -ECRYPT;
}
-/* Key rotation helper functions implementation */
-static int should_rotate_key_rx(struct ossl_crypt_ctx * ctx,
- uint8_t rx_phase)
+int openssl_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out)
{
- assert(ctx != NULL);
+ EVP_KDF * kdf;
+ EVP_KDF_CTX * kctx;
+ OSSL_PARAM params[5];
+ int mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY;
+ int idx = 0;
+ int ret = -1;
+
+ kdf = EVP_KDF_fetch(NULL, "HKDF", NULL);
+ if (kdf == NULL)
+ goto fail_fetch;
- /* Phase must have changed */
- if (rx_phase == ctx->rot.phase)
- return 0;
+ kctx = EVP_KDF_CTX_new(kdf);
+ if (kctx == NULL)
+ goto fail_ctx;
+
+ params[idx++] = OSSL_PARAM_construct_utf8_string(
+ "digest", (char *) "SHA256", 0);
+ params[idx++] = OSSL_PARAM_construct_int("mode", &mode);
+ params[idx++] = OSSL_PARAM_construct_octet_string(
+ "key", key.data, key.len);
+ params[idx++] = OSSL_PARAM_construct_octet_string(
+ "info", info.data, info.len);
+ params[idx] = OSSL_PARAM_construct_end();
- if (ROTATION_TOO_RECENT(ctx))
- return 0;
+ if (EVP_KDF_derive(kctx, out.data, out.len, params) == 1)
+ ret = 0;
- return 1;
+ EVP_KDF_CTX_free(kctx);
+ fail_ctx:
+ EVP_KDF_free(kdf);
+ fail_fetch:
+ return ret;
}
-static int rotate_key(struct ossl_crypt_ctx * ctx)
+/* AEAD seal: encrypt in with key/nonce, bind aad, append tag */
+int openssl_seal(struct ossl_crypt_ctx * ctx,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ uint8_t * out,
+ uint8_t * tag)
{
- struct kdf_info ki;
- uint8_t * tmp;
+ int out_sz;
+ int tmp_sz;
assert(ctx != NULL);
+ assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */
- /* Swap keys - move current to prev */
- tmp = ctx->keys.prv;
- ctx->keys.prv = ctx->keys.cur;
+ EVP_CIPHER_CTX_reset(ctx->evp_ctx);
- if (tmp != NULL) {
- /* Reuse old prev_key memory for new key */
- ctx->keys.cur = tmp;
- } else {
- /* First rotation - allocate new memory */
- ctx->keys.cur = OPENSSL_secure_malloc(SYMMKEYSZ);
- if (ctx->keys.cur == NULL)
- return -ECRYPT;
- }
+ if (EVP_EncryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL,
+ NULL, NULL) != 1)
+ return -1;
- /* Derive new key from previous key using HKDF */
- ki.secret.data = ctx->keys.prv;
- ki.secret.len = SYMMKEYSZ;
- ki.nid = NID_sha256;
- ki.salt.data = ctx->rot.salt;
- ki.salt.len = HKDF_SALT_LEN;
- ki.info.data = (uint8_t *) HKDF_INFO_ROTATION;
- ki.info.len = strlen(HKDF_INFO_ROTATION);
- ki.key.data = ctx->keys.cur;
- ki.key.len = SYMMKEYSZ;
+ /* Pin the AEAD nonce to 96 bits (SP 800-38D deterministic IV). */
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_IVLEN,
+ AEAD_NONCE_LEN, NULL) != 1)
+ return -1;
- if (derive_key_hkdf(&ki) != 0)
- return -ECRYPT;
+ if (EVP_EncryptInit_ex(ctx->evp_ctx, NULL, NULL,
+ key, nonce) != 1)
+ return -1;
- ctx->rot.age = 0;
- ctx->rot.phase = !ctx->rot.phase;
+ if (EVP_EncryptUpdate(ctx->evp_ctx, NULL, &tmp_sz,
+ aad.data, (int) aad.len) != 1)
+ return -1;
- return 0;
-}
+ if (EVP_EncryptUpdate(ctx->evp_ctx, out, &out_sz,
+ in.data, (int) in.len) != 1)
+ return -1;
-static void cleanup_old_key(struct ossl_crypt_ctx * ctx)
-{
- assert(ctx != NULL);
+ if (EVP_EncryptFinal_ex(ctx->evp_ctx, out + out_sz, &tmp_sz) != 1)
+ return -1;
- if (ctx->keys.prv == NULL)
- return;
+ out_sz += tmp_sz;
- if (!HAS_GRACE_EXPIRED(ctx))
- return;
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_GET_TAG,
+ ctx->tagsz, tag) != 1)
+ return -1;
- OPENSSL_secure_clear_free(ctx->keys.prv, SYMMKEYSZ);
- ctx->keys.prv = NULL;
+ return out_sz;
}
-static int try_decrypt(struct ossl_crypt_ctx * ctx,
- uint8_t * key,
- uint8_t * iv,
- uint8_t * input,
- int in_sz,
- uint8_t * out,
- int * out_sz)
+/* AEAD open: decrypt in with key/nonce, verify aad and tag */
+int openssl_open(struct ossl_crypt_ctx * ctx,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ const uint8_t * tag,
+ buffer_t * out)
{
- uint8_t * tag;
- int tmp_sz;
- int ret;
+ int out_sz;
+ int tmp_sz;
- tag = input + in_sz;
+ assert(ctx != NULL);
+ assert(ctx->tagsz > 0); /* AEAD mandated at ctx creation */
EVP_CIPHER_CTX_reset(ctx->evp_ctx);
- ret = EVP_DecryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL, key, iv);
- if (ret != 1)
+ if (EVP_DecryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL,
+ NULL, NULL) != 1)
return -1;
- if (ctx->tagsz > 0) {
- ret = EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_TAG,
- ctx->tagsz, tag);
- if (ret != 1)
- return -1;
- }
+ /* Pin the AEAD nonce to 96 bits (SP 800-38D deterministic IV). */
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_IVLEN,
+ AEAD_NONCE_LEN, NULL) != 1)
+ return -1;
- ret = EVP_DecryptUpdate(ctx->evp_ctx, out, &tmp_sz, input, in_sz);
- if (ret != 1)
+ if (EVP_DecryptInit_ex(ctx->evp_ctx, NULL, NULL, key, nonce) != 1)
return -1;
- *out_sz = tmp_sz;
+ if (EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_SET_TAG,
+ ctx->tagsz, (void *) tag) != 1)
+ return -1;
- ret = EVP_DecryptFinal_ex(ctx->evp_ctx, out + tmp_sz, &tmp_sz);
- if (ret != 1)
+ if (EVP_DecryptUpdate(ctx->evp_ctx, NULL, &tmp_sz,
+ aad.data, (int) aad.len) != 1)
return -1;
- *out_sz += tmp_sz;
+ if (EVP_DecryptUpdate(ctx->evp_ctx, out->data, &out_sz,
+ in.data, (int) in.len) != 1)
+ return -1;
- return 0;
+ if (EVP_DecryptFinal_ex(ctx->evp_ctx, out->data + out_sz,
+ &tmp_sz) != 1)
+ return -1;
+
+ out_sz += tmp_sz;
+
+ out->len = (size_t) out_sz;
+
+ return out_sz;
}
/*
@@ -397,11 +398,14 @@ static int __openssl_dhe_derive(EVP_PKEY * pkp,
ret = i2d_PUBKEY(pkp, &local_pk.data);
if (ret <= 0)
goto fail_local;
+
local_pk.len = (size_t) ret;
+ ki.salt.len = HKDF_SALT_LEN;
+ ki.salt.data = salt_buf;
+
/* Derive salt from both public keys */
- if (derive_salt_from_pk_bytes_dhe(local_pk, remote_pk, salt_buf,
- HKDF_SALT_LEN) < 0)
+ if (derive_salt_from_pk_bytes_dhe(local_pk, remote_pk, ki.salt) < 0)
goto fail_salt;
ctx = EVP_PKEY_CTX_new(pkp, NULL);
@@ -438,8 +442,6 @@ static int __openssl_dhe_derive(EVP_PKEY * pkp,
ki.info.data = (uint8_t *) HKDF_INFO_DHE;
ki.key.len = SYMMKEYSZ;
ki.key.data = s;
- ki.salt.len = HKDF_SALT_LEN;
- ki.salt.data = salt_buf;
/* Derive symmetric key from shared secret using HKDF */
ret = derive_key_hkdf(&ki);
@@ -718,13 +720,17 @@ ssize_t openssl_kem_encap(buffer_t pk,
EVP_PKEY * pub;
uint8_t * pos;
uint8_t salt[HKDF_SALT_LEN];
+ buffer_t salt_b;
ssize_t ret;
assert(pk.data != NULL);
assert(ct != NULL);
assert(s != NULL);
- if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0)
+ salt_b.len = HKDF_SALT_LEN;
+ salt_b.data = salt;
+
+ if (derive_salt_from_pk_bytes(pk, salt_b) < 0)
goto fail_salt;
pos = pk.data;
@@ -750,13 +756,17 @@ ssize_t openssl_kem_encap_raw(buffer_t pk,
EVP_PKEY * pub;
const char * algo;
uint8_t salt[HKDF_SALT_LEN];
+ buffer_t salt_b;
ssize_t ret;
assert(pk.data != NULL);
assert(ct != NULL);
assert(s != NULL);
- if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0)
+ salt_b.len = HKDF_SALT_LEN;
+ salt_b.data = salt;
+
+ if (derive_salt_from_pk_bytes(pk, salt_b) < 0)
goto fail_salt;
algo = __openssl_hybrid_algo_from_len(pk.len);
@@ -790,12 +800,16 @@ int openssl_kem_decap(EVP_PKEY * priv,
size_t secret_len;
int ret;
uint8_t salt[HKDF_SALT_LEN];
+ buffer_t salt_b;
/* Extract public key bytes from private key */
if (get_pk_bytes_from_key(priv, &pk) < 0)
goto fail_pk;
- if (derive_salt_from_pk_bytes(pk, salt, HKDF_SALT_LEN) < 0)
+ salt_b.len = HKDF_SALT_LEN;
+ salt_b.data = salt;
+
+ if (derive_salt_from_pk_bytes(pk, salt_b) < 0)
goto fail_salt;
ctx = EVP_PKEY_CTX_new(priv, NULL);
@@ -858,13 +872,14 @@ void openssl_pkp_destroy(EVP_PKEY * pkp)
EVP_PKEY_free(pkp);
}
-int __openssl_get_curve(EVP_PKEY * pub,
- char * algo)
+static int openssl_get_curve(EVP_PKEY * pub,
+ char * algo)
{
int ret;
size_t len = KEX_ALGO_BUFSZ;
ret = EVP_PKEY_get_utf8_string_param(pub, "group", algo, len, &len);
+
return ret == 1 ? 0 : -ECRYPT;
}
@@ -889,9 +904,10 @@ int openssl_get_algo_from_pk_der(buffer_t pk,
strcpy(algo, type_str);
- if ((IS_EC_GROUP(algo) || IS_DH_GROUP(algo)) &&
- __openssl_get_curve(pub, algo) < 0)
- goto fail_pub;
+ if (IS_EC_GROUP(algo) || IS_DH_GROUP(algo)) {
+ if (openssl_get_curve(pub, algo) < 0)
+ goto fail_pub;
+ }
EVP_PKEY_free(pub);
return 0;
@@ -949,141 +965,122 @@ int openssl_dhe_derive(EVP_PKEY * pkp,
return -ECRYPT;
}
-int openssl_encrypt(struct ossl_crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+/* Set up a fresh AEAD cipher ctx for nid: reject non-AEAD / oversized IV. */
+static int ossl_cipher_ctx_init(struct ossl_crypt_ctx * ctx,
+ int nid)
{
- uint8_t * ptr;
- uint8_t * iv;
- int in_sz;
- int out_sz;
- int tmp_sz;
- int ret;
-
- assert(ctx != NULL);
-
- in_sz = (int) in.len;
-
- out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + \
- ctx->ivsz + ctx->tagsz);
- if (out->data == NULL)
- goto fail_malloc;
-
- iv = out->data;
- ptr = out->data + ctx->ivsz;
+ ctx->cipher = EVP_get_cipherbynid(nid);
+ if (ctx->cipher == NULL)
+ return -1;
- if (random_buffer(iv, ctx->ivsz) < 0)
- goto fail_encrypt;
+ /* IV must fit the NONCESZ nonce buffer. */
+ if (EVP_CIPHER_get_iv_length(ctx->cipher) > NONCESZ)
+ return -1;
- /* Set IV bit 7 to current key phase (KEY_ROTATION_BIT of counter) */
- if (ctx->rot.cntr & ctx->rot.mask)
- iv[0] |= 0x80;
- else
- iv[0] &= 0x7F;
+ /* Authenticated encryption is mandatory; reject non-AEAD ciphers. */
+ if ((EVP_CIPHER_flags(ctx->cipher) & EVP_CIPH_FLAG_AEAD_CIPHER) == 0)
+ return -1;
- EVP_CIPHER_CTX_reset(ctx->evp_ctx);
+ ctx->tagsz = AEAD_TAG_LEN;
- ret = EVP_EncryptInit_ex(ctx->evp_ctx, ctx->cipher, NULL,
- ctx->keys.cur, iv);
- if (ret != 1)
- goto fail_encrypt;
+ ctx->evp_ctx = EVP_CIPHER_CTX_new();
+ if (ctx->evp_ctx == NULL)
+ return -1;
- ret = EVP_EncryptUpdate(ctx->evp_ctx, ptr, &tmp_sz, in.data, in_sz);
- if (ret != 1)
- goto fail_encrypt;
+ return 0;
+}
- out_sz = tmp_sz;
- ret = EVP_EncryptFinal_ex(ctx->evp_ctx, ptr + tmp_sz, &tmp_sz);
- if (ret != 1)
- goto fail_encrypt;
+/* One-shot AEAD seal over an explicit key/nonce (no keyrot). out = ct ‖ tag. */
+int openssl_oneshot_seal(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out)
+{
+ struct ossl_crypt_ctx ctx;
+ int out_sz;
- out_sz += tmp_sz;
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
- /* For AEAD ciphers, get and append the authentication tag */
- if (ctx->tagsz > 0) {
- ret = EVP_CIPHER_CTX_ctrl(ctx->evp_ctx, EVP_CTRL_AEAD_GET_TAG,
- ctx->tagsz, ptr + out_sz);
- if (ret != 1)
- goto fail_encrypt;
- out_sz += ctx->tagsz;
- }
+ memset(&ctx, 0, sizeof(ctx));
- assert(out_sz >= in_sz);
+ if (ossl_cipher_ctx_init(&ctx, nid) < 0)
+ goto fail_cipher;
- out->len = (size_t) out_sz + ctx->ivsz;
+ out->data = malloc(in.len + EVP_MAX_BLOCK_LENGTH + ctx.tagsz);
+ if (out->data == NULL)
+ goto fail_ctx;
- /* Increment packet counter and check for key rotation */
- ctx->rot.cntr++;
- ctx->rot.age++;
+ out_sz = openssl_seal(&ctx, key, nonce, aad, in,
+ out->data, out->data + in.len);
+ if (out_sz < 0)
+ goto fail_seal;
- if (HAS_PHASE_BIT_TOGGLED(ctx)) {
- if (rotate_key(ctx) != 0)
- goto fail_encrypt;
- }
+ out->len = (size_t) out_sz + ctx.tagsz;
- cleanup_old_key(ctx);
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
return 0;
- fail_encrypt:
+
+ fail_seal:
free(out->data);
- fail_malloc:
+ fail_ctx:
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
+ fail_cipher:
clrbuf(*out);
return -ECRYPT;
}
-int openssl_decrypt(struct ossl_crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out)
+/* One-shot AEAD open; in = ct ‖ tag, verifies aad and tag. */
+int openssl_oneshot_open(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out)
{
- uint8_t * iv;
- uint8_t * input;
- uint8_t rx_phase;
- int out_sz;
- int in_sz;
-
- assert(ctx != NULL);
-
- in_sz = (int) in.len - ctx->ivsz;
- if (in_sz < ctx->tagsz)
- return -ECRYPT;
+ struct ossl_crypt_ctx ctx;
+ buffer_t ct;
+ const uint8_t * tag;
+ int in_sz;
- in_sz -= ctx->tagsz;
+ assert(key != NULL);
+ assert(nonce != NULL);
+ assert(out != NULL);
- out->data = malloc(in_sz + EVP_MAX_BLOCK_LENGTH);
- if (out->data == NULL)
- goto fail_malloc;
+ memset(&ctx, 0, sizeof(ctx));
- iv = in.data;
- input = in.data + ctx->ivsz;
+ if (ossl_cipher_ctx_init(&ctx, nid) < 0)
+ goto fail_cipher;
- /* Extract phase from IV bit 7 and check for key rotation */
- rx_phase = (iv[0] & 0x80) ? 1 : 0;
+ if (in.len < (size_t) ctx.tagsz)
+ goto fail_ctx;
- if (should_rotate_key_rx(ctx, rx_phase)) {
- if (rotate_key(ctx) != 0)
- goto fail_decrypt;
- }
+ in_sz = (int) in.len - ctx.tagsz;
- ctx->rot.cntr++;
- ctx->rot.age++;
+ out->data = malloc((size_t) in_sz + EVP_MAX_BLOCK_LENGTH);
+ if (out->data == NULL)
+ goto fail_ctx;
- if (try_decrypt(ctx, ctx->keys.cur, iv, input, in_sz, out->data,
- &out_sz) != 0) {
- if (ctx->keys.prv == NULL)
- goto fail_decrypt;
- if (try_decrypt(ctx, ctx->keys.prv, iv, input, in_sz,
- out->data, &out_sz) != 0)
- goto fail_decrypt;
- }
+ ct.data = in.data;
+ ct.len = (size_t) in_sz;
+ tag = in.data + in_sz;
- assert(out_sz <= in_sz);
+ if (openssl_open(&ctx, key, nonce, aad, ct, tag, out) < 0)
+ goto fail_open;
- out->len = (size_t) out_sz;
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
return 0;
- fail_decrypt:
+
+ fail_open:
free(out->data);
- fail_malloc:
+ fail_ctx:
+ EVP_CIPHER_CTX_free(ctx.evp_ctx);
+ fail_cipher:
clrbuf(*out);
return -ECRYPT;
}
@@ -1094,51 +1091,19 @@ struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk)
assert(sk != NULL);
assert(sk->key != NULL);
- assert(sk->rot_bit > 0 && sk->rot_bit < 32);
ctx = malloc(sizeof(*ctx));
if (ctx == NULL)
- goto fail_malloc;
+ goto fail_malloc;
memset(ctx, 0, sizeof(*ctx));
- ctx->keys.cur = OPENSSL_secure_malloc(SYMMKEYSZ);
- if (ctx->keys.cur == NULL)
- goto fail_key;
-
- memcpy(ctx->keys.cur, sk->key, SYMMKEYSZ);
-
- ctx->keys.prv = NULL;
-
- /* Derive rotation salt from initial shared secret */
- if (EVP_Digest(sk->key, SYMMKEYSZ, ctx->rot.salt, NULL,
- EVP_sha256(), NULL) != 1)
- goto fail_cipher;
-
- ctx->cipher = EVP_get_cipherbynid(sk->nid);
- if (ctx->cipher == NULL)
- goto fail_cipher;
-
- ctx->ivsz = EVP_CIPHER_iv_length(ctx->cipher);
-
- /* Set tag size for AEAD ciphers (GCM, CCM, OCB, ChaCha20-Poly1305) */
- if (EVP_CIPHER_flags(ctx->cipher) & EVP_CIPH_FLAG_AEAD_CIPHER)
- ctx->tagsz = 16; /* Standard AEAD tag length (128 bits) */
-
- ctx->rot.cntr = 0;
- ctx->rot.mask = (1U << sk->rot_bit);
- ctx->rot.age = 0;
- ctx->rot.phase = 0;
-
- ctx->evp_ctx = EVP_CIPHER_CTX_new();
- if (ctx->evp_ctx == NULL)
+ if (ossl_cipher_ctx_init(ctx, sk->nid) < 0)
goto fail_cipher;
return ctx;
fail_cipher:
- OPENSSL_secure_clear_free(ctx->keys.cur, SYMMKEYSZ);
- fail_key:
free(ctx);
fail_malloc:
return NULL;
@@ -1149,23 +1114,10 @@ void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx)
if (ctx == NULL)
return;
- if (ctx->keys.cur != NULL)
- OPENSSL_secure_clear_free(ctx->keys.cur, SYMMKEYSZ);
-
- if (ctx->keys.prv != NULL)
- OPENSSL_secure_clear_free(ctx->keys.prv, SYMMKEYSZ);
-
EVP_CIPHER_CTX_free(ctx->evp_ctx);
free(ctx);
}
-int openssl_crypt_get_ivsz(struct ossl_crypt_ctx * ctx)
-{
- assert(ctx != NULL);
-
- return ctx->ivsz;
-}
-
int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx)
{
assert(ctx != NULL);
@@ -1937,9 +1889,10 @@ void * openssl_secure_malloc(size_t size)
return OPENSSL_secure_malloc(size);
}
-void openssl_secure_free(void * ptr)
+void openssl_secure_free(void * ptr,
+ size_t size)
{
- OPENSSL_secure_free(ptr);
+ OPENSSL_secure_clear_free(ptr, size);
}
void openssl_secure_clear(void * ptr,
@@ -1947,6 +1900,7 @@ void openssl_secure_clear(void * ptr,
{
OPENSSL_cleanse(ptr, size);
}
+
void openssl_cleanup(void)
{
OPENSSL_cleanup();
diff --git a/src/lib/crypt/openssl.h b/src/lib/crypt/openssl.h
index 2578a0d2..e5cc35f7 100644
--- a/src/lib/crypt/openssl.h
+++ b/src/lib/crypt/openssl.h
@@ -61,20 +61,44 @@ int openssl_get_algo_from_pk_der(buffer_t pk,
int openssl_get_algo_from_pk_raw(buffer_t pk,
char * algo);
-int openssl_encrypt(struct ossl_crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out);
-
-int openssl_decrypt(struct ossl_crypt_ctx * ctx,
- buffer_t in,
- buffer_t * out);
+int openssl_seal(struct ossl_crypt_ctx * ctx,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ uint8_t * out,
+ uint8_t * tag);
+
+int openssl_open(struct ossl_crypt_ctx * ctx,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ const uint8_t * tag,
+ buffer_t * out);
+
+int openssl_oneshot_seal(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out);
+
+int openssl_oneshot_open(int nid,
+ const uint8_t * key,
+ const uint8_t * nonce,
+ buffer_t aad,
+ buffer_t in,
+ buffer_t * out);
+
+int openssl_hkdf_expand(buffer_t key,
+ buffer_t info,
+ buffer_t out);
struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk);
void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx);
-int openssl_crypt_get_ivsz(struct ossl_crypt_ctx * ctx);
-
int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx);
/* AUTHENTICATION */
diff --git a/src/lib/dev.c b/src/lib/dev.c
index ce358ac4..543bd13e 100644
--- a/src/lib/dev.c
+++ b/src/lib/dev.c
@@ -98,7 +98,7 @@ struct flow {
ssize_t part_idx;
struct crypt_ctx * crypt;
- int headsz; /* IV */
+ int headsz; /* selector */
int tailsz; /* Tag + CRC */
struct timespec snd_act;
@@ -296,7 +296,7 @@ static int spb_decrypt(struct flow * flow,
in.len = ssm_pk_buff_len(spb);
if (crypt_decrypt(flow->crypt, in, &out) < 0)
- return -ENOMEM;
+ return -ECRYPT;
head = ssm_pk_buff_pop(spb, flow->headsz) + flow->headsz;
@@ -711,11 +711,10 @@ static int flow_init(struct flow_info * info,
flow->tailsz = 0;
if (IS_ENCRYPTED(sk)) {
- sk->rot_bit = KEY_ROTATION_BIT;
flow->crypt = crypt_create_ctx(sk);
if (flow->crypt == NULL)
goto fail_crypt;
- flow->headsz = crypt_get_ivsz(flow->crypt);
+ flow->headsz = crypt_get_headsz(flow->crypt);
flow->tailsz = crypt_get_tagsz(flow->crypt);
}
@@ -1012,7 +1011,9 @@ int flow_accept(qosspec_t * qs,
if (err < 0)
return err;
- crypt.key = key;
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_RESP;
err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
@@ -1067,7 +1068,9 @@ int flow_alloc(const char * dst,
clock_gettime(PTHREAD_COND_CLOCK, &t1);
- crypt.key = key;
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_INIT;
err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
@@ -1106,7 +1109,9 @@ int flow_join(const char * dst,
if (err < 0)
return err;
- crypt.key = key;
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_INIT;
err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
@@ -2228,7 +2233,8 @@ int np1_flow_alloc(pid_t n_pid,
int flow_id)
{
struct flow_info flow;
- struct crypt_sk crypt = { .nid = NID_undef, .key = NULL };
+ struct crypt_sk crypt = { .nid = NID_undef, .key = NULL,
+ .epoch = 0, .role = CRYPT_ROLE_INIT };
memset(&flow, 0, sizeof(flow));
@@ -2321,7 +2327,9 @@ int ipcp_flow_req_arr(const buffer_t * dst,
if (err < 0)
return err;
- crypt.key = key;
+ crypt.key = key;
+ crypt.epoch = 0;
+ crypt.role = CRYPT_ROLE_INIT;
err = flow__irm_result_des(&msg, &flow, &crypt);
if (err < 0)
diff --git a/src/lib/tests/CMakeLists.txt b/src/lib/tests/CMakeLists.txt
index 32836589..002d94af 100644
--- a/src/lib/tests/CMakeLists.txt
+++ b/src/lib/tests/CMakeLists.txt
@@ -14,6 +14,7 @@ create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
hash_test.c
kex_test.c
kex_test_ml_kem.c
+ keyrot_test.c
md5_test.c
sha3_test.c
sockets_test.c
@@ -24,6 +25,9 @@ create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_SOURCE_DIR}/src/lib)
+
disable_test_logging_for_target(${PARENT_DIR}_test)
target_link_libraries(${PARENT_DIR}_test ouroboros-common)
diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c
index 028c4eb5..2d752238 100644
--- a/src/lib/tests/crypt_test.c
+++ b/src/lib/tests/crypt_test.c
@@ -30,6 +30,7 @@
#include <stdio.h>
#define TEST_PACKET_SIZE 1500
+#define TEST_N_PACKETS 1000
extern const uint16_t crypt_supported_nids[];
extern const uint16_t md_supported_nids[];
@@ -39,9 +40,10 @@ static int test_crypt_create_destroy(void)
struct crypt_ctx * ctx;
uint8_t key[SYMMKEYSZ];
struct crypt_sk sk = {
- .nid = NID_aes_256_gcm,
- .key = key,
- .rot_bit = KEY_ROTATION_BIT
+ .nid = NID_aes_256_gcm,
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
};
TEST_START();
@@ -67,18 +69,27 @@ static int test_crypt_create_destroy(void)
static int test_crypt_encrypt_decrypt(int nid)
{
uint8_t pkt[TEST_PACKET_SIZE];
- struct crypt_ctx * ctx;
+ struct crypt_ctx * tx;
+ struct crypt_ctx * rx;
uint8_t key[SYMMKEYSZ];
- struct crypt_sk sk = {
- .nid = NID_aes_256_gcm,
- .key = key,
- .rot_bit = KEY_ROTATION_BIT
+ struct crypt_sk sk_tx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
+ };
+ struct crypt_sk sk_rx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_RESP
};
buffer_t in;
buffer_t out;
buffer_t out2;
const char * cipher;
+ sk_tx.nid = nid;
+ sk_rx.nid = nid;
+
cipher = crypt_nid_to_str(nid);
TEST_START("(%s)", cipher);
@@ -92,53 +103,63 @@ static int test_crypt_encrypt_decrypt(int nid)
goto fail_init;
}
- ctx = crypt_create_ctx(&sk);
- if (ctx == NULL) {
- printf("Failed to initialize cryptography.\n");
+ tx = crypt_create_ctx(&sk_tx);
+ if (tx == NULL) {
+ printf("Failed to initialize TX cryptography.\n");
goto fail_init;
}
+ rx = crypt_create_ctx(&sk_rx);
+ if (rx == NULL) {
+ printf("Failed to initialize RX cryptography.\n");
+ goto fail_tx;
+ }
+
in.len = sizeof(pkt);
in.data = pkt;
- if (crypt_encrypt(ctx, in, &out) < 0) {
+ if (crypt_encrypt(tx, in, &out) < 0) {
printf("Encryption failed.\n");
goto fail_encrypt;
}
if (out.len < in.len) {
printf("Encryption returned too little data.\n");
- goto fail_encrypt;
+ goto fail_chk;
}
- if (crypt_decrypt(ctx, out, &out2) < 0) {
+ if (crypt_decrypt(rx, out, &out2) < 0) {
printf("Decryption failed.\n");
goto fail_decrypt;
}
if (out2.len != in.len) {
printf("Decrypted data length does not match original.\n");
- goto fail_chk;
+ goto fail_chk2;
}
if (memcmp(in.data, out2.data, in.len) != 0) {
printf("Decrypted data does not match original.\n");
- goto fail_chk;
+ goto fail_chk2;
}
- crypt_destroy_ctx(ctx);
freebuf(out2);
freebuf(out);
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
TEST_SUCCESS("(%s)", cipher);
return TEST_RC_SUCCESS;
- fail_chk:
+ fail_chk2:
freebuf(out2);
fail_decrypt:
+ fail_chk:
freebuf(out);
fail_encrypt:
- crypt_destroy_ctx(ctx);
+ crypt_destroy_ctx(rx);
+ fail_tx:
+ crypt_destroy_ctx(tx);
fail_init:
TEST_FAIL("(%s)", cipher);
return TEST_RC_FAIL;
@@ -155,6 +176,214 @@ static int test_encrypt_decrypt_all(void)
return ret;
}
+static int test_crypt_multi_packet(int nid)
+{
+ uint8_t pkt[TEST_PACKET_SIZE];
+ struct crypt_ctx * tx;
+ struct crypt_ctx * rx;
+ uint8_t key[SYMMKEYSZ];
+ struct crypt_sk sk_tx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
+ };
+ struct crypt_sk sk_rx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_RESP
+ };
+ buffer_t in;
+ buffer_t enc;
+ buffer_t dec;
+ const char * cipher;
+ int i;
+
+ sk_tx.nid = nid;
+ sk_rx.nid = nid;
+
+ cipher = crypt_nid_to_str(nid);
+ TEST_START("(%s)", cipher);
+
+ if (random_buffer(key, sizeof(key)) < 0) {
+ printf("Failed to generate random key.\n");
+ goto fail_init;
+ }
+
+ if (random_buffer(pkt, sizeof(pkt)) < 0) {
+ printf("Failed to generate random data.\n");
+ goto fail_init;
+ }
+
+ tx = crypt_create_ctx(&sk_tx);
+ if (tx == NULL) {
+ printf("Failed to create TX context.\n");
+ goto fail_init;
+ }
+
+ rx = crypt_create_ctx(&sk_rx);
+ if (rx == NULL) {
+ printf("Failed to create RX context.\n");
+ goto fail_tx;
+ }
+
+ in.len = sizeof(pkt);
+ in.data = pkt;
+
+ for (i = 0; i < TEST_N_PACKETS; i++) {
+ if (crypt_encrypt(tx, in, &enc) < 0) {
+ printf("Encryption failed at packet %d.\n", i);
+ goto fail_rx;
+ }
+
+ if (crypt_decrypt(rx, enc, &dec) < 0) {
+ printf("Decryption failed at packet %d.\n", i);
+ freebuf(enc);
+ goto fail_rx;
+ }
+
+ if (dec.len != in.len ||
+ memcmp(in.data, dec.data, in.len) != 0) {
+ printf("Data mismatch at packet %d.\n", i);
+ freebuf(dec);
+ freebuf(enc);
+ goto fail_rx;
+ }
+
+ freebuf(dec);
+ freebuf(enc);
+ }
+
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
+
+ TEST_SUCCESS("(%s)", cipher);
+
+ return TEST_RC_SUCCESS;
+ fail_rx:
+ crypt_destroy_ctx(rx);
+ fail_tx:
+ crypt_destroy_ctx(tx);
+ fail_init:
+ TEST_FAIL("(%s)", cipher);
+ return TEST_RC_FAIL;
+}
+
+static int test_multi_packet_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; crypt_supported_nids[i] != NID_undef; i++)
+ ret |= test_crypt_multi_packet(crypt_supported_nids[i]);
+
+ return ret;
+}
+
+static int test_crypt_aad_tamper(int nid)
+{
+ uint8_t pkt[TEST_PACKET_SIZE];
+ struct crypt_ctx * tx;
+ struct crypt_ctx * rx;
+ uint8_t key[SYMMKEYSZ];
+ struct crypt_sk sk_tx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
+ };
+ struct crypt_sk sk_rx = {
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_RESP
+ };
+ buffer_t in;
+ buffer_t enc;
+ buffer_t dec;
+ const char * cipher;
+
+ sk_tx.nid = nid;
+ sk_rx.nid = nid;
+
+ cipher = crypt_nid_to_str(nid);
+ TEST_START("(%s)", cipher);
+
+ if (random_buffer(key, sizeof(key)) < 0) {
+ printf("Failed to generate random key.\n");
+ goto fail_init;
+ }
+
+ if (random_buffer(pkt, sizeof(pkt)) < 0) {
+ printf("Failed to generate random data.\n");
+ goto fail_init;
+ }
+
+ tx = crypt_create_ctx(&sk_tx);
+ if (tx == NULL) {
+ printf("Failed to create TX context.\n");
+ goto fail_init;
+ }
+
+ rx = crypt_create_ctx(&sk_rx);
+ if (rx == NULL) {
+ printf("Failed to create RX context.\n");
+ goto fail_tx;
+ }
+
+ /* Only AEAD ciphers bind the selector as AAD. */
+ if (crypt_get_tagsz(tx) == 0) {
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
+
+ TEST_SUCCESS("(%s)", cipher);
+
+ return TEST_RC_SUCCESS;
+ }
+
+ in.len = sizeof(pkt);
+ in.data = pkt;
+
+ if (crypt_encrypt(tx, in, &enc) < 0) {
+ printf("Encryption failed.\n");
+ goto fail_rx;
+ }
+
+ /* Flip a seq byte: epoch/node stay valid so the AEAD tag rejects. */
+ enc.data[5] ^= 0x01;
+
+ if (crypt_decrypt(rx, enc, &dec) == 0) {
+ printf("Decryption accepted a tampered selector.\n");
+ freebuf(dec);
+ freebuf(enc);
+ goto fail_rx;
+ }
+
+ freebuf(enc);
+
+ crypt_destroy_ctx(rx);
+ crypt_destroy_ctx(tx);
+
+ TEST_SUCCESS("(%s)", cipher);
+
+ return TEST_RC_SUCCESS;
+ fail_rx:
+ crypt_destroy_ctx(rx);
+ fail_tx:
+ crypt_destroy_ctx(tx);
+ fail_init:
+ TEST_FAIL("(%s)", cipher);
+ return TEST_RC_FAIL;
+}
+
+static int test_aad_tamper_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; crypt_supported_nids[i] != NID_undef; i++)
+ ret |= test_crypt_aad_tamper(crypt_supported_nids[i]);
+
+ return ret;
+}
+
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
#include <openssl/obj_mac.h>
@@ -256,109 +485,17 @@ static int test_md_nid_values(void)
}
#endif
-static int test_key_rotation(void)
+static int test_crypt_headsz(void)
{
- uint8_t pkt[TEST_PACKET_SIZE];
- struct crypt_ctx * tx_ctx;
- struct crypt_ctx * rx_ctx;
- uint8_t key[SYMMKEYSZ];
- struct crypt_sk sk = {
- .nid = NID_aes_256_gcm,
- .key = key,
- .rot_bit = 7
- };
- buffer_t in;
- buffer_t enc;
- buffer_t dec;
- uint32_t i;
- uint32_t threshold;
-
- TEST_START();
-
- if (random_buffer(key, sizeof(key)) < 0) {
- printf("Failed to generate random key.\n");
- goto fail;
- }
-
- if (random_buffer(pkt, sizeof(pkt)) < 0) {
- printf("Failed to generate random data.\n");
- goto fail;
- }
-
- tx_ctx = crypt_create_ctx(&sk);
- if (tx_ctx == NULL) {
- printf("Failed to create TX context.\n");
- goto fail;
- }
-
- rx_ctx = crypt_create_ctx(&sk);
- if (rx_ctx == NULL) {
- printf("Failed to create RX context.\n");
- goto fail_tx;
- }
-
- in.len = sizeof(pkt);
- in.data = pkt;
-
- threshold = (1U << sk.rot_bit);
-
- /* Encrypt and decrypt across multiple rotations */
- for (i = 0; i < threshold * 3; i++) {
- if (crypt_encrypt(tx_ctx, in, &enc) < 0) {
- printf("Encryption failed at packet %u.\n", i);
- goto fail_rx;
- }
-
- if (crypt_decrypt(rx_ctx, enc, &dec) < 0) {
- printf("Decryption failed at packet %u.\n", i);
- freebuf(enc);
- goto fail_rx;
- }
-
- if (dec.len != in.len ||
- memcmp(in.data, dec.data, in.len) != 0) {
- printf("Data mismatch at packet %u.\n", i);
- freebuf(dec);
- freebuf(enc);
- goto fail_rx;
- }
-
- freebuf(dec);
- freebuf(enc);
- }
-
- crypt_destroy_ctx(rx_ctx);
- crypt_destroy_ctx(tx_ctx);
-
- TEST_SUCCESS();
-
- return TEST_RC_SUCCESS;
- fail_rx:
- crypt_destroy_ctx(rx_ctx);
- fail_tx:
- crypt_destroy_ctx(tx_ctx);
- fail:
- TEST_FAIL();
- return TEST_RC_FAIL;
-}
-
-static int test_key_phase_bit(void)
-{
- uint8_t pkt[TEST_PACKET_SIZE];
struct crypt_ctx * ctx;
uint8_t key[SYMMKEYSZ];
struct crypt_sk sk = {
- .nid = NID_aes_256_gcm,
- .key = key,
- .rot_bit = 7
+ .nid = NID_aes_256_gcm,
+ .key = key,
+ .epoch = 0,
+ .role = CRYPT_ROLE_INIT
};
- buffer_t in;
- buffer_t out;
- uint32_t count;
- uint32_t threshold;
- uint8_t phase_before;
- uint8_t phase_after;
- int ivsz;
+ int headsz;
TEST_START();
@@ -367,58 +504,15 @@ static int test_key_phase_bit(void)
goto fail;
}
- if (random_buffer(pkt, sizeof(pkt)) < 0) {
- printf("Failed to generate random data.\n");
- goto fail;
- }
-
ctx = crypt_create_ctx(&sk);
if (ctx == NULL) {
printf("Failed to initialize cryptography.\n");
goto fail;
}
- ivsz = crypt_get_ivsz(ctx);
- if (ivsz <= 0) {
- printf("Invalid IV size.\n");
- goto fail_ctx;
- }
-
- in.len = sizeof(pkt);
- in.data = pkt;
-
- /* Encrypt packets up to just before rotation threshold */
- threshold = (1U << sk.rot_bit);
-
- /* Encrypt threshold - 1 packets (indices 0 to threshold-2) */
- for (count = 0; count < threshold - 1; count++) {
- if (crypt_encrypt(ctx, in, &out) < 0) {
- printf("Encryption failed at count %u.\n", count);
- goto fail_ctx;
- }
- freebuf(out);
- }
-
- /* Packet at index threshold-1: phase should still be initial */
- if (crypt_encrypt(ctx, in, &out) < 0) {
- printf("Encryption failed before rotation.\n");
- goto fail_ctx;
- }
- phase_before = (out.data[0] & 0x80) ? 1 : 0;
- freebuf(out);
-
- /* Packet at index threshold: phase should have toggled */
- if (crypt_encrypt(ctx, in, &out) < 0) {
- printf("Encryption failed at rotation threshold.\n");
- goto fail_ctx;
- }
- phase_after = (out.data[0] & 0x80) ? 1 : 0;
- freebuf(out);
-
- /* Phase bit should have toggled */
- if (phase_before == phase_after) {
- printf("Phase bit did not toggle: before=%u, after=%u.\n",
- phase_before, phase_after);
+ headsz = crypt_get_headsz(ctx);
+ if (headsz != 6) {
+ printf("Unexpected header size: %d (expected 6).\n", headsz);
goto fail_ctx;
}
@@ -447,11 +541,13 @@ int crypt_test(int argc,
#ifdef HAVE_OPENSSL
ret |= test_cipher_nid_values();
ret |= test_md_nid_values();
- ret |= test_key_rotation();
- ret |= test_key_phase_bit();
+ ret |= test_multi_packet_all();
+ ret |= test_aad_tamper_all();
+ ret |= test_crypt_headsz();
#else
- (void) test_key_rotation;
- (void) test_key_phase_bit;
+ (void) test_multi_packet_all;
+ (void) test_aad_tamper_all;
+ (void) test_crypt_headsz;
return TEST_RC_SKIP;
#endif
diff --git a/src/lib/tests/keyrot_test.c b/src/lib/tests/keyrot_test.c
new file mode 100644
index 00000000..1c9f741b
--- /dev/null
+++ b/src/lib/tests/keyrot_test.c
@@ -0,0 +1,1083 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Test of the key-rotation schedule
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include "config.h"
+
+#include <test/test.h>
+
+#ifdef HAVE_OPENSSL
+#include <ouroboros/crypt.h>
+#include <ouroboros/pthread.h>
+
+#include "crypt/keyrot.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+static const uint8_t SEED_A[SYMMKEYSZ] = {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20
+};
+
+static int test_create_destroy(void)
+{
+ struct keyrot * kr;
+
+ TEST_START();
+
+ kr = keyrot_create(SEED_A, 0, 0);
+ if (kr == NULL)
+ goto fail;
+
+ keyrot_destroy(kr);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_epoch_range(void)
+{
+ struct keyrot * a;
+
+ TEST_START();
+
+ /* epoch is a 4-bit wire field; 16 and up must be refused. */
+ if (keyrot_create(SEED_A, 16, 0) != NULL)
+ goto fail;
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ if (keyrot_rekey(a, SEED_A, 16) == 0)
+ goto fail_a;
+
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tx_deterministic(void)
+{
+ struct keyrot * a;
+ struct keyrot * b;
+ uint8_t sela[KR_SELECTOR_LEN];
+ uint8_t selb[KR_SELECTOR_LEN];
+ uint8_t na[KR_NONCE_LEN];
+ uint8_t nb[KR_NONCE_LEN];
+ const uint8_t * ka;
+ const uint8_t * kb;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ b = keyrot_create(SEED_A, 0, 0);
+ if (b == NULL)
+ goto fail_a;
+
+ if (keyrot_tx_next(a, sela, &ka, na) != 0)
+ goto fail_b;
+
+ if (keyrot_tx_next(b, selb, &kb, nb) != 0)
+ goto fail_b;
+
+ if (memcmp(sela, selb, KR_SELECTOR_LEN) != 0)
+ goto fail_b;
+
+ if (memcmp(ka, kb, SYMMKEYSZ) != 0)
+ goto fail_b;
+
+ if (memcmp(na, nb, KR_NONCE_LEN) != 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_selector_layout(void)
+{
+ struct keyrot * a;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t nonce[KR_NONCE_LEN];
+ const uint8_t * k;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 3, 0);
+ if (a == NULL)
+ goto fail;
+
+ /* First packet: epoch 3, node 0, seq 0 */
+ if (keyrot_tx_next(a, sel, &k, nonce) != 0)
+ goto fail_a;
+
+ if ((sel[0] >> 4) != 3) /* epoch */
+ goto fail_a;
+
+ if ((((sel[0] & 0x0F) << 8) | sel[1]) != 0) /* node */
+ goto fail_a;
+
+ if (sel[2] != 0 || sel[3] != 0 || sel[4] != 0 || sel[5] != 0)
+ goto fail_a;
+
+ /* Second packet: seq advances to 1 */
+ if (keyrot_tx_next(a, sel, &k, nonce) != 0)
+ goto fail_a;
+
+ if (sel[5] != 1)
+ goto fail_a;
+
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_nodes_left_initial(void)
+{
+ struct keyrot * a;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ if (keyrot_tx_nodes_left(a) != KEY_NODE_COUNT)
+ goto fail_a;
+
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_roundtrip(void)
+{
+ struct keyrot * a; /* role 0 */
+ struct keyrot * b; /* role 1 */
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t ntx[KR_NONCE_LEN];
+ uint8_t nrx[KR_NONCE_LEN];
+ uint8_t ktx[SYMMKEYSZ];
+ const uint8_t * ptx;
+ const uint8_t * prx;
+ struct kr_rx rx;
+ int i;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail_a;
+
+ for (i = 0; i < 256; i++) {
+ if (keyrot_tx_next(a, sel, &ptx, ntx) != 0)
+ goto fail_b;
+ memcpy(ktx, ptx, SYMMKEYSZ);
+ if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0)
+ goto fail_b;
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+ if (memcmp(ktx, prx, SYMMKEYSZ) != 0)
+ goto fail_b;
+ if (memcmp(ntx, nrx, KR_NONCE_LEN) != 0)
+ goto fail_b;
+ }
+
+ keyrot_destroy(b);
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_direction_separation(void)
+{
+ struct keyrot * a; /* role 0 */
+ struct keyrot * b; /* role 1 */
+ uint8_t sela[KR_SELECTOR_LEN];
+ uint8_t selb[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ uint8_t ka[SYMMKEYSZ];
+ const uint8_t * pa;
+ const uint8_t * pb;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail_a;
+
+ if (keyrot_tx_next(a, sela, &pa, n) != 0)
+ goto fail_b;
+
+ memcpy(ka, pa, SYMMKEYSZ);
+ if (keyrot_tx_next(b, selb, &pb, n) != 0)
+ goto fail_b;
+
+ /* Same position, different role -> different leaf key */
+ if (memcmp(ka, pb, SYMMKEYSZ) == 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Build a selector by hand (test knows the wire format). */
+static void mk_sel(uint8_t epoch,
+ uint16_t node,
+ uint32_t seq,
+ uint8_t sel[KR_SELECTOR_LEN])
+{
+ sel[0] = (uint8_t) ((epoch << 4) | ((node >> 8) & 0x0F));
+ sel[1] = (uint8_t) (node & 0xFF);
+ sel[2] = (uint8_t) (seq >> 24);
+ sel[3] = (uint8_t) (seq >> 16);
+ sel[4] = (uint8_t) (seq >> 8);
+ sel[5] = (uint8_t) (seq);
+}
+
+static int test_random_access(void)
+{
+ struct keyrot * b;
+ uint8_t s0[KR_SELECTOR_LEN];
+ uint8_t s5[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ uint8_t k_first[SYMMKEYSZ];
+ uint8_t k_node5[SYMMKEYSZ];
+ const uint8_t * p;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ mk_sel(0, 0, 0, s0);
+ mk_sel(0, 5, 12345, s5); /* a far-ahead node, mid-span */
+
+ /* Jump straight to node 0 */
+ if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0)
+ goto fail_b;
+
+ memcpy(k_first, p, SYMMKEYSZ);
+
+ /* Jump forward to node 5 (simulates a burst skip) */
+ if (keyrot_rx_lookup(b, s5, &p, n, &rx) != 0)
+ goto fail_b;
+
+ memcpy(k_node5, p, SYMMKEYSZ);
+
+ /* Different nodes must yield different keys */
+ if (memcmp(k_first, k_node5, SYMMKEYSZ) == 0)
+ goto fail_b;
+
+ /* Jump back to node 0: still works, identical (no wedge) */
+ if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0)
+ goto fail_b;
+
+ if (memcmp(k_first, p, SYMMKEYSZ) != 0)
+ goto fail_b;
+
+ /* Out-of-range node must be rejected */
+ mk_sel(0, KEY_NODE_COUNT, 0, s0);
+ if (keyrot_rx_lookup(b, s0, &p, n, &rx) == 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static const uint8_t SEED_B[SYMMKEYSZ] = {
+ 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,
+ 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0,
+ 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8,
+ 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0
+};
+
+/*
+ * Look up and commit one within-node counter on epoch 0. Returns 0 on
+ * accept, 1 on a rejected commit (replay or too old), and -1 if the
+ * lookup itself failed - kept distinct so a reject assertion can never
+ * pass on an unrelated lookup miss.
+ */
+static int commit_ctr(struct keyrot * kr,
+ uint32_t ctr)
+{
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+
+ mk_sel(0, 0, ctr, sel);
+
+ if (keyrot_rx_lookup(kr, sel, &k, n, &rx) != 0)
+ return -1;
+
+ return keyrot_rx_commit(kr, &rx) == 0 ? 0 : 1;
+}
+
+static int test_replay_window(void)
+{
+ struct keyrot * b;
+ struct keyrot * c;
+ uint32_t base;
+ uint32_t jump;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ /* Fresh counters accepted; an immediate replay is rejected. */
+ if (commit_ctr(b, 100) != 0)
+ goto fail_b;
+
+ if (commit_ctr(b, 100) != 1)
+ goto fail_b;
+
+ /* In-window reorder: accepted once, rejected on replay. */
+ if (commit_ctr(b, 105) != 0)
+ goto fail_b;
+
+ if (commit_ctr(b, 102) != 0)
+ goto fail_b;
+
+ if (commit_ctr(b, 102) != 1)
+ goto fail_b;
+
+ /* Too-old boundary: the window edge is rejected, just inside is not. */
+ base = 4 * KEY_REPLAY_WINDOW;
+ if (commit_ctr(b, base) != 0)
+ goto fail_b;
+
+ if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64)) != 1)
+ goto fail_b;
+
+ if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64) + 1) != 0)
+ goto fail_b;
+
+ /*
+ * RFC 6479 slack-word regression: two low counters, then a
+ * forward jump of a full bitmap that aliases their slot, then a
+ * replay of a low counter. Without the reserved slack word this
+ * replay is wrongly accepted.
+ */
+ c = keyrot_create(SEED_A, 0, 1);
+ if (c == NULL)
+ goto fail_b;
+
+ if (commit_ctr(c, 70) != 0)
+ goto fail_c;
+
+ if (commit_ctr(c, 74) != 0)
+ goto fail_c;
+
+ jump = KEY_REPLAY_WINDOW + 63;
+ if (commit_ctr(c, jump) != 0)
+ goto fail_c;
+
+ if (commit_ctr(c, 74) != 1)
+ goto fail_c;
+
+ keyrot_destroy(c);
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_c:
+ keyrot_destroy(c);
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_lookup_no_commit(void)
+{
+ struct keyrot * b;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+ int i;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ mk_sel(0, 0, 100, sel);
+
+ /* Repeated lookups are pre-AEAD and must not consume the slot. */
+ for (i = 0; i < 4; i++) {
+ if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0)
+ goto fail_b;
+ }
+
+ /* The slot is still fresh, so the first commit accepts ... */
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ /* ... and only the commit advanced it, so the next is a replay. */
+ if (keyrot_rx_commit(b, &rx) == 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_commit_prev_batch(void)
+{
+ struct keyrot * b;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ /* Capture a packet under cur (epoch 0). */
+ mk_sel(0, 0, 7, sel);
+ if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0)
+ goto fail_b;
+
+ /* Re-key: the captured batch becomes prev and the flag clears. */
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ /* The straggler commits under prev without claiming a switch. */
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ if (keyrot_peer_switched(b))
+ goto fail_b;
+
+ /* prev still holds a replay window: its replay is rejected. */
+ if (keyrot_rx_commit(b, &rx) == 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_replay_forward_clear(void)
+{
+ struct keyrot * d;
+ uint32_t low;
+ uint32_t alias;
+ uint32_t jump;
+
+ TEST_START();
+
+ d = keyrot_create(SEED_A, 0, 1);
+ if (d == NULL)
+ goto fail;
+
+ /* alias shares low's slot a window away; the jump must clear it. */
+ low = 10;
+ alias = low + KEY_REPLAY_WINDOW;
+ jump = alias + KEY_REPLAY_WINDOW / 2;
+
+ if (commit_ctr(d, low) != 0)
+ goto fail_d;
+
+ if (commit_ctr(d, jump) != 0)
+ goto fail_d;
+
+ if (commit_ctr(d, alias) != 0)
+ goto fail_d;
+
+ if (commit_ctr(d, alias) != 1)
+ goto fail_d;
+
+ keyrot_destroy(d);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_d:
+ keyrot_destroy(d);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_rekey_overlap(void)
+{
+ struct keyrot * a; /* role 0 */
+ struct keyrot * b; /* role 1 */
+ uint8_t old_sel[KR_SELECTOR_LEN];
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t ntx[KR_NONCE_LEN];
+ uint8_t nrx[KR_NONCE_LEN];
+ uint8_t ktx[SYMMKEYSZ];
+ const uint8_t * ptx;
+ const uint8_t * prx;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail_a;
+
+ /* Send one gen-0 packet; keep its selector for the overlap. */
+ if (keyrot_tx_next(a, old_sel, &ptx, ntx) != 0)
+ goto fail_b;
+
+ memcpy(ktx, ptx, SYMMKEYSZ);
+ if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0)
+ goto fail_b;
+
+ if (memcmp(ktx, prx, SYMMKEYSZ) != 0)
+ goto fail_b;
+
+ /* Both ends re-key to epoch 1 with a fresh seed. */
+ if (keyrot_rekey(a, SEED_B, 1) != 0)
+ goto fail_b;
+
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ /* TX is gated until promotion; promote a to emit the new epoch. */
+ keyrot_tx_promote(a);
+
+ /* New gen-1 traffic works. */
+ if (keyrot_tx_next(a, sel, &ptx, ntx) != 0)
+ goto fail_b;
+
+ memcpy(ktx, ptx, SYMMKEYSZ);
+ if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0)
+ goto fail_b;
+
+ if (memcmp(ktx, prx, SYMMKEYSZ) != 0)
+ goto fail_b;
+
+ /* A straggling gen-0 packet still decrypts (overlap window). */
+ if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0)
+ goto fail_b;
+
+ /* An unknown epoch is rejected. */
+ mk_sel(7, 0, 0, sel);
+ if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) == 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_tx_gate(void)
+{
+ struct keyrot * a; /* role 0 */
+ struct keyrot * b; /* role 1 */
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * p;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail_a;
+
+ /* Both re-key to epoch 1; TX must stay on epoch 0 until promoted. */
+ if (keyrot_rekey(a, SEED_B, 1) != 0)
+ goto fail_b;
+
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ /* a's TX still stamps the old epoch (0). */
+ if (keyrot_tx_next(a, sel, &p, n) != 0)
+ goto fail_b;
+
+ if ((sel[0] >> 4) != 0)
+ goto fail_b;
+
+ /* b decrypts the old-epoch packet via its prev batch. */
+ if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0)
+ goto fail_b;
+
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ /* b has not yet seen the new epoch from a. */
+ if (keyrot_peer_switched(b))
+ goto fail_b;
+
+ /* a promotes; its TX now stamps the new epoch (1). */
+ keyrot_tx_promote(a);
+ if (keyrot_tx_next(a, sel, &p, n) != 0)
+ goto fail_b;
+
+ if ((sel[0] >> 4) != 1)
+ goto fail_b;
+
+ /* b sees the new epoch and reports the peer switched. */
+ if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0)
+ goto fail_b;
+
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ if (!keyrot_peer_switched(b))
+ goto fail_b;
+
+ keyrot_destroy(b);
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_peer_switched_commit_only(void)
+{
+ struct keyrot * b;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ /* A re-key clears the flag until a packet is seen on cur. */
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ if (keyrot_peer_switched(b))
+ goto fail_b;
+
+ mk_sel(1, 0, 0, sel);
+
+ /* Lookup is pre-AEAD: selecting a key must not flip the flag. */
+ if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0)
+ goto fail_b;
+
+ if (keyrot_peer_switched(b))
+ goto fail_b;
+
+ /* Commit runs post-AEAD and is what records the peer switched. */
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ if (!keyrot_peer_switched(b))
+ goto fail_b;
+
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_commit_evicted(void)
+{
+ struct keyrot * b;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 1);
+ if (b == NULL)
+ goto fail;
+
+ mk_sel(0, 0, 3, sel);
+ if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0)
+ goto fail_b;
+
+ /* Two re-keys drop the captured batch from both cur and prev. */
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ if (keyrot_rekey(b, SEED_A, 2) != 0)
+ goto fail_b;
+
+ /* Commit on an evicted batch is a silent no-op, not a fault. */
+ if (keyrot_rx_commit(b, &rx) != 0)
+ goto fail_b;
+
+ keyrot_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_b:
+ keyrot_destroy(b);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Concurrency: many TX threads + RX + re-key share one keyrot. The
+ * (epoch, counter) the TX side stamps must be globally unique (no AEAD
+ * nonce reuse). Capped below 16 re-keys so epoch maps 1:1 to a batch and
+ * the wire epoch never wraps (a wrapped epoch under a fresh key is not
+ * reuse but would false-trip the uniqueness check). Run under TSan to
+ * catch data races the static reviews can't.
+ */
+#define CT_THREADS 4
+#define CT_PKTS 2000
+#define CT_REKEYS 8
+
+struct ct_rec {
+ uint8_t epoch;
+ uint64_t ctr;
+};
+
+struct ct_arg {
+ struct keyrot * kr;
+ struct ct_rec * recs;
+ size_t n;
+};
+
+static void * ct_tx_thread(void * a)
+{
+ struct ct_arg * arg = a;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t nonce[KR_NONCE_LEN];
+ const uint8_t * k;
+ uint64_t ctr;
+ size_t i;
+ size_t j;
+
+ for (i = 0; i < CT_PKTS; i++) {
+ if (keyrot_tx_next(arg->kr, sel, &k, nonce) != 0)
+ continue;
+
+ ctr = 0;
+ for (j = 0; j < 8; j++)
+ ctr = (ctr << 8) | nonce[j];
+
+ arg->recs[arg->n].epoch = (uint8_t) (sel[0] >> 4);
+ arg->recs[arg->n].ctr = ctr;
+ arg->n++;
+ }
+
+ return NULL;
+}
+
+static void * ct_rx_thread(void * a)
+{
+ struct keyrot * kr = a;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t nonce[KR_NONCE_LEN];
+ const uint8_t * k;
+ struct kr_rx rx;
+ size_t i;
+
+ /* Exercise rx_lookup against re-key reclaim; results ignored. */
+ for (i = 0; i < CT_PKTS; i++) {
+ mk_sel((uint8_t) (i % 16), 0, (uint32_t) i, sel);
+ if (keyrot_rx_lookup(kr, sel, &k, nonce, &rx) == 0)
+ (void) keyrot_rx_commit(kr, &rx);
+ }
+
+ return NULL;
+}
+
+static void * ct_rekey_thread(void * a)
+{
+ struct keyrot * kr = a;
+ struct timespec t;
+ int e;
+
+ t.tv_sec = 0;
+ t.tv_nsec = 2 * 1000 * 1000; /* 2 ms */
+
+ for (e = 1; e <= CT_REKEYS; e++) {
+ nanosleep(&t, NULL);
+ if (keyrot_rekey(kr, (e & 1) ? SEED_B : SEED_A,
+ (uint8_t) e) != 0)
+ break;
+ keyrot_tx_promote(kr);
+ }
+
+ return NULL;
+}
+
+static int ct_cmp(const void * x,
+ const void * y)
+{
+ const struct ct_rec * a = x;
+ const struct ct_rec * b = y;
+
+ if (a->epoch != b->epoch)
+ return a->epoch < b->epoch ? -1 : 1;
+
+ if (a->ctr != b->ctr)
+ return a->ctr < b->ctr ? -1 : 1;
+
+ return 0;
+}
+
+static int test_concurrent_nonce_unique(void)
+{
+ struct keyrot * kr;
+ struct ct_arg arg[CT_THREADS];
+ pthread_t tx[CT_THREADS];
+ pthread_t rx;
+ pthread_t rk;
+ struct ct_rec * all;
+ size_t total;
+ size_t i;
+ bool reuse = false;
+
+ TEST_START();
+
+ kr = keyrot_create(SEED_A, 0, 0);
+ if (kr == NULL)
+ goto fail;
+
+ all = malloc(sizeof(*all) * CT_THREADS * CT_PKTS);
+ if (all == NULL)
+ goto fail_kr;
+
+ for (i = 0; i < CT_THREADS; i++) {
+ arg[i].kr = kr;
+ arg[i].n = 0;
+ arg[i].recs = all + i * CT_PKTS;
+ }
+
+ for (i = 0; i < CT_THREADS; i++)
+ pthread_create(&tx[i], NULL, ct_tx_thread, &arg[i]);
+
+ pthread_create(&rx, NULL, ct_rx_thread, kr);
+ pthread_create(&rk, NULL, ct_rekey_thread, kr);
+
+ for (i = 0; i < CT_THREADS; i++)
+ pthread_join(tx[i], NULL);
+
+ pthread_join(rx, NULL);
+ pthread_join(rk, NULL);
+
+ total = 0;
+ for (i = 0; i < CT_THREADS; i++) {
+ memmove(all + total, all + i * CT_PKTS,
+ arg[i].n * sizeof(*all));
+ total += arg[i].n;
+ }
+
+ qsort(all, total, sizeof(*all), ct_cmp);
+
+ for (i = 1; i < total; i++)
+ if (ct_cmp(&all[i - 1], &all[i]) == 0) {
+ printf("(epoch %u, ctr %llu) reused\n",
+ all[i].epoch,
+ (unsigned long long) all[i].ctr);
+ reuse = true;
+ break;
+ }
+
+ free(all);
+
+ if (reuse)
+ goto fail_kr;
+
+ keyrot_destroy(kr);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_kr:
+ keyrot_destroy(kr);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+#endif /* HAVE_OPENSSL */
+
+int keyrot_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#ifdef HAVE_OPENSSL
+ ret |= test_create_destroy();
+ ret |= test_epoch_range();
+ ret |= test_tx_deterministic();
+ ret |= test_selector_layout();
+ ret |= test_nodes_left_initial();
+ ret |= test_roundtrip();
+ ret |= test_direction_separation();
+ ret |= test_random_access();
+ ret |= test_peer_switched_commit_only();
+ ret |= test_commit_evicted();
+ ret |= test_replay_window();
+ ret |= test_lookup_no_commit();
+ ret |= test_commit_prev_batch();
+ ret |= test_replay_forward_clear();
+ ret |= test_rekey_overlap();
+ ret |= test_tx_gate();
+ ret |= test_concurrent_nonce_unique();
+#endif
+ return ret;
+}