diff options
| -rw-r--r-- | cmake/config/ipcp/eth.cmake | 4 | ||||
| -rw-r--r-- | cmake/config/lib.cmake | 4 | ||||
| -rw-r--r-- | cmake/dependencies/crypt/openssl.cmake | 51 | ||||
| -rw-r--r-- | src/ipcpd/eth/eth.c | 27 | ||||
| -rw-r--r-- | src/ipcpd/unicast/dir/dht.c | 53 | ||||
| -rw-r--r-- | src/irmd/config.h.in | 3 | ||||
| -rw-r--r-- | src/irmd/oap/cli.c | 6 | ||||
| -rw-r--r-- | src/irmd/oap/hdr.c | 18 | ||||
| -rw-r--r-- | src/irmd/oap/hdr.h | 6 | ||||
| -rw-r--r-- | src/irmd/oap/io.c | 2 | ||||
| -rw-r--r-- | src/irmd/oap/srv.c | 6 | ||||
| -rw-r--r-- | src/irmd/oap/tests/common.c | 128 | ||||
| -rw-r--r-- | src/irmd/oap/tests/common.h | 15 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test.c | 421 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test_ml_dsa.c | 324 | ||||
| -rw-r--r-- | src/lib/config.h.in | 6 | ||||
| -rw-r--r-- | src/lib/crypt.c | 10 | ||||
| -rw-r--r-- | src/lib/crypt/keyrot.c | 34 | ||||
| -rw-r--r-- | src/lib/dev.c | 107 | ||||
| -rw-r--r-- | src/lib/ssm/rbuff.c | 19 | ||||
| -rw-r--r-- | src/lib/tests/auth_test.c | 5 | ||||
| -rw-r--r-- | src/lib/tests/auth_test_ml_dsa.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/auth_test_slh_dsa.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/crypt_test.c | 45 | ||||
| -rw-r--r-- | src/lib/tests/kex_test.c | 60 | ||||
| -rw-r--r-- | src/lib/tests/kex_test_ml_kem.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/keyrot_test.c | 175 |
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(ð_data.stat.kern_rcv), LOAD_RELAXED(ð_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(ð_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 *) ð_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(); |
