summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/config/ipcp/eth.cmake4
-rw-r--r--cmake/config/lib.cmake4
-rw-r--r--cmake/dependencies/crypt/openssl.cmake51
-rw-r--r--src/ipcpd/eth/eth.c27
-rw-r--r--src/ipcpd/unicast/dir/dht.c53
-rw-r--r--src/irmd/config.h.in3
-rw-r--r--src/irmd/oap/cli.c6
-rw-r--r--src/irmd/oap/hdr.c18
-rw-r--r--src/irmd/oap/hdr.h6
-rw-r--r--src/irmd/oap/io.c2
-rw-r--r--src/irmd/oap/srv.c6
-rw-r--r--src/irmd/oap/tests/common.c128
-rw-r--r--src/irmd/oap/tests/common.h15
-rw-r--r--src/irmd/oap/tests/oap_test.c421
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c324
-rw-r--r--src/lib/config.h.in6
-rw-r--r--src/lib/crypt.c10
-rw-r--r--src/lib/crypt/keyrot.c34
-rw-r--r--src/lib/dev.c107
-rw-r--r--src/lib/ssm/rbuff.c19
-rw-r--r--src/lib/tests/auth_test.c5
-rw-r--r--src/lib/tests/auth_test_ml_dsa.c2
-rw-r--r--src/lib/tests/auth_test_slh_dsa.c2
-rw-r--r--src/lib/tests/crypt_test.c45
-rw-r--r--src/lib/tests/kex_test.c60
-rw-r--r--src/lib/tests/kex_test_ml_kem.c2
-rw-r--r--src/lib/tests/keyrot_test.c175
27 files changed, 1187 insertions, 348 deletions
diff --git a/cmake/config/ipcp/eth.cmake b/cmake/config/ipcp/eth.cmake
index d336d647..6a044d4b 100644
--- a/cmake/config/ipcp/eth.cmake
+++ b/cmake/config/ipcp/eth.cmake
@@ -10,8 +10,8 @@ set(IPCP_ETH_WR_THR 1 CACHE STRING
"Number of writer threads in Ethernet IPCP")
set(IPCP_ETH_QDISC_BYPASS false CACHE BOOL
"Bypass the Qdisc in the kernel when using raw sockets")
-set(IPCP_ETH_SNDBUF 0 CACHE STRING
- "Raw socket SO_SNDBUF in bytes; 0 = leave kernel default (wmem_default)")
+set(IPCP_ETH_SNDBUF 4096 CACHE STRING
+ "Raw socket SO_SNDBUF in bytes (floored to one frame); 0 = kernel default")
set(IPCP_ETH_RCVBUF 0 CACHE STRING
"Raw socket SO_RCVBUF in bytes; 0 = leave kernel default (rmem_default)")
set(IPCP_ETH_LO_MTU 9000 CACHE STRING
diff --git a/cmake/config/lib.cmake b/cmake/config/lib.cmake
index 2c01b311..81a7d6ba 100644
--- a/cmake/config/lib.cmake
+++ b/cmake/config/lib.cmake
@@ -97,6 +97,8 @@ 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")
+set(KEY_REKEY_WM_CHECK_BITS 16 CACHE STRING
+ "Re-key watermark is consulted once per 2^n flow writes")
if(NOT KEY_REPLAY_WINDOW MATCHES "^[0-9]+$")
message(FATAL_ERROR "KEY_REPLAY_WINDOW must be a positive integer")
endif()
@@ -111,7 +113,7 @@ endif()
# 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_wm_check "1 << ${KEY_REKEY_WM_CHECK_BITS}")
math(EXPR _rk_lead
"${KEY_REKEY_WATERMARK} << (${KEY_LEAF_BITS} + ${KEY_NODE_BITS})")
math(EXPR _rk_min "2 * ${_rk_wm_check}")
diff --git a/cmake/dependencies/crypt/openssl.cmake b/cmake/dependencies/crypt/openssl.cmake
index 38eb826f..b0fa8d11 100644
--- a/cmake/dependencies/crypt/openssl.cmake
+++ b/cmake/dependencies/crypt/openssl.cmake
@@ -25,49 +25,32 @@ message(STATUS "OpenSSL support enabled, found version ${OPENSSL_VERSION}")
set(HAVE_OPENSSL TRUE CACHE INTERNAL
"OpenSSL cryptography support available")
-if(OPENSSL_VERSION VERSION_GREATER_EQUAL "3.4.0")
- set(DISABLE_ML_KEM FALSE CACHE BOOL
- "Disable ML-KEM support")
- set(DISABLE_ML_DSA FALSE CACHE BOOL
- "Disable ML-DSA support")
- if(NOT DISABLE_ML_KEM)
- set(HAVE_OPENSSL_ML_KEM TRUE CACHE INTERNAL
- "OpenSSL ML-KEM available")
- message(STATUS "OpenSSL ML-KEM support enabled")
- else()
- message(STATUS "OpenSSL ML-KEM support disabled")
- unset(HAVE_OPENSSL_ML_KEM CACHE)
- endif()
- if(NOT DISABLE_ML_DSA)
- set(HAVE_OPENSSL_ML_DSA TRUE CACHE INTERNAL
- "OpenSSL ML-DSA available")
- message(STATUS "OpenSSL ML-DSA support enabled")
- else()
- message(STATUS "OpenSSL ML-DSA support disabled")
- unset(HAVE_OPENSSL_ML_DSA CACHE)
- endif()
-else()
- message(STATUS
- "Install OpenSSL >= 3.4.0 for ML-KEM/ML-DSA")
- unset(HAVE_OPENSSL_ML_KEM CACHE)
- unset(HAVE_OPENSSL_ML_DSA CACHE)
-endif()
-
if(OPENSSL_VERSION VERSION_GREATER_EQUAL "3.5.0")
- set(DISABLE_SLH_DSA FALSE CACHE BOOL
+ set(DISABLE_ML FALSE CACHE BOOL
+ "Disable ML-KEM/ML-DSA support")
+ set(DISABLE_SLH FALSE CACHE BOOL
"Disable SLH-DSA support")
- if(NOT DISABLE_SLH_DSA)
- set(HAVE_OPENSSL_SLH_DSA TRUE CACHE INTERNAL
+ if(NOT DISABLE_ML)
+ set(HAVE_ML TRUE CACHE INTERNAL
+ "OpenSSL ML-KEM/ML-DSA available")
+ message(STATUS "OpenSSL ML-KEM/ML-DSA support enabled")
+ else()
+ message(STATUS "OpenSSL ML-KEM/ML-DSA support disabled")
+ unset(HAVE_ML CACHE)
+ endif()
+ if(NOT DISABLE_SLH)
+ set(HAVE_SLH TRUE CACHE INTERNAL
"OpenSSL SLH-DSA available")
message(STATUS "OpenSSL SLH-DSA support enabled")
else()
message(STATUS "OpenSSL SLH-DSA support disabled")
- unset(HAVE_OPENSSL_SLH_DSA CACHE)
+ unset(HAVE_SLH CACHE)
endif()
else()
message(STATUS
- "Install OpenSSL >= 3.5.0 for SLH-DSA")
- unset(HAVE_OPENSSL_SLH_DSA CACHE)
+ "Install OpenSSL >= 3.5.0 for PQC (ML-KEM/ML-DSA/SLH-DSA)")
+ unset(HAVE_ML CACHE)
+ unset(HAVE_SLH CACHE)
endif()
# Secure memory options are in cmake/config/global.cmake
diff --git a/src/ipcpd/eth/eth.c b/src/ipcpd/eth/eth.c
index 7e038a03..2d3bd249 100644
--- a/src/ipcpd/eth/eth.c
+++ b/src/ipcpd/eth/eth.c
@@ -83,6 +83,7 @@
#include <linux/if_ether.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
+#include <linux/sockios.h>
#endif
#ifdef __FreeBSD__
@@ -437,6 +438,7 @@ static int eth_rib_read(const char * path,
int queued = 0;
socklen_t optlen = sizeof(rcvbuf);
# if defined(__linux__)
+ int outq = 0;
struct tpacket_stats tp_stats;
socklen_t tp_len = sizeof(tp_stats);
# endif
@@ -448,6 +450,7 @@ static int eth_rib_read(const char * path,
SO_SNDBUF, &sndbuf, &optlen);
ioctl(eth_data.s_fd, FIONREAD, &queued);
# if defined(__linux__)
+ ioctl(eth_data.s_fd, SIOCOUTQ, &outq);
if (getsockopt(eth_data.s_fd, SOL_PACKET,
PACKET_STATISTICS,
&tp_stats, &tp_len) == 0) {
@@ -483,12 +486,14 @@ static int eth_rib_read(const char * path,
n += sprintf(buf + n,
"Socket rcvbuf (bytes): %20d\n"
"Socket sndbuf (bytes): %20d\n"
- "Socket queued (bytes): %20d\n",
+ "Socket ingress (bytes): %20d\n",
rcvbuf, sndbuf, queued);
# if defined(__linux__)
n += sprintf(buf + n,
+ "Socket egress (bytes): %20d\n"
"Kernel frames received: %20zu\n"
"Kernel frames dropped: %20zu\n",
+ outq,
LOAD_RELAXED(&eth_data.stat.kern_rcv),
LOAD_RELAXED(&eth_data.stat.kern_drp));
# endif
@@ -576,6 +581,7 @@ static int eth_rib_readdir(char *** buf)
fail_entry:
while (idx-- > 0)
free((*buf)[idx]);
+
free(*buf);
fail_entries:
pthread_rwlock_unlock(&eth_data.flows_lock);
@@ -1942,15 +1948,22 @@ static int eth_init_raw_socket(struct ifreq * ifr)
#endif
sndbuf = IPCP_ETH_SNDBUF;
- if (sndbuf > 0 && setsockopt(eth_data.s_fd, SOL_SOCKET, SO_SNDBUF,
- &sndbuf, sizeof(sndbuf))) {
- log_info("Failed to set SO_SNDBUF to %d.", sndbuf);
+ if (sndbuf > 0) {
+ /* Never below one frame or sendto() cannot fit an SDU. */
+ sndbuf = MAX(sndbuf, (int) (ETH_HEADER_TOT_SIZE + ETH_MTU));
+ if (setsockopt(eth_data.s_fd, SOL_SOCKET, SO_SNDBUF,
+ &sndbuf, sizeof(sndbuf)))
+ log_info("Failed to set SO_SNDBUF to %d.", sndbuf);
}
rcvbuf = IPCP_ETH_RCVBUF;
- if (rcvbuf > 0 && setsockopt(eth_data.s_fd, SOL_SOCKET, SO_RCVBUF,
- &rcvbuf, sizeof(rcvbuf))) {
- log_info("Failed to set SO_RCVBUF to %d.", rcvbuf);
+ if (rcvbuf > 0) {
+ /* SO_RCVBUFFORCE bypasses rmem_max; SO_RCVBUF is fallback. */
+ if (setsockopt(eth_data.s_fd, SOL_SOCKET, SO_RCVBUFFORCE,
+ &rcvbuf, sizeof(rcvbuf))
+ && setsockopt(eth_data.s_fd, SOL_SOCKET, SO_RCVBUF,
+ &rcvbuf, sizeof(rcvbuf)))
+ log_info("Failed to set SO_RCVBUF to %d.", rcvbuf);
}
if (bind(eth_data.s_fd, (struct sockaddr *) &eth_data.device,
diff --git a/src/ipcpd/unicast/dir/dht.c b/src/ipcpd/unicast/dir/dht.c
index 8eeea800..753d9a73 100644
--- a/src/ipcpd/unicast/dir/dht.c
+++ b/src/ipcpd/unicast/dir/dht.c
@@ -1597,6 +1597,7 @@ static ssize_t dht_kv_get_contacts(const uint8_t * key,
fail_contact:
while (i-- > 0)
dht_contact_msg__free_unpacked((*msgs)[i], NULL);
+
free(*msgs);
*msgs = NULL;
fail_msgs:
@@ -1763,6 +1764,7 @@ static int split_bucket(struct bucket * b)
fail_child:
while (i-- > 0)
bucket_destroy(b->children[i]);
+
return -1;
}
@@ -2849,6 +2851,7 @@ static dht_msg_t * do_dht_kv_find_node_req(const dht_find_req_msg_t * req)
fail_msg:
while (len-- > 0)
dht_contact_msg__free_unpacked(contacts[len], NULL);
+
free(contacts);
fail_contacts:
return NULL;
@@ -2953,6 +2956,7 @@ static dht_msg_t * do_dht_kv_find_value_req(const dht_find_req_msg_t * req)
fail_vals:
while (n_contacts-- > 0)
dht_contact_msg__free_unpacked(contacts[n_contacts], NULL);
+
free(contacts);
fail_contacts:
return NULL;
@@ -3312,6 +3316,42 @@ static int emergency_peer(struct list_head * pl)
return -ENOMEM;
}
+static bool __dht_kv_bucket_has_addr(struct bucket * b,
+ uint64_t addr)
+{
+ struct list_head * p;
+ size_t i;
+
+ assert(b != NULL);
+
+ if (*b->children != NULL)
+ for (i = 0; i < (1L << DHT_BETA); ++i)
+ if (__dht_kv_bucket_has_addr(b->children[i], addr))
+ return true;
+
+ llist_for_each(p, &b->contacts) {
+ struct contact * c;
+ c = list_entry(p, struct contact, next);
+ if (c->addr == addr)
+ return true;
+ }
+
+ return false;
+}
+
+static bool dht_kv_knows_peer(void)
+{
+ bool found;
+
+ pthread_rwlock_rdlock(&dht.db.lock);
+
+ found = __dht_kv_bucket_has_addr(dht.db.contacts.root, dht.peer);
+
+ pthread_rwlock_unlock(&dht.db.lock);
+
+ return found;
+}
+
static int dht_kv_seed_bootstrap_peer(void)
{
struct list_head pl;
@@ -3323,6 +3363,9 @@ static int dht_kv_seed_bootstrap_peer(void)
return 0;
}
+ if (dht_kv_knows_peer())
+ return 0;
+
if (emergency_peer(&pl) < 0) {
log_err("Could not create emergency peer.");
goto fail_peer;
@@ -3338,7 +3381,8 @@ static int dht_kv_seed_bootstrap_peer(void)
peer_list_destroy(&pl);
- return 0;
+ /* Sent, but not bootstrapped until the peer is in the DHT. */
+ return -EAGAIN;
fail_query:
peer_list_destroy(&pl);
fail_peer:
@@ -3427,6 +3471,8 @@ static void value_list_destroy(struct list_head * vl)
#define MUST_REPLICATE(v, now) ((now)->tv_sec > (v)->t_repl + dht.t_repl)
#define MUST_REPUBLISH(v, now) /* Close to expiry deadline */ \
(((v)->t_exp - (now)->tv_sec) < (DHT_N_REPUB * dht.t_repl))
+/* A local value must be (re)stored if near expiry or never stored. */
+#define MUST_STORE_LVAL(v, now) (MUST_REPUBLISH(v, now) || (v)->t_repl == 0)
static void dht_entry_get_repl_lists(const struct dht_entry * e,
struct list_head * repl,
struct list_head * rebl,
@@ -3448,7 +3494,7 @@ static void dht_entry_get_repl_lists(const struct dht_entry * e,
llist_for_each(p, &e->lvals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
- if (MUST_REPLICATE(v, now) && MUST_REPUBLISH(v, now)) {
+ if (MUST_REPLICATE(v, now) && MUST_STORE_LVAL(v, now)) {
/* Add expire time here, to allow creating val_entry */
n = val_entry_create(v->val, now->tv_sec + dht.t_exp);
if (n == NULL)
@@ -3738,6 +3784,9 @@ static void * work(void * o)
log_dbg("DHT worker starting %ld seconds interval.", intv * n);
+ /* Flush names registered before we had peers to store them. */
+ dht_kv_replicate();
+
while (true) {
int i = 0;
while (tasks[i] != NULL) {
diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in
index e14cff75..53f80ca4 100644
--- a/src/irmd/config.h.in
+++ b/src/irmd/config.h.in
@@ -82,8 +82,7 @@
#cmakedefine HAVE_LIBGCRYPT
#cmakedefine HAVE_OPENSSL
#ifdef HAVE_OPENSSL
-#cmakedefine HAVE_OPENSSL_ML_KEM
-#cmakedefine HAVE_OPENSSL_ML_DSA
+#cmakedefine HAVE_ML
#endif
#define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@
#ifdef CONFIG_OUROBOROS_DEBUG
diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c
index 689d67ca..3518b4d1 100644
--- a/src/irmd/oap/cli.c
+++ b/src/irmd/oap/cli.c
@@ -250,6 +250,7 @@ int oap_cli_prepare(void ** ctx,
void * crt = NULL;
buffer_t no_tag = BUF_INIT;
ssize_t ret;
+ int enc_flags = 0;
assert(ctx != NULL);
assert(info != NULL);
@@ -302,6 +303,9 @@ int oap_cli_prepare(void ** ctx,
crt = NULL;
}
+ if (rekey)
+ enc_flags = OAP_ENC_REKEY;
+
oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->scfg.c.nid);
if (do_client_kex_prepare(info->name, s) < 0) {
@@ -310,7 +314,7 @@ int oap_cli_prepare(void ** ctx,
}
if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->scfg,
- no_tag, NID_undef, NULL)) {
+ no_tag, NID_undef, NULL, enc_flags)) {
log_err_id(s->id.data, "Failed to create OAP request header.");
goto fail_hdr;
}
diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c
index f8400b46..6f355133 100644
--- a/src/irmd/oap/hdr.c
+++ b/src/irmd/oap/hdr.c
@@ -397,7 +397,8 @@ static int oap_hdr_encode_sealed(struct oap_hdr * hdr,
struct sec_config * scfg,
buffer_t rsp_tag,
int req_md_nid,
- const uint8_t * seal_key)
+ const uint8_t * seal_key,
+ int flags)
{
struct timespec now;
uint64_t stamp;
@@ -449,7 +450,8 @@ static int oap_hdr_encode_sealed(struct oap_hdr * hdr,
free(prefix.data);
freebuf(der);
- if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, false) < 0)
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid,
+ flags & OAP_ENC_REKEY) < 0)
goto fail_decode;
return 0;
@@ -472,7 +474,8 @@ int oap_hdr_encode(struct oap_hdr * hdr,
struct sec_config * scfg,
buffer_t rsp_tag,
int req_md_nid,
- const uint8_t * seal_key)
+ const uint8_t * seal_key,
+ int flags)
{
struct timespec now;
uint64_t stamp;
@@ -488,7 +491,7 @@ int oap_hdr_encode(struct oap_hdr * hdr,
if (seal_key != NULL)
return oap_hdr_encode_sealed(hdr, pkp, crt, scfg, rsp_tag,
- req_md_nid, seal_key);
+ req_md_nid, seal_key, flags);
clock_gettime(CLOCK_REALTIME, &now);
stamp = hton64(TS_TO_UINT64(now));
@@ -550,10 +553,13 @@ int oap_hdr_encode(struct oap_hdr * hdr,
goto fail_realloc;
memcpy(hdr->hdr.data + offset, sig.data, sig.len);
- clrbuf(out);
}
- if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, false) < 0)
+ /* Ownership moved to hdr->hdr; drop the alias to avoid double-free. */
+ clrbuf(out);
+
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid,
+ flags & OAP_ENC_REKEY) < 0)
goto fail_decode;
freebuf(der);
diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h
index 1a599727..66fbac9a 100644
--- a/src/irmd/oap/hdr.h
+++ b/src/irmd/oap/hdr.h
@@ -168,13 +168,17 @@ void oap_hdr_init(struct oap_hdr * hdr,
void oap_hdr_fini(struct oap_hdr * oap_hdr);
+/* oap_hdr_encode option flags */
+#define OAP_ENC_REKEY (1U << 0) /* signed, cert-less re-key packet */
+
int oap_hdr_encode(struct oap_hdr * hdr,
void * pkp,
void * crt,
struct sec_config * scfg,
buffer_t rsp_tag,
int req_md_nid,
- const uint8_t * seal_key);
+ const uint8_t * seal_key,
+ int flags);
int oap_hdr_decode(struct oap_hdr * hdr,
buffer_t buf,
diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c
index b5daa432..845723fa 100644
--- a/src/irmd/oap/io.c
+++ b/src/irmd/oap/io.c
@@ -133,7 +133,7 @@ int load_sec_config(const char * name,
log_info("Key exchange not configured for %s.", name);
return 0;
}
-#ifndef HAVE_OPENSSL_ML_KEM
+#ifndef HAVE_ML
if (IS_KEM_ALGORITHM(cfg->x.str)) {
log_err("PQC not available, can't use %s for %s.",
cfg->x.str, name);
diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c
index 5d631618..cc3dec5b 100644
--- a/src/irmd/oap/srv.c
+++ b/src/irmd/oap/srv.c
@@ -400,6 +400,7 @@ int oap_srv_process(const struct name_info * info,
void * pkp = NULL;
void * crt = NULL;
int req_md_nid;
+ int enc_flags = 0;
int ret;
assert(info != NULL);
@@ -426,6 +427,9 @@ int oap_srv_process(const struct name_info * info,
crt = NULL;
}
+ if (rekey)
+ enc_flags = OAP_ENC_REKEY;
+
if (load_srv_sec_config(info, &scfg) < 0) {
log_err("Failed to load security config for %s.", info->name);
goto fail_kex;
@@ -535,7 +539,7 @@ int oap_srv_process(const struct name_info * info,
}
ret = oap_hdr_encode(&local_hdr, pkp, crt, &scfg,
- rsp_tag, req_md_nid, seal_key);
+ rsp_tag, req_md_nid, seal_key, enc_flags);
crypt_secure_clear(hs_key, SYMMKEYSZ);
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c
index 49ea9187..16d52c63 100644
--- a/src/irmd/oap/tests/common.c
+++ b/src/irmd/oap/tests/common.c
@@ -37,8 +37,6 @@ int load_srv_sec_config(const struct name_info * info,
memset(cfg, 0, sizeof(*cfg));
cfg->a.req = test_cfg.srv.req_auth;
- if (test_cfg.srv.cacert != NULL)
- strcpy(cfg->a.cacert, test_cfg.srv.cacert);
/* Digest is kept without kex, as in parse_sec_config */
SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md);
@@ -62,8 +60,6 @@ int load_cli_sec_config(const struct name_info * info,
memset(cfg, 0, sizeof(*cfg));
cfg->a.req = test_cfg.cli.req_auth;
- if (test_cfg.cli.cacert != NULL)
- strcpy(cfg->a.cacert, test_cfg.cli.cacert);
/* Digest is kept without kex, as in parse_sec_config */
SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md);
@@ -261,17 +257,35 @@ int roundtrip_auth_only(const char * root_ca,
return TEST_RC_FAIL;
}
+static const char * rekey_mode(bool srv_auth,
+ bool cli_auth)
+{
+ if (srv_auth && cli_auth)
+ return "mutual";
+
+ if (srv_auth)
+ return "srv-only";
+
+ if (cli_auth)
+ return "cli-only";
+
+ return "none";
+}
+
int roundtrip_rekey(const char * root_ca,
- const char * im_ca_str)
+ const char * im_ca_str,
+ bool srv_auth,
+ bool cli_auth)
{
struct oap_test_ctx ctx;
+ uint8_t key0[SYMMKEYSZ];
+ const char * mode = rekey_mode(srv_auth, cli_auth);
- TEST_START();
+ TEST_START("(%s)", mode);
if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
goto fail;
- /* Initial handshake: the client caches the server cert. */
if (oap_cli_prepare_ctx(&ctx) < 0) {
printf("Initial client prepare failed.\n");
goto fail_cleanup;
@@ -292,11 +306,20 @@ int roundtrip_rekey(const char * root_ca,
goto fail_cleanup;
}
- if (ctx.cli_crt.len == 0) {
+ /* The client caches the server cert only if the server authed. */
+ if (srv_auth && ctx.cli_crt.len == 0) {
printf("Server cert was not cached for re-key.\n");
goto fail_cleanup;
}
+ /* The server caches the client cert only if the client authed. */
+ if (cli_auth && ctx.srv_crt.len == 0) {
+ printf("Client cert was not cached by the server.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(key0, ctx.cli.key, SYMMKEYSZ);
+
/* Re-key: cert dropped on the wire, verified against the cache. */
freebuf(ctx.req_hdr);
freebuf(ctx.resp_hdr);
@@ -324,24 +347,36 @@ int roundtrip_rekey(const char * root_ca,
goto fail_cleanup;
}
+ if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) {
+ printf("Re-key did not produce a fresh key.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) {
+ printf("Cipher not set after re-key.\n");
+ goto fail_cleanup;
+ }
+
oap_test_teardown(&ctx);
- TEST_SUCCESS();
+ TEST_SUCCESS("(%s)", mode);
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
fail:
- TEST_FAIL();
+ TEST_FAIL("(%s)", mode);
return TEST_RC_FAIL;
}
int roundtrip_rekey_badcache(const char * root_ca,
- const char * im_ca_str)
+ const char * im_ca_str,
+ bool cli_auth)
{
struct oap_test_ctx ctx;
+ const char * mode = rekey_mode(true, cli_auth);
- TEST_START();
+ TEST_START("(%s)", mode);
if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
goto fail;
@@ -366,7 +401,7 @@ int roundtrip_rekey_badcache(const char * root_ca,
goto fail_cleanup;
}
- /* Corrupt the cached cert: the re-key must fail closed. */
+ /* Corrupt the client's cached server cert: re-key must fail closed. */
ctx.cli_crt.data[ctx.cli_crt.len / 2] ^= 0xFF;
freebuf(ctx.req_hdr);
@@ -392,13 +427,76 @@ int roundtrip_rekey_badcache(const char * root_ca,
oap_test_teardown(&ctx);
- TEST_SUCCESS();
+ TEST_SUCCESS("(%s)", mode);
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
fail:
- TEST_FAIL();
+ TEST_FAIL("(%s)", mode);
+ return TEST_RC_FAIL;
+}
+
+int roundtrip_rekey_srv_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth)
+{
+ struct oap_test_ctx ctx;
+ const char * mode = rekey_mode(srv_auth, true);
+
+ TEST_START("(%s)", mode);
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.srv_crt.len == 0) {
+ printf("Client cert was not cached by the server.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the server's cached client cert: re-key must fail closed. */
+ ctx.srv_crt.data[ctx.srv_crt.len / 2] ^= 0xFF;
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server accepted a corrupted cached client cert.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", mode);
+
+ return TEST_RC_SUCCESS;
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", mode);
return TEST_RC_FAIL;
}
diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h
index c47096fb..7aead07a 100644
--- a/src/irmd/oap/tests/common.h
+++ b/src/irmd/oap/tests/common.h
@@ -30,6 +30,9 @@
#include <stdbool.h>
+#define AUTH true
+#define NO_AUTH false
+
/* Per-side security configuration for tests */
struct test_sec_cfg {
int kex; /* KEX algorithm NID */
@@ -39,7 +42,6 @@ struct test_sec_cfg {
int kem_mode; /* KEM encapsulation mode (0 for ECDH) */
bool auth; /* Use authentication (certificates) */
bool req_auth; /* Require peer authentication */
- const char * cacert; /* Pinned issuing CA path */
};
/* Test configuration - set by each test before running roundtrip */
@@ -94,10 +96,17 @@ int roundtrip_auth_only(const char * root_ca,
const char * im_ca_str);
int roundtrip_rekey(const char * root_ca,
- const char * im_ca_str);
+ const char * im_ca_str,
+ bool srv_auth,
+ bool cli_auth);
int roundtrip_rekey_badcache(const char * root_ca,
- const char * im_ca_str);
+ const char * im_ca_str,
+ bool cli_auth);
+
+int roundtrip_rekey_srv_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth);
int roundtrip_kex_only(void);
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
index fc10150b..145b68c7 100644
--- a/src/irmd/oap/tests/oap_test.c
+++ b/src/irmd/oap/tests/oap_test.c
@@ -54,9 +54,6 @@
#include <openssl/evp.h>
#endif
-#define AUTH true
-#define NO_AUTH false
-
extern const uint16_t kex_supported_nids[];
extern const uint16_t md_supported_nids[];
@@ -117,6 +114,13 @@ static void test_default_cfg(void)
test_cfg.cli.auth = NO_AUTH;
}
+/* Encrypted, unauthenticated on both sides. */
+static void test_enc_noauth_cfg(void)
+{
+ test_default_cfg();
+ test_cfg.srv.auth = NO_AUTH;
+}
+
static int test_oap_auth_init_fini(void)
{
TEST_START();
@@ -203,37 +207,71 @@ static int test_oap_roundtrip_auth_only(void)
return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec);
}
-static int test_oap_rekey(void)
+static int test_oap_rekey(bool srv_auth,
+ bool cli_auth)
{
test_default_cfg();
+ test_cfg.srv.auth = srv_auth;
+ test_cfg.cli.auth = cli_auth;
+
+ return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec,
+ srv_auth, cli_auth);
+}
+
+static int test_oap_rekey_all(void)
+{
+ int ret = 0;
- return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec);
+ ret |= test_oap_rekey(AUTH, NO_AUTH);
+ ret |= test_oap_rekey(AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, NO_AUTH);
+
+ return ret;
}
-static int test_oap_rekey_badcache(void)
+static int test_oap_rekey_srv_badcache(bool srv_auth)
{
test_default_cfg();
+ test_cfg.srv.auth = srv_auth;
+ test_cfg.cli.auth = AUTH;
- return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec);
+ return roundtrip_rekey_srv_badcache(root_ca_crt_ec, im_ca_crt_ec,
+ srv_auth);
}
-static int test_oap_roundtrip_kex_only(void)
+static int test_oap_rekey_srv_badcache_all(void)
{
- memset(&test_cfg, 0, sizeof(test_cfg));
+ int ret = 0;
- /* Server: KEX only, no auth */
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
+ ret |= test_oap_rekey_srv_badcache(AUTH);
+ ret |= test_oap_rekey_srv_badcache(NO_AUTH);
- /* Client: KEX only, no auth */
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ return ret;
+}
+
+static int test_oap_rekey_badcache(bool cli_auth)
+{
+ test_default_cfg();
+ test_cfg.cli.auth = cli_auth;
+
+ return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec,
+ cli_auth);
+}
+
+static int test_oap_rekey_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_badcache(NO_AUTH);
+ ret |= test_oap_rekey_badcache(AUTH);
+
+ return ret;
+}
+
+static int test_oap_roundtrip_kex_only(void)
+{
+ test_enc_noauth_cfg();
return roundtrip_kex_only();
}
@@ -434,8 +472,13 @@ static int test_oap_deflated_length_field(void)
/* Header field offsets for byte manipulation */
#define OAP_CIPHER_NID_OFFSET 24
+#define OAP_KDF_NID_OFFSET 26
+#define OAP_MD_NID_OFFSET 28
#define OAP_KEX_LEN_OFFSET 32
+/* A NID the crypto backend does not recognise; guarded by a test below. */
+#define UNSUPPORTED_NID 9999
+
/* Server rejects request when cipher NID set but no KEX data provided */
static int test_oap_nid_without_kex(void)
{
@@ -443,20 +486,9 @@ static int test_oap_nid_without_kex(void)
uint16_t cipher_nid;
uint16_t zero = 0;
- TEST_START();
+ test_enc_noauth_cfg();
- /* Configure unsigned KEX-only mode */
- memset(&test_cfg, 0, sizeof(test_cfg));
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -491,26 +523,44 @@ static int test_oap_nid_without_kex(void)
return TEST_RC_FAIL;
}
-/* Server rejects OAP request with unsupported cipher NID */
-static int test_oap_unsupported_nid(void)
+/* Guard: the tamper tests below rely on UNSUPPORTED_NID being invalid. */
+static int test_oap_unsupported_nid_undefined(void)
+{
+ TEST_START();
+
+ if (crypt_cipher_rank(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid cipher NID.\n");
+ goto fail;
+ }
+
+ if (crypt_kdf_rank(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid KDF NID.\n");
+ goto fail;
+ }
+
+ if (md_validate_nid(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid digest NID.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server rejects a request whose cipher/kdf/digest NID is unsupported */
+static int test_oap_unsupported_nid(size_t offset,
+ const char * label)
{
struct oap_test_ctx ctx;
uint16_t bad_nid;
- TEST_START();
+ test_enc_noauth_cfg();
- /* Configure unsigned KEX-only mode */
- memset(&test_cfg, 0, sizeof(test_cfg));
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START("(%s)", label);
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -520,48 +570,47 @@ static int test_oap_unsupported_nid(void)
goto fail_cleanup;
}
- /* Tamper: set cipher_nid to unsupported value */
- bad_nid = hton16(9999);
- memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &bad_nid,
- sizeof(bad_nid));
+ bad_nid = hton16(UNSUPPORTED_NID);
+ memcpy(ctx.req_hdr.data + offset, &bad_nid, sizeof(bad_nid));
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject unsupported cipher NID.\n");
+ printf("Server should reject unsupported %s NID.\n", label);
goto fail_cleanup;
}
oap_test_teardown(&ctx);
- TEST_SUCCESS();
+ TEST_SUCCESS("(%s)", label);
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
fail:
- TEST_FAIL();
+ TEST_FAIL("(%s)", label);
return TEST_RC_FAIL;
}
+static int test_oap_unsupported_nid_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_unsupported_nid(OAP_CIPHER_NID_OFFSET, "cipher");
+ ret |= test_oap_unsupported_nid(OAP_KDF_NID_OFFSET, "kdf");
+ ret |= test_oap_unsupported_nid(OAP_MD_NID_OFFSET, "digest");
+
+ return ret;
+}
+
/* Client rejects a response whose key-confirmation tag is tampered */
static int test_oap_key_confirm_mismatch(void)
{
struct oap_test_ctx ctx;
- TEST_START();
-
/* Unauthenticated + encrypted: response unsigned, KC is the gate */
- memset(&test_cfg, 0, sizeof(test_cfg));
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ test_enc_noauth_cfg();
+
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -821,21 +870,9 @@ static int test_oap_cli_rejects_downgrade(void)
struct oap_test_ctx ctx;
uint16_t weak;
- TEST_START();
-
- memset(&test_cfg, 0, sizeof(test_cfg));
-
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
+ test_enc_noauth_cfg();
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -884,21 +921,12 @@ static int test_oap_cli_rejects_suite_swap(void)
struct oap_test_ctx ctx;
uint16_t swap;
- TEST_START();
-
- memset(&test_cfg, 0, sizeof(test_cfg));
-
/* Both AES-128-GCM: a swap to AES-256 outranks the client floor */
- test_cfg.srv.kex = NID_X25519;
+ test_enc_noauth_cfg();
test_cfg.srv.cipher = NID_aes_128_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
test_cfg.cli.cipher = NID_aes_128_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -1064,15 +1092,17 @@ static int test_oap_roundtrip_md_all(void)
/* Timestamp is at offset 16 (after the 16-byte ID) */
#define OAP_TIMESTAMP_OFFSET 16
/* Test that packets with outdated timestamps are rejected */
-static int test_oap_outdated_packet(void)
+/* Server rejects a request whose timestamp is outside the replay window */
+static int test_oap_ts_reject(int delta_sec,
+ const char * label)
{
struct oap_test_ctx ctx;
- struct timespec old_ts;
- uint64_t old_stamp;
+ struct timespec ts;
+ uint64_t stamp;
test_default_cfg();
- TEST_START();
+ TEST_START("(%s)", label);
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -1087,78 +1117,39 @@ static int test_oap_outdated_packet(void)
goto fail_cleanup;
}
- /* Set timestamp to 30 seconds in the past (> 20s replay timer) */
- clock_gettime(CLOCK_REALTIME, &old_ts);
- old_ts.tv_sec -= OAP_REPLAY_TIMER + 10;
- old_stamp = hton64(TS_TO_UINT64(old_ts));
- memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &old_stamp,
- sizeof(old_stamp));
+ clock_gettime(CLOCK_REALTIME, &ts);
+ ts.tv_sec += delta_sec;
+ stamp = hton64(TS_TO_UINT64(ts));
+ memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &stamp,
+ sizeof(stamp));
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject outdated packet.\n");
+ printf("Server should reject %s packet.\n", label);
goto fail_cleanup;
}
oap_test_teardown(&ctx);
- TEST_SUCCESS();
+ TEST_SUCCESS("(%s)", label);
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
fail:
- TEST_FAIL();
+ TEST_FAIL("(%s)", label);
return TEST_RC_FAIL;
}
-/* Test that packets from the future are rejected */
-static int test_oap_future_packet(void)
+static int test_oap_ts_reject_all(void)
{
- struct oap_test_ctx ctx;
- struct timespec future_ts;
- uint64_t future_stamp;
-
- test_default_cfg();
-
- TEST_START();
-
- if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
- goto fail;
-
- if (oap_cli_prepare_ctx(&ctx) < 0) {
- printf("Client prepare failed.\n");
- goto fail_cleanup;
- }
-
- if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) {
- printf("Request too short for test.\n");
- goto fail_cleanup;
- }
-
- /* Set timestamp to 1 second in the future (> 100ms slack) */
- clock_gettime(CLOCK_REALTIME, &future_ts);
- future_ts.tv_sec += 1;
- future_stamp = hton64(TS_TO_UINT64(future_ts));
- memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &future_stamp,
- sizeof(future_stamp));
-
- if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject future packet.\n");
- goto fail_cleanup;
- }
-
- oap_test_teardown(&ctx);
-
- TEST_SUCCESS();
+ int ret = 0;
- return TEST_RC_SUCCESS;
+ /* Past the 20s replay window, and past the 100ms future slack. */
+ ret |= test_oap_ts_reject(-(OAP_REPLAY_TIMER + 10), "outdated");
+ ret |= test_oap_ts_reject(1, "future");
- fail_cleanup:
- oap_test_teardown(&ctx);
- fail:
- TEST_FAIL();
- return TEST_RC_FAIL;
+ return ret;
}
/* Test that replayed packets (same ID + timestamp) are rejected */
@@ -1300,9 +1291,12 @@ static int test_oap_replay_generations(void)
{
struct oap_hdr h;
struct timespec now;
+ struct timespec wait;
uint8_t id[OAP_ID_SIZE];
uint64_t cur;
uint64_t gen_ns;
+ uint64_t off;
+ uint64_t wait_ns;
uint64_t stamp_a;
uint64_t stamp_b;
@@ -1313,10 +1307,21 @@ static int test_oap_replay_generations(void)
goto fail;
}
- clock_gettime(CLOCK_REALTIME, &now);
- cur = TS_TO_UINT64(now);
gen_ns = (uint64_t) OAP_REPLAY_TIMER * BILLION;
+ /* Prev-gen stamp flakes on staleness near a generation top. */
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ off = cur % gen_ns;
+ if (gen_ns - off < BILLION) {
+ wait_ns = gen_ns - off + MILLION;
+ wait.tv_sec = (time_t) (wait_ns / BILLION);
+ wait.tv_nsec = (long) (wait_ns % BILLION);
+ nanosleep(&wait, NULL);
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ }
+
/* stamp_a in the current generation, stamp_b one generation older */
stamp_a = cur;
stamp_b = (cur / gen_ns) * gen_ns - 1;
@@ -1826,6 +1831,101 @@ static int test_oap_sealed_tamper(void)
return TEST_RC_FAIL;
}
+/* Cleartext md-only: rsp_tag echoes H(request) and is the sole gate */
+static int test_oap_cleartext_echo_tamper(void)
+{
+ struct oap_test_ctx ctx;
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* rsp_tag is the trailing field of an unsealed, unsigned response */
+ ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a tampered request echo.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Client rejects a response whose session ID does not match the request */
+static int test_oap_response_id_tamper(void)
+{
+ struct oap_test_ctx ctx;
+
+ /* Cleartext md-only: no seal, so the ID check itself must reject. */
+ memset(&test_cfg, 0, sizeof(test_cfg));
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* The session ID is the first field of the fixed header. */
+ ctx.resp_hdr.data[0] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a mismatched response ID.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
int oap_test(int argc,
char **argv)
{
@@ -1842,8 +1942,9 @@ int oap_test(int argc,
ret |= test_oap_roundtrip_auth_only();
ret |= test_oap_roundtrip_kex_only();
ret |= test_oap_piggyback_data();
- ret |= test_oap_rekey();
- ret |= test_oap_rekey_badcache();
+ ret |= test_oap_rekey_all();
+ ret |= test_oap_rekey_badcache_all();
+ ret |= test_oap_rekey_srv_badcache_all();
ret |= test_oap_roundtrip_all();
ret |= test_oap_roundtrip_md_all();
@@ -1855,7 +1956,8 @@ int oap_test(int argc,
ret |= test_oap_inflated_length_field();
ret |= test_oap_deflated_length_field();
ret |= test_oap_nid_without_kex();
- ret |= test_oap_unsupported_nid();
+ ret |= test_oap_unsupported_nid_undefined();
+ ret |= test_oap_unsupported_nid_all();
ret |= test_oap_cipher_mismatch();
ret |= test_oap_srv_enc_cli_none();
@@ -1864,8 +1966,7 @@ int oap_test(int argc,
ret |= test_oap_cli_rejects_suite_swap();
ret |= test_oap_srv_rejects_weak_kex();
- ret |= test_oap_outdated_packet();
- ret |= test_oap_future_packet();
+ ret |= test_oap_ts_reject_all();
ret |= test_oap_replay_packet();
ret |= test_oap_missing_root_ca();
ret |= test_oap_server_name_mismatch();
@@ -1880,6 +1981,8 @@ int oap_test(int argc,
ret |= test_oap_server_cert_hidden();
ret |= test_oap_sealed_tamper();
+ ret |= test_oap_cleartext_echo_tamper();
+ ret |= test_oap_response_id_tamper();
#else
(void) test_oap_roundtrip_auth_only;
(void) test_oap_roundtrip_kex_only;
diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c
index 8691aa00..36712830 100644
--- a/src/irmd/oap/tests/oap_test_ml_dsa.c
+++ b/src/irmd/oap/tests/oap_test_ml_dsa.c
@@ -29,6 +29,7 @@
#include "config.h"
#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
#include <ouroboros/flow.h>
#include <ouroboros/name.h>
#include <ouroboros/random.h>
@@ -46,10 +47,12 @@
#include <openssl/evp.h>
#endif
-#define CLI_AUTH 1
-#define NO_CLI_AUTH 0
-#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP
-#define SRV_ENCAP KEM_MODE_SERVER_ENCAP
+#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP
+#define SRV_ENCAP KEM_MODE_SERVER_ENCAP
+
+/* Wire constants for inspecting the encoded kex_len field. */
+#define OAP_KEX_LEN_OFFSET 32
+#define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 1 = client encaps */
extern const uint16_t kex_supported_nids[];
extern const uint16_t md_supported_nids[];
@@ -241,7 +244,7 @@ static int test_oap_roundtrip_auth_only(void)
/* Digest pin does not apply to PQC: the digest is intrinsic */
static int test_oap_cli_md_pin_exempts_pqc(void)
{
- test_cfg_init(NID_undef, NID_undef, NID_undef, 0, NO_CLI_AUTH);
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, NO_AUTH);
test_cfg.cli.md = NID_sha256;
return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
@@ -249,32 +252,78 @@ static int test_oap_cli_md_pin_exempts_pqc(void)
static int test_oap_srv_md_pin_exempts_pqc(void)
{
- test_cfg_init(NID_undef, NID_undef, NID_undef, 0, CLI_AUTH);
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, AUTH);
test_cfg.srv.md = NID_sha256;
return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
}
-static int test_oap_rekey(void)
+static int test_oap_rekey(bool srv_auth,
+ bool cli_auth)
{
test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
- 0, NO_CLI_AUTH);
+ 0, cli_auth);
+ test_cfg.srv.auth = srv_auth;
- return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml);
+ return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml,
+ srv_auth, cli_auth);
}
-static int test_oap_rekey_badcache(void)
+static int test_oap_rekey_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey(AUTH, NO_AUTH);
+ ret |= test_oap_rekey(AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, NO_AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_badcache(bool cli_auth)
{
test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
- 0, NO_CLI_AUTH);
+ 0, cli_auth);
- return roundtrip_rekey_badcache(root_ca_crt_ml, im_ca_crt_ml);
+ return roundtrip_rekey_badcache(root_ca_crt_ml, im_ca_crt_ml,
+ cli_auth);
+}
+
+static int test_oap_rekey_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_badcache(NO_AUTH);
+ ret |= test_oap_rekey_badcache(AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_srv_badcache(bool srv_auth)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, AUTH);
+ test_cfg.srv.auth = srv_auth;
+
+ return roundtrip_rekey_srv_badcache(root_ca_crt_ml, im_ca_crt_ml,
+ srv_auth);
+}
+
+static int test_oap_rekey_srv_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_srv_badcache(AUTH);
+ ret |= test_oap_rekey_srv_badcache(NO_AUTH);
+
+ return ret;
}
static int test_oap_corrupted_request(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, CLI_AUTH);
+ SRV_ENCAP, AUTH);
return corrupted_request(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -282,7 +331,7 @@ static int test_oap_corrupted_request(void)
static int test_oap_corrupted_response(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, NO_CLI_AUTH);
+ SRV_ENCAP, NO_AUTH);
return corrupted_response(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -290,7 +339,7 @@ static int test_oap_corrupted_response(void)
static int test_oap_truncated_request(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, NO_CLI_AUTH);
+ SRV_ENCAP, NO_AUTH);
return truncated_request(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -303,7 +352,7 @@ static int test_oap_roundtrip_kem(int kex,
const char * mode_str = kem_mode == CLI_ENCAP ? "cli" : "srv";
test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(),
- kem_mode, NO_CLI_AUTH);
+ kem_mode, NO_AUTH);
TEST_START("(%s, %s encaps)", kex_str, mode_str);
@@ -366,6 +415,231 @@ static int test_oap_roundtrip_kem_all(void)
return ret;
}
+/* Re-key over a KEM KEX: forced ephemeral server-encap + cert-drop cache. */
+static int test_oap_rekey_kem(int kex,
+ int kem_mode)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+ const char * mode_str = "srv";
+ uint8_t key0[SYMMKEYSZ];
+
+ if (kem_mode == CLI_ENCAP)
+ mode_str = "cli";
+
+ test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(),
+ kem_mode, NO_AUTH);
+
+ TEST_START("(%s, %s encaps)", kex_str, mode_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Initial keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached for re-key.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(key0, ctx.cli.key, SYMMKEYSZ);
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Re-key keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) {
+ printf("Re-key did not produce a fresh key.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s, %s encaps)", kex_str, mode_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s, %s encaps)", kex_str, mode_str);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_rekey_kem_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ const char * algo = kex_nid_to_str(kex_supported_nids[i]);
+
+ if (!IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_rekey_kem(kex_supported_nids[i], SRV_ENCAP);
+ ret |= test_oap_rekey_kem(kex_supported_nids[i], CLI_ENCAP);
+ }
+
+ return ret;
+}
+
+/*
+ * Client-encap bakes the KDF into the ciphertext, so the server cannot
+ * upgrade it: a client KDF weaker than the server floor must be rejected.
+ */
+static int test_oap_kem_kdf_floor(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ test_cfg_init(kex, NID_aes_256_gcm, NID_sha256,
+ CLI_ENCAP, NO_AUTH);
+ test_cfg.srv.kdf = NID_sha512;
+ test_cfg.cli.kdf = NID_sha256;
+
+ TEST_START("(%s)", kex_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server accepted a client KDF below its floor.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A client-encap flow re-keys to ephemeral server-encap, so it keeps
+ * forward secrecy. The re-key request must advertise server-encap
+ * (kex_len Role bit clear) rather than re-using client encapsulation.
+ */
+static int test_oap_rekey_kem_forcing(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+ uint16_t kex_len;
+
+ test_cfg_init(kex, NID_aes_256_gcm, NID_sha256,
+ CLI_ENCAP, NO_AUTH);
+
+ TEST_START("(%s)", kex_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(&kex_len, ctx.req_hdr.data + OAP_KEX_LEN_OFFSET,
+ sizeof(kex_len));
+ kex_len = ntoh16(kex_len);
+
+ if (kex_len & OAP_KEX_ROLE_BIT) {
+ printf("Re-key did not force server-encap KEX.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
static int test_oap_kem_srv_uncfg(int kex)
{
struct oap_test_ctx ctx;
@@ -454,7 +728,7 @@ int oap_test_ml_dsa(int argc,
(void) argc;
(void) argv;
-#ifdef HAVE_OPENSSL_ML_KEM
+#ifdef HAVE_ML
ret |= test_oap_roundtrip_auth_only();
ret |= test_oap_cli_md_pin_exempts_pqc();
ret |= test_oap_srv_md_pin_exempts_pqc();
@@ -467,16 +741,28 @@ int oap_test_ml_dsa(int argc,
ret |= test_oap_corrupted_response();
ret |= test_oap_truncated_request();
- ret |= test_oap_rekey();
- ret |= test_oap_rekey_badcache();
+ ret |= test_oap_rekey_all();
+ ret |= test_oap_rekey_badcache_all();
+ ret |= test_oap_rekey_srv_badcache_all();
+ ret |= test_oap_rekey_kem_all();
+ ret |= test_oap_kem_kdf_floor(NID_MLKEM768);
+ ret |= test_oap_rekey_kem_forcing(NID_MLKEM768);
#else
(void) test_oap_roundtrip_auth_only;
(void) test_oap_cli_md_pin_exempts_pqc;
(void) test_oap_srv_md_pin_exempts_pqc;
(void) test_oap_rekey;
+ (void) test_oap_rekey_all;
(void) test_oap_rekey_badcache;
+ (void) test_oap_rekey_badcache_all;
+ (void) test_oap_rekey_srv_badcache;
+ (void) test_oap_rekey_srv_badcache_all;
(void) test_oap_roundtrip_kem;
(void) test_oap_roundtrip_kem_all;
+ (void) test_oap_rekey_kem;
+ (void) test_oap_rekey_kem_all;
+ (void) test_oap_kem_kdf_floor;
+ (void) test_oap_rekey_kem_forcing;
(void) test_oap_kem_srv_uncfg;
(void) test_oap_kem_srv_uncfg_all;
(void) test_oap_corrupted_request;
diff --git a/src/lib/config.h.in b/src/lib/config.h.in
index 06c5e23f..4ebaa194 100644
--- a/src/lib/config.h.in
+++ b/src/lib/config.h.in
@@ -33,9 +33,8 @@
#cmakedefine HAVE_LIBGCRYPT
#cmakedefine HAVE_OPENSSL
#ifdef HAVE_OPENSSL
-#cmakedefine HAVE_OPENSSL_ML_KEM
-#cmakedefine HAVE_OPENSSL_ML_DSA
-#cmakedefine HAVE_OPENSSL_SLH_DSA
+#cmakedefine HAVE_ML
+#cmakedefine HAVE_SLH
#define HAVE_ENCRYPTION
#define SECMEM_MINSIZE @SECMEM_MINSIZE@
#endif
@@ -107,3 +106,4 @@
#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 */
+#define FLOW_WM_CHECK (1u << @KEY_REKEY_WM_CHECK_BITS@) /* wm chk/n wr */
diff --git a/src/lib/crypt.c b/src/lib/crypt.c
index 5da9d392..cbbb9bc2 100644
--- a/src/lib/crypt.c
+++ b/src/lib/crypt.c
@@ -96,7 +96,7 @@ const uint16_t kex_supported_nids[] = {
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
+#ifdef HAVE_ML
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) */
@@ -887,11 +887,17 @@ int crypt_get_headsz(struct crypt_ctx * ctx)
int crypt_rekey(struct crypt_ctx * ctx,
struct crypt_sk * sk)
{
+ int ret;
+
assert(ctx != NULL);
assert(sk != NULL);
assert(ctx->kr != NULL);
- return keyrot_rekey(ctx->kr, sk->key, sk->epoch) == 0 ? 0 : -ECRYPT;
+ ret = keyrot_rekey(ctx->kr, sk->key, sk->epoch);
+ if (ret == -EREPLAY)
+ return -EREPLAY;
+
+ return ret == 0 ? 0 : -ECRYPT;
}
int crypt_get_tagsz(struct crypt_ctx * ctx)
diff --git a/src/lib/crypt/keyrot.c b/src/lib/crypt/keyrot.c
index 8b0d9429..e98df356 100644
--- a/src/lib/crypt/keyrot.c
+++ b/src/lib/crypt/keyrot.c
@@ -26,6 +26,7 @@
#include <ouroboros/atomics.h>
#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
#include <ouroboros/pthread.h>
#include <ouroboros/rcu.h>
@@ -469,12 +470,30 @@ void keyrot_destroy(struct keyrot * kr)
free(kr);
}
+/* A dup live epoch shadows straggler RX; epoch is peer-driven. */
+static struct kr_batch * kr_live_batch(struct kr_batch * cur,
+ struct kr_batch * prev,
+ uint8_t epoch)
+{
+ if (epoch == cur->epoch)
+ return cur;
+
+ if (prev != NULL && epoch == prev->epoch)
+ return prev;
+
+ return NULL;
+}
+
int keyrot_rekey(struct keyrot * kr,
const uint8_t * root,
uint8_t epoch)
{
struct kr_batch * nb;
struct kr_batch * old_prev;
+ struct kr_batch * cur;
+ struct kr_batch * prev;
+ struct kr_batch * live;
+ int ret;
assert(kr != NULL);
assert(root != NULL);
@@ -488,6 +507,21 @@ int keyrot_rekey(struct keyrot * kr,
rcu_wrlock(&kr->guard);
+ cur = rcu_deref(kr->cur);
+ prev = rcu_deref(kr->prev);
+
+ live = kr_live_batch(cur, prev, epoch);
+ if (live != NULL) {
+ /* The first node key identifies the root. */
+ if (crypt_ct_cmp(live->nodes, nb->nodes, SYMMKEYSZ) == 0)
+ ret = -EREPLAY;
+ else
+ ret = -1;
+ rcu_wrunlock(&kr->guard);
+ kr_batch_free(nb);
+ return ret;
+ }
+
old_prev = kr->prev;
rcu_assign(kr->prev, kr->cur);
rcu_publish(nb);
diff --git a/src/lib/dev.c b/src/lib/dev.c
index 845d07a8..166aba5c 100644
--- a/src/lib/dev.c
+++ b/src/lib/dev.c
@@ -364,10 +364,7 @@ static int crc_check(struct ssm_pk_buff * spb,
/* FRCT included here so it can use proc and dev.c statics directly. */
#include "frct.c"
-/*
- * SACK / DATA carry trailer CRC32; HCS protects the headers on every
- * FRCT packet. Decrypt before any check so plaintext is authoritative.
- */
+/* Decrypt before any check so the plaintext is authoritative. */
static bool invalid_pkt(struct flow * flow,
struct ssm_pk_buff * spb)
{
@@ -449,10 +446,6 @@ static void compute_wait_deadline(const struct timespec * dl,
*out = *dl;
}
-/*
- * proc.lock rdlock held across each iteration so flow_fini's wrlock
- * waits for us to finish; FLOWDOWN already set means we exit promptly.
- */
static void flow_drain_rx_nb(struct flow * flow)
{
ssize_t idx;
@@ -529,11 +522,7 @@ static void flow_drain_rx_nb(struct flow * flow)
/* Throttle re-key retries so a failed attempt can't storm the IRMd. */
#define REKEY_BACKOFF_NS (250 * MILLION)
-/*
- * Pull a parked re-key seed from the IRMd and install it. Driven from the
- * data path when RB_REKEY shows on rx_rb. crypt_rekey is concurrency-safe
- * on its own; proc.lock (rd) only guards against teardown.
- */
+/* proc.lock (rd) only guards teardown; crypt_rekey self-synchronises. */
static void flow_rekey(struct flow * flow)
{
struct flow_info info;
@@ -597,10 +586,7 @@ static void flow_rekey(struct flow * flow)
crypt_secure_clear(key, SYMMKEYSZ);
}
-/*
- * Wait clamped by caller deadline, next tw expiry, and TICTIME;
- * a clamp-timeout means tw work is due, not caller-deadline.
- */
+/* A clamp-timeout means tw work is due, not the caller deadline. */
static int flow_rx_one(struct flow * flow,
struct timespec * abs)
{
@@ -629,7 +615,12 @@ static int flow_rx_one(struct flow * flow,
continue;
}
+ pthread_cleanup_push(__cleanup_rwlock_unlock, &proc.lock);
+
idx = ssm_rbuff_read_b(rx_rb, &wait_abs);
+
+ pthread_cleanup_pop(false);
+
if (idx == -ETIMEDOUT) {
pthread_rwlock_unlock(&proc.lock);
if (deadline_passed(abs))
@@ -688,12 +679,7 @@ static void flow_clear(int fd)
proc.flows[fd].info.id = -1;
}
-/*
- * Set RB_FLOWDOWN on rx/tx so any in-flight blocking reads or writes
- * wake up and drop their proc.lock rdlock. Must run BEFORE flow_fini's
- * wrlock, else the wrlock blocks on those rdlock holders and the
- * in-flight calls never see the FLOWDOWN signal.
- */
+/* Order before flow_fini's wrlock, which blocks on rdlock holders. */
static void flow_quiesce(int fd)
{
struct ssm_rbuff * rx_rb = proc.flows[fd].rx_rb;
@@ -864,9 +850,10 @@ static void init(int argc,
char ** argv,
char ** envp)
{
- struct proc_info info;
- char * prog = argv[0];
- int i;
+ struct proc_info info;
+ char * prog = argv[0];
+ int i;
+ pthread_rwlockattr_t attr;
#ifdef PROC_FLOW_STATS
char procstr[32];
#endif
@@ -951,7 +938,16 @@ static void init(int argc,
goto fail_cond;
}
- if (pthread_rwlock_init(&proc.lock, NULL) < 0) {
+ /* Writer-preferred: FRCT readers must not starve flow accept. */
+ if (pthread_rwlockattr_init(&attr) != 0) {
+ fprintf(stderr, "FATAL: Could not init rwlock attributes.\n");
+ goto fail_rwlock_attr;
+ }
+#if defined(__GLIBC__)
+ pthread_rwlockattr_setkind_np(
+ &attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
+#endif
+ if (pthread_rwlock_init(&proc.lock, &attr) != 0) {
fprintf(stderr, "FATAL: Could not initialize flow lock.\n");
goto fail_flow_lock;
}
@@ -981,6 +977,8 @@ static void init(int argc,
}
}
#endif
+ pthread_rwlockattr_destroy(&attr);
+
return;
#if defined PROC_FLOW_STATS
@@ -994,6 +992,8 @@ static void init(int argc,
fail_fqset:
pthread_rwlock_destroy(&proc.lock);
fail_flow_lock:
+ pthread_rwlockattr_destroy(&attr);
+ fail_rwlock_attr:
pthread_cond_destroy(&proc.cond);
fail_cond:
pthread_mutex_destroy(&proc.mtx);
@@ -1629,12 +1629,6 @@ static __inline__ uint16_t flow_frag_role(size_t i, size_t n)
return FRCT_FR_MID;
}
-/*
- * Stream-mode write: split buf into chunks of
- * (frag_mtu - PCI - PCI_STREAM) bytes; each chunk goes through the
- * normal tx path. frcti_snd injects the [start,end) extension and
- * advances snd_byte_next under its wrlock. No FFGM/LFGM role bits.
- */
static ssize_t flow_write_stream(struct flow * flow,
const void * buf,
size_t count,
@@ -1761,18 +1755,8 @@ static ssize_t flow_write_frag(struct flow * flow,
}
/*
- * Watermark: re-key when the TX batch is within KEY_REKEY_WATERMARK node
- * keys of exhaustion (0 disables), ahead of the timer; consult keyrot at
- * most once per FLOW_WM_CHECK writes.
- */
-#define FLOW_WM_CHECK (1u << 16)
-
-/*
- * Switch TX to the freshly installed epoch. The initiator holds the OAP
- * key-confirm tag and bootstraps after the install grace, which also lets
- * the peer install the batch first. The responder has no such proof: it
- * waits for peer_synced (a packet under the new batch), with a last-resort
- * promote near exhaustion so a silent peer can't stall it.
+ * Initiator promotes on the install grace (it holds the key-confirm
+ * tag); responder waits for peer_synced, with a near-exhaustion floor.
*/
static void flow_tx_promote(struct flow * flow)
{
@@ -1806,11 +1790,7 @@ static void flow_tx_promote(struct flow * flow)
flow->rk_grace.tv_nsec = 0;
}
-/*
- * Ask the IRMd to start an OAP re-key for this flow. The reply carries no
- * key; the seed arrives later over RB_REKEY. Fired from the write path as
- * the TX batch nears exhaustion, ahead of the timer.
- */
+/* The reply carries no key; the seed arrives later over RB_REKEY. */
static int flow_rekey_trigger(struct flow * flow)
{
struct flow_info info;
@@ -1834,11 +1814,6 @@ static int flow_rekey_trigger(struct flow * flow)
return 0;
}
-/*
- * True when the live TX batch has run low and no re-key is in flight.
- * Advances a throttle so the (locking) keyrot consult runs at most once
- * per FLOW_WM_CHECK writes.
- */
static bool flow_wm_due(struct flow * flow)
{
uint32_t tick;
@@ -2681,23 +2656,24 @@ int ipcp_flow_read(int fd,
pthread_rwlock_rdlock(&proc.lock);
- assert(flow->info.id >= 0);
+ if (flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return -ENOTALLOC;
+ }
/* Raw flow: deliver the popped pkt directly (no FRCT rq). */
if (flow->frcti == NULL) {
- pthread_rwlock_unlock(&proc.lock);
idx = flow_rx_spb(flow, spb, false, NULL);
+ pthread_rwlock_unlock(&proc.lock);
return idx < 0 ? (int) idx : 0;
}
while (!FRCTI_PDU_READY(flow->frcti)) {
- pthread_rwlock_unlock(&proc.lock);
-
idx = flow_rx_spb(flow, spb, false, NULL);
- if (idx < 0)
+ if (idx < 0) {
+ pthread_rwlock_unlock(&proc.lock);
return idx;
-
- pthread_rwlock_rdlock(&proc.lock);
+ }
FRCTI_RCV(flow->frcti, *spb);
}
@@ -2770,10 +2746,13 @@ int np1_flow_read(int fd,
flow = &proc.flows[fd];
- assert(flow->info.id >= 0);
-
pthread_rwlock_rdlock(&proc.lock);
+ if (flow->info.id < 0) {
+ pthread_rwlock_unlock(&proc.lock);
+ return -ENOTALLOC;
+ }
+
off = ssm_rbuff_read(flow->rx_rb);
if (off < 0) {
pthread_rwlock_unlock(&proc.lock);
diff --git a/src/lib/ssm/rbuff.c b/src/lib/ssm/rbuff.c
index 0121af89..7886b5c2 100644
--- a/src/lib/ssm/rbuff.c
+++ b/src/lib/ssm/rbuff.c
@@ -242,6 +242,15 @@ void ssm_rbuff_close(struct ssm_rbuff * rb)
rbuff_destroy(rb);
}
+/* Cancel cleanup for a blocked reader: unlock mtx AND drop the n_users ref. */
+static void __cleanup_rbuff_reader(void * o)
+{
+ struct ssm_rbuff * rb = (struct ssm_rbuff *) o;
+
+ pthread_mutex_unlock(rb->mtx);
+ __atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
+}
+
int ssm_rbuff_write(struct ssm_rbuff * rb,
size_t off)
{
@@ -318,7 +327,7 @@ int ssm_rbuff_write_b(struct ssm_rbuff * rb,
robust_mutex_lock(rb->mtx);
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx);
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
while (IS_FULL(rb) && ret != -ETIMEDOUT) {
flags = __atomic_load_n(rb->flags, __ATOMIC_SEQ_CST);
@@ -417,7 +426,7 @@ ssize_t ssm_rbuff_read_b(struct ssm_rbuff * rb,
robust_mutex_lock(rb->mtx);
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx);
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
while (IS_EMPTY(rb) &&
idx != -ETIMEDOUT &&
@@ -483,12 +492,14 @@ void ssm_rbuff_fini(struct ssm_rbuff * rb)
robust_mutex_lock(rb->mtx);
- pthread_cleanup_push(__cleanup_mutex_unlock, rb->mtx);
+ pthread_cleanup_push(__cleanup_rbuff_reader, rb);
while (!IS_EMPTY(rb))
robust_wait(rb->del, rb->mtx, NULL);
- pthread_cleanup_pop(true);
+ pthread_cleanup_pop(false);
+
+ pthread_mutex_unlock(rb->mtx);
__atomic_fetch_sub(&rb->n_users, 1, __ATOMIC_SEQ_CST);
}
diff --git a/src/lib/tests/auth_test.c b/src/lib/tests/auth_test.c
index af7cf81c..61f97683 100644
--- a/src/lib/tests/auth_test.c
+++ b/src/lib/tests/auth_test.c
@@ -304,7 +304,7 @@ static int test_store_add(void)
fail_add:
crypt_free_crt(_root_ca_crt);
fail_load:
- crypt_free_crt(_root_ca_crt);
+ auth_destroy_ctx(ctx);
fail_create:
TEST_FAIL();
return TEST_RC_FAIL;
@@ -737,7 +737,8 @@ int auth_test(int argc,
(void) test_auth_bad_signature;
(void) test_crt_str;
- ret = TEST_RC_SKIP;
+ if (ret == 0)
+ ret = TEST_RC_SKIP;
#endif
return ret;
}
diff --git a/src/lib/tests/auth_test_ml_dsa.c b/src/lib/tests/auth_test_ml_dsa.c
index cc72e61b..e324c32d 100644
--- a/src/lib/tests/auth_test_ml_dsa.c
+++ b/src/lib/tests/auth_test_ml_dsa.c
@@ -333,7 +333,7 @@ int auth_test_ml_dsa(int argc,
(void) argc;
(void) argv;
-#ifdef HAVE_OPENSSL_ML_DSA
+#ifdef HAVE_ML
ret |= test_auth_create_destroy_ctx();
ret |= test_load_free_crt();
ret |= test_load_free_privkey();
diff --git a/src/lib/tests/auth_test_slh_dsa.c b/src/lib/tests/auth_test_slh_dsa.c
index 511d20fe..e9af8da8 100644
--- a/src/lib/tests/auth_test_slh_dsa.c
+++ b/src/lib/tests/auth_test_slh_dsa.c
@@ -344,7 +344,7 @@ int auth_test_slh_dsa(int argc,
(void) argc;
(void) argv;
-#ifdef HAVE_OPENSSL_SLH_DSA
+#ifdef HAVE_SLH
ret |= test_auth_create_destroy_ctx();
ret |= test_load_free_crt();
ret |= test_load_free_privkey();
diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c
index 2d752238..f00618d8 100644
--- a/src/lib/tests/crypt_test.c
+++ b/src/lib/tests/crypt_test.c
@@ -528,6 +528,47 @@ static int test_crypt_headsz(void)
return TEST_RC_FAIL;
}
+static int test_crypt_ct_cmp(void)
+{
+ uint8_t a[64];
+ uint8_t b[64];
+ size_t i;
+
+ TEST_START();
+
+ for (i = 0; i < sizeof(a); i++)
+ a[i] = (uint8_t) i;
+
+ memcpy(b, a, sizeof(a));
+
+ if (crypt_ct_cmp(a, b, sizeof(a)) != 0) {
+ printf("Equal buffers should compare equal.\n");
+ goto fail;
+ }
+
+ if (crypt_ct_cmp(a, b, 0) != 0) {
+ printf("Zero length should compare equal.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < sizeof(a); i++) {
+ b[i] ^= 0x01;
+ if (crypt_ct_cmp(a, b, sizeof(a)) == 0) {
+ printf("Difference at byte %zu not detected.\n", i);
+ goto fail;
+ }
+
+ b[i] ^= 0x01;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
int crypt_test(int argc,
char ** argv)
{
@@ -538,6 +579,7 @@ int crypt_test(int argc,
ret |= test_crypt_create_destroy();
ret |= test_encrypt_decrypt_all();
+ ret |= test_crypt_ct_cmp();
#ifdef HAVE_OPENSSL
ret |= test_cipher_nid_values();
ret |= test_md_nid_values();
@@ -549,7 +591,8 @@ int crypt_test(int argc,
(void) test_aad_tamper_all;
(void) test_crypt_headsz;
- return TEST_RC_SKIP;
+ if (ret == 0)
+ ret = TEST_RC_SKIP;
#endif
return ret;
}
diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c
index 0a00ccab..5b2ccfc2 100644
--- a/src/lib/tests/kex_test.c
+++ b/src/lib/tests/kex_test.c
@@ -305,7 +305,7 @@ static int test_kex_validate_algo(void)
goto fail;
}
-#ifdef HAVE_OPENSSL_ML_KEM
+#ifdef HAVE_ML
if (kex_validate_algo("ML-KEM-768") != 0) {
printf("ML-KEM-768 should be valid.\n");
goto fail;
@@ -443,6 +443,57 @@ static int test_kex_dhe_wrong_algo(void)
return TEST_RC_FAIL;
}
+static int test_kex_dhe_no_kdf(void)
+{
+ struct sec_config kex;
+ void * pkp1;
+ void * pkp2;
+ buffer_t pk2;
+ ssize_t len;
+ uint8_t buf1[CRYPT_KEY_BUFSZ];
+ uint8_t buf2[CRYPT_KEY_BUFSZ];
+ uint8_t s[SYMMKEYSZ];
+
+ TEST_START();
+
+ memset(&kex, 0, sizeof(kex));
+ SET_KEX_ALGO(&kex, "X25519");
+
+ if (kex_pkp_create(&kex, &pkp1, buf1) < 0) {
+ printf("Failed to create first key pair.\n");
+ goto fail;
+ }
+
+ len = kex_pkp_create(&kex, &pkp2, buf2);
+ if (len < 0) {
+ printf("Failed to create second key pair.\n");
+ goto fail_pkp1;
+ }
+
+ pk2.len = (size_t) len;
+ pk2.data = buf2;
+
+ /* No KDF configured: derive must fail, not fall back. */
+ if (kex_dhe_derive(&kex, pkp1, pk2, s) == 0) {
+ printf("Derive succeeded without a KDF.\n");
+ goto fail_pkp2;
+ }
+
+ kex_pkp_destroy(pkp2);
+ kex_pkp_destroy(pkp1);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pkp2:
+ kex_pkp_destroy(pkp2);
+ fail_pkp1:
+ kex_pkp_destroy(pkp1);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
static int test_kex_load_dhe_privkey(void)
{
void * key;
@@ -1171,6 +1222,7 @@ int kex_test(int argc,
ret |= test_kex_validate_algo();
ret |= test_kex_get_algo_from_pk_all();
ret |= test_kex_dhe_wrong_algo();
+ ret |= test_kex_dhe_no_kdf();
ret |= test_kex_dhe_corrupted_pubkey_all();
ret |= test_kex_load_dhe_privkey();
ret |= test_kex_load_dhe_pubkey();
@@ -1183,12 +1235,14 @@ int kex_test(int argc,
(void) test_kex_all;
(void) test_kex_validate_algo;
(void) test_kex_get_algo_from_pk_all;
- (void) test_kex_dhe_wrong_algo();
+ (void) test_kex_dhe_wrong_algo;
+ (void) test_kex_dhe_no_kdf;
(void) test_kex_dhe_corrupted_pubkey_all;
(void) test_kex_load_dhe_privkey;
(void) test_kex_load_dhe_pubkey;
- ret = TEST_RC_SKIP;
+ if (ret == 0)
+ ret = TEST_RC_SKIP;
#endif
return ret;
}
diff --git a/src/lib/tests/kex_test_ml_kem.c b/src/lib/tests/kex_test_ml_kem.c
index 7761c3dc..1f7d7531 100644
--- a/src/lib/tests/kex_test_ml_kem.c
+++ b/src/lib/tests/kex_test_ml_kem.c
@@ -528,7 +528,7 @@ int kex_test_ml_kem(int argc,
(void) argc;
(void) argv;
-#ifdef HAVE_OPENSSL_ML_KEM
+#ifdef HAVE_ML
ret |= test_kex_load_kem_privkey();
ret |= test_kex_load_kem_pubkey();
ret |= test_kex_kem_all();
diff --git a/src/lib/tests/keyrot_test.c b/src/lib/tests/keyrot_test.c
index 1c9f741b..efdc718e 100644
--- a/src/lib/tests/keyrot_test.c
+++ b/src/lib/tests/keyrot_test.c
@@ -28,6 +28,7 @@
#ifdef HAVE_OPENSSL
#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
#include <ouroboros/pthread.h>
#include "crypt/keyrot.h"
@@ -45,6 +46,13 @@ static const uint8_t SEED_A[SYMMKEYSZ] = {
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20
};
+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
+};
+
static int test_create_destroy(void)
{
struct keyrot * kr;
@@ -94,6 +102,100 @@ static int test_epoch_range(void)
return TEST_RC_FAIL;
}
+/* Epochs of the live batches (cur, prev) must stay unique. */
+static int test_rekey_dup_epoch(void)
+{
+ struct keyrot * a;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 0, 0);
+ if (a == NULL)
+ goto fail;
+
+ if (keyrot_rekey(a, SEED_B, 0) != -1) {
+ printf("New key to the current epoch not a conflict.\n");
+ goto fail_a;
+ }
+
+ if (keyrot_rekey(a, SEED_B, 1) != 0) {
+ printf("Re-key to a fresh epoch refused.\n");
+ goto fail_a;
+ }
+
+ if (keyrot_rekey(a, SEED_B, 1) != -EREPLAY) {
+ printf("Same key to the current epoch not a replay.\n");
+ goto fail_a;
+ }
+
+ if (keyrot_rekey(a, SEED_A, 1) != -1) {
+ printf("New key to the current epoch not a conflict.\n");
+ goto fail_a;
+ }
+
+ if (keyrot_rekey(a, SEED_A, 0) != -EREPLAY) {
+ printf("Same key to the previous epoch not a replay.\n");
+ goto fail_a;
+ }
+
+ if (keyrot_rekey(a, SEED_B, 0) != -1) {
+ printf("New key to the previous epoch not a conflict.\n");
+ goto fail_a;
+ }
+
+ keyrot_destroy(a);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_a:
+ keyrot_destroy(a);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The 4-bit wire epoch legitimately wraps 15 -> 0. */
+static int test_rekey_epoch_wrap(void)
+{
+ struct keyrot * a;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+
+ TEST_START();
+
+ a = keyrot_create(SEED_A, 14, 0);
+ if (a == NULL)
+ goto fail;
+
+ if (keyrot_rekey(a, SEED_B, 15) != 0)
+ goto fail_a;
+
+ if (keyrot_rekey(a, SEED_A, 0) != 0) {
+ printf("Epoch wrap 15 -> 0 refused.\n");
+ goto fail_a;
+ }
+
+ keyrot_tx_promote(a);
+
+ if (keyrot_tx_next(a, sel, &k, n) != 0) {
+ printf("TX failed after epoch wrap.\n");
+ 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;
@@ -102,8 +204,9 @@ static int test_tx_deterministic(void)
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;
+ uint8_t ka[SYMMKEYSZ];
+ const uint8_t * pa;
+ const uint8_t * pb;
TEST_START();
@@ -115,16 +218,18 @@ static int test_tx_deterministic(void)
if (b == NULL)
goto fail_a;
- if (keyrot_tx_next(a, sela, &ka, na) != 0)
+ if (keyrot_tx_next(a, sela, &pa, na) != 0)
goto fail_b;
- if (keyrot_tx_next(b, selb, &kb, nb) != 0)
+ /* Copy out: pa points into the tcache, pb may reuse the slot. */
+ memcpy(ka, pa, SYMMKEYSZ);
+ if (keyrot_tx_next(b, selb, &pb, nb) != 0)
goto fail_b;
if (memcmp(sela, selb, KR_SELECTOR_LEN) != 0)
goto fail_b;
- if (memcmp(ka, kb, SYMMKEYSZ) != 0)
+ if (memcmp(ka, pb, SYMMKEYSZ) != 0)
goto fail_b;
if (memcmp(na, nb, KR_NONCE_LEN) != 0)
@@ -388,13 +493,6 @@ static int test_random_access(void)
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
@@ -873,6 +971,56 @@ static int test_commit_evicted(void)
return TEST_RC_FAIL;
}
+/* TX fails closed when the tx_epoch batch is evicted, until promote. */
+static int test_tx_fail_closed(void)
+{
+ struct keyrot * b;
+ uint8_t sel[KR_SELECTOR_LEN];
+ uint8_t n[KR_NONCE_LEN];
+ const uint8_t * k;
+
+ TEST_START();
+
+ b = keyrot_create(SEED_A, 0, 0);
+ if (b == NULL)
+ goto fail;
+
+ if (keyrot_rekey(b, SEED_B, 1) != 0)
+ goto fail_b;
+
+ if (keyrot_tx_next(b, sel, &k, n) != 0) {
+ printf("TX should keep the old epoch after one re-key.\n");
+ goto fail_b;
+ }
+
+ /* Second re-key without promote evicts the TX epoch-0 batch. */
+ if (keyrot_rekey(b, SEED_A, 2) != 0)
+ goto fail_b;
+
+ if (keyrot_tx_next(b, sel, &k, n) == 0) {
+ printf("TX should fail closed with tx_epoch evicted.\n");
+ goto fail_b;
+ }
+
+ keyrot_tx_promote(b);
+
+ if (keyrot_tx_next(b, sel, &k, n) != 0) {
+ printf("TX should resync after promote.\n");
+ 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
@@ -1063,6 +1211,8 @@ int keyrot_test(int argc,
#ifdef HAVE_OPENSSL
ret |= test_create_destroy();
ret |= test_epoch_range();
+ ret |= test_rekey_dup_epoch();
+ ret |= test_rekey_epoch_wrap();
ret |= test_tx_deterministic();
ret |= test_selector_layout();
ret |= test_nodes_left_initial();
@@ -1071,6 +1221,7 @@ int keyrot_test(int argc,
ret |= test_random_access();
ret |= test_peer_switched_commit_only();
ret |= test_commit_evicted();
+ ret |= test_tx_fail_closed();
ret |= test_replay_window();
ret |= test_lookup_no_commit();
ret |= test_commit_prev_batch();