summaryrefslogtreecommitdiff
path: root/src/irmd
diff options
context:
space:
mode:
Diffstat (limited to 'src/irmd')
-rw-r--r--src/irmd/CMakeLists.txt122
-rw-r--r--src/irmd/config.h.in70
-rw-r--r--src/irmd/configfile.c334
-rw-r--r--src/irmd/configfile.h2
-rw-r--r--src/irmd/ipcp.c79
-rw-r--r--src/irmd/ipcp.h5
-rw-r--r--src/irmd/irmd.h4
-rw-r--r--src/irmd/main.c2033
-rw-r--r--src/irmd/oap.h84
-rw-r--r--src/irmd/oap/auth.c558
-rw-r--r--src/irmd/oap/auth.h68
-rw-r--r--src/irmd/oap/cli.c672
-rw-r--r--src/irmd/oap/hdr.c755
-rw-r--r--src/irmd/oap/hdr.h195
-rw-r--r--src/irmd/oap/internal.h119
-rw-r--r--src/irmd/oap/io.c158
-rw-r--r--src/irmd/oap/io.h40
-rw-r--r--src/irmd/oap/srv.c582
-rw-r--r--src/irmd/oap/tests/CMakeLists.txt64
-rw-r--r--src/irmd/oap/tests/common.c619
-rw-r--r--src/irmd/oap/tests/common.h113
-rw-r--r--src/irmd/oap/tests/oap_test.c1926
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c491
-rw-r--r--src/irmd/reg/CMakeLists.txt7
-rw-r--r--src/irmd/reg/flow.c65
-rw-r--r--src/irmd/reg/flow.h48
-rw-r--r--src/irmd/reg/ipcp.c9
-rw-r--r--src/irmd/reg/ipcp.h2
-rw-r--r--src/irmd/reg/name.c81
-rw-r--r--src/irmd/reg/name.h17
-rw-r--r--src/irmd/reg/pool.c97
-rw-r--r--src/irmd/reg/pool.h48
-rw-r--r--src/irmd/reg/proc.c38
-rw-r--r--src/irmd/reg/proc.h11
-rw-r--r--src/irmd/reg/prog.c24
-rw-r--r--src/irmd/reg/prog.h5
-rw-r--r--src/irmd/reg/reg.c1331
-rw-r--r--src/irmd/reg/reg.h115
-rw-r--r--src/irmd/reg/tests/CMakeLists.txt34
-rw-r--r--src/irmd/reg/tests/flow_test.c113
-rw-r--r--src/irmd/reg/tests/ipcp_test.c13
-rw-r--r--src/irmd/reg/tests/name_test.c96
-rw-r--r--src/irmd/reg/tests/proc_test.c48
-rw-r--r--src/irmd/reg/tests/prog_test.c28
-rw-r--r--src/irmd/reg/tests/reg_test.c1055
-rw-r--r--src/irmd/tests/CMakeLists.txt19
46 files changed, 10994 insertions, 1403 deletions
diff --git a/src/irmd/CMakeLists.txt b/src/irmd/CMakeLists.txt
index c9c2e553..5aa457ff 100644
--- a/src/irmd/CMakeLists.txt
+++ b/src/irmd/CMakeLists.txt
@@ -1,95 +1,67 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
+# IRMd (IPC Resource Manager daemon) build configuration
+# Configuration options are in cmake/config/global.cmake and cmake/config/irmd.cmake
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-find_library(LIBTOML_LIBRARIES toml QUIET)
-if (LIBTOML_LIBRARIES)
- set(DISABLE_CONFIGFILE FALSE CACHE BOOL
- "Disable configuration file support")
- if (NOT DISABLE_CONFIGFILE)
- set(OUROBOROS_CONFIG_DIR /etc/ouroboros/ CACHE STRING
- "Configuration directory")
- set(OUROBOROS_CONFIG_FILE irmd.conf CACHE STRING
- "Name of the IRMd configuration file")
- set(HAVE_TOML TRUE)
- message(STATUS "Found TOML C99 library: " ${LIBTOML_LIBRARIES})
- message(STATUS "Configuration file support enabled")
- message(STATUS "Configuration directory: ${OUROBOROS_CONFIG_DIR}")
- set(INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}")
- configure_file("${CMAKE_SOURCE_DIR}/irmd.conf.in"
- "${CMAKE_BINARY_DIR}/irmd.conf.example" @ONLY)
- install(FILES "${CMAKE_BINARY_DIR}/irmd.conf.example"
- DESTINATION "${OUROBOROS_CONFIG_DIR}")
- unset(INSTALL_DIR)
- mark_as_advanced(LIBTOML_LIBRARIES)
- else ()
- message(STATUS "Configuration file support disabled by user")
- unset(OUROBOROS_CONFIG_FILE CACHE)
- unset(OUROBOROS_CONFIG_DIR CACHE)
- set(HAVE_TOML FALSE)
- endif ()
-else ()
- message(STATUS "Install tomlc99 for config file support")
- message(STATUS " https://github.com/cktan/tomlc99")
- set(LIBTOML_LIBRARIES "")
- unset(DISABLE_CONFIGFILE CACHE)
- unset(HAVE_TOML)
-endif ()
-
-set(IRMD_REQ_ARR_TIMEOUT 1000 CACHE STRING
- "Timeout for an application to respond to a new flow (ms)")
-
-set(BOOTSTRAP_TIMEOUT 5000 CACHE STRING
- "Timeout for an IPCP to bootstrap (ms)")
-set(ENROLL_TIMEOUT 20000 CACHE STRING
- "Timeout for an IPCP to enroll (ms)")
-set(REG_TIMEOUT 20000 CACHE STRING
- "Timeout for registering a name (ms)")
-set(QUERY_TIMEOUT 20000 CACHE STRING
- "Timeout to query a name with an IPCP (ms)")
-set(CONNECT_TIMEOUT 20000 CACHE STRING
- "Timeout to connect an IPCP to another IPCP (ms)")
-set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
- "Timeout for a flow allocation response (ms)")
-set(IRMD_MIN_THREADS 8 CACHE STRING
- "Minimum number of worker threads in the IRMd")
-set(IRMD_ADD_THREADS 8 CACHE STRING
- "Number of extra threads to start when the IRMD faces thread starvation")
-set(IRMD_PKILL_TIMEOUT 30 CACHE STRING
- "Number of seconds to wait before sending SIGKILL to subprocesses on exit")
-set(IRMD_KILL_ALL_PROCESSES TRUE CACHE BOOL
- "Kill all processes on exit")
+# Generate and install configuration files if TOML support available
+# HAVE_TOML is set in cmake/dependencies/irmd/libtoml.cmake
+if(HAVE_TOML)
+ set(INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}")
+ configure_file("${CMAKE_SOURCE_DIR}/irmd.conf.in"
+ "${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example" @ONLY)
+ configure_file("${CMAKE_SOURCE_DIR}/sec.conf.in"
+ "${CMAKE_BINARY_DIR}/sec.conf.example" @ONLY)
+ install(FILES "${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example"
+ DESTINATION "${OUROBOROS_CONFIG_DIR}")
+ install(FILES "${CMAKE_BINARY_DIR}/sec.conf.example"
+ DESTINATION "${OUROBOROS_CONFIG_DIR}")
+ install(CODE "
+ if(NOT EXISTS \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\")
+ file(WRITE \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\" \"\")
+ endif()
+ ")
+ unset(INSTALL_DIR)
+endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/config.h" @ONLY)
-set(SOURCE_FILES
- # Add source files here
+set(IRMD_SOURCES
ipcp.c
configfile.c
main.c
+ oap/io.c
+ oap/hdr.c
+ oap/auth.c
+ oap/srv.c
+ oap/cli.c
reg/flow.c
reg/ipcp.c
+ reg/pool.c
reg/proc.c
reg/prog.c
reg/name.c
reg/reg.c
- )
+)
+
+add_executable(irmd ${IRMD_SOURCES})
+
+add_dependencies(irmd version_header)
-add_executable (irmd ${SOURCE_FILES})
+target_include_directories(irmd PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include)
-target_link_libraries (irmd LINK_PUBLIC ouroboros-common
- ${LIBTOML_LIBRARIES})
+target_link_libraries(irmd PRIVATE ouroboros-common)
+if(HAVE_TOML)
+ target_link_libraries(irmd PRIVATE toml::toml)
+endif()
-include(AddCompileFlags)
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(irmd -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+ouroboros_target_debug_definitions(irmd)
install(TARGETS irmd RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
-# Enable once irmd has tests
-#add_subdirectory(tests)
-add_subdirectory(reg)
+if(BUILD_TESTS)
+ add_subdirectory(oap/tests)
+ add_subdirectory(reg/tests)
+endif()
diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in
index fa1156b9..e14cff75 100644
--- a/src/irmd/config.h.in
+++ b/src/irmd/config.h.in
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* Configuration for the IPC Resource Manager
*
@@ -21,7 +21,8 @@
*/
-#define IPCP_UDP_EXEC "@IPCP_UDP_TARGET@"
+#define IPCP_UDP4_EXEC "@IPCP_UDP4_TARGET@"
+#define IPCP_UDP6_EXEC "@IPCP_UDP6_TARGET@"
#define IPCP_ETH_LLC_EXEC "@IPCP_ETH_LLC_TARGET@"
#define IPCP_ETH_DIX_EXEC "@IPCP_ETH_DIX_TARGET@"
#define IPCP_UNICAST_EXEC "@IPCP_UNICAST_TARGET@"
@@ -40,6 +41,11 @@
#define FLOW_ALLOC_TIMEOUT @FLOW_ALLOC_TIMEOUT@
#define FLOW_DEALLOC_TIMEOUT @FLOW_DEALLOC_TIMEOUT@
+#define OAP_REPLAY_TIMER @OAP_REPLAY_TIMER@
+#define OAP_REPLAY_MAX @OAP_REPLAY_MAX@
+#define OAP_REKEY_TIMER @OAP_REKEY_TIMER@
+#cmakedefine01 OAP_CLIENT_AUTH_DEFAULT
+
#define BOOTSTRAP_TIMEOUT @BOOTSTRAP_TIMEOUT@
#define ENROLL_TIMEOUT @ENROLL_TIMEOUT@
#define REG_TIMEOUT @REG_TIMEOUT@
@@ -50,6 +56,7 @@
#define IRMD_MIN_THREADS @IRMD_MIN_THREADS@
#define IRMD_ADD_THREADS @IRMD_ADD_THREADS@
+#define SSM_PID_GSPP 0
#cmakedefine HAVE_FUSE
#ifdef HAVE_FUSE
@@ -62,36 +69,39 @@
#define OUROBOROS_CONFIG_FILE "@OUROBOROS_CONFIG_FILE@"
#endif
+#define OUROBOROS_SECURITY_DIR "@OUROBOROS_SECURITY_DIR@"
+#define OUROBOROS_CA_CRT_DIR "@OUROBOROS_CA_CRT_DIR@"
+#define OUROBOROS_SRV_CRT_DIR "@OUROBOROS_SRV_CRT_DIR@"
+#define OUROBOROS_CLI_CRT_DIR "@OUROBOROS_CLI_CRT_DIR@"
+#define OUROBOROS_CHAIN_DIR "@OUROBOROS_UNTRUSTED_DIR@"
+
#define IRMD_PKILL_TIMEOUT @IRMD_PKILL_TIMEOUT@
+#cmakedefine DISABLE_DIRECT_IPC
#cmakedefine IRMD_KILL_ALL_PROCESSES
#cmakedefine HAVE_LIBGCRYPT
+#cmakedefine HAVE_OPENSSL
+#ifdef HAVE_OPENSSL
+#cmakedefine HAVE_OPENSSL_ML_KEM
+#cmakedefine HAVE_OPENSSL_ML_DSA
+#endif
+#define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@
+#ifdef CONFIG_OUROBOROS_DEBUG
+#cmakedefine DEBUG_PROTO_OAP
+#endif
+
+#define _B ""
+#define _G ""
+#define RST ""
-#define O7S_ASCII_ART \
-"\n" \
-" ▄▄█████▄▄▄\n" \
-" ▄█▀▀ ▀▀███▄ " \
-"█\n" \
-" ██ ▄▄▄ ▄███▄ " \
-"▄ ▄ ▄ ▄▄" \
-" ▄▄ █ ▄▄ " \
-" ▄▄ ▄ ▄▄ " \
-"▄▄ ▄▄\n" \
-" ██ █ █ " \
-"[38;5;4m█████ █ █ " \
-"█▀ ▀ █ █" \
-" █▀ █ █ " \
-"█ █▀ ▀ █" \
-" █ ▀▄ ▀\n" \
-" ██ ▀▄▄▄▀ ▀█▀ " \
-"█ █ █ " \
-"█ █ █▄ █ " \
-"█ █ █ █" \
-" █ ▄ ▀▄\n" \
-" █▄ █ ▀▀▀" \
-" ▀ ▀ ▀▀" \
-" ▀ ▀▀ ▀▀ " \
-"▀ ▀▀ ▀▀\n" \
-" ▀█▄▄▄▄▄▄▄▄▀\n" \
-" ▀▀▀▀▀▀\n" \
-"\n"
+#define O7S_ASCII_ART \
+RST "\n" \
+_B " ▄▄█████▄▄▄ \n" \
+_B " ▄█▀▀ ▀▀███▄ " _G " █ \n" \
+_B " ██ ▄▄▄ ▄███▄ " _G "▄ ▄ ▄ ▄ ▄▄ █▄▄ ▄▄ ▄ ▄ ▄▄ ▄▄ \n" \
+_B " ██ █ █ █████ " _G "█ █ █▀ ▀ █ █ █ █ █ █ █▀ ▀ █ █ ▀▄ ▀\n" \
+_B " ██ ▀▄▄▄▀ ▀█▀ " _G "█ █ █ █ █ █ █ █ █ █ █ █ ▄ ▀▄\n" \
+_B " █▄ █ " _G " ▀▀ ▀ ▀ ▀▀ ▀▀▀ ▀▀ ▀ ▀▀ ▀▀ \n" \
+_B " ▀█▄▄▄▄▄▄▄▄▀ \n" \
+_B " ▀▀▀▀▀▀ \n" \
+RST "\n"
diff --git a/src/irmd/configfile.c b/src/irmd/configfile.c
index 688c4ade..35cf4292 100644
--- a/src/irmd/configfile.c
+++ b/src/irmd/configfile.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager / Configuration from file
*
@@ -47,8 +47,12 @@
#include <string.h>
#include <toml.h>
#include <arpa/inet.h>
+#ifdef __FreeBSD__
+#include <sys/socket.h>
+#endif
#define ERRBUFSZ 200
+#define DATUMSZ 256
static int toml_hash(toml_table_t * table,
struct layer_info * info)
@@ -157,14 +161,66 @@ static int toml_eth_dix(toml_table_t * table,
return 0;
}
-static int toml_udp(toml_table_t * table,
- struct ipcp_config * conf)
+static int toml_udp4(toml_table_t * table,
+ struct ipcp_config * conf)
+{
+ struct udp4_config * udp4;
+ toml_datum_t ip;
+ toml_datum_t port;
+ toml_datum_t dns;
+
+ *conf = udp4_default_conf;
+ udp4 = &conf->udp4;
+
+ ip = toml_string_in(table, "ip");
+ if (!ip.ok) {
+ log_err("No IP address specified!");
+ goto fail_ip;
+ }
+
+ if (inet_pton (AF_INET, ip.u.s, &udp4->ip_addr.s_addr) != 1) {
+ log_err("Failed to parse IPv4 address %s.", ip.u.s);
+ goto fail_addr;
+ }
+
+ port = toml_int_in(table, "port");
+ if (port.ok)
+ udp4->port = port.u.i;
+
+ dns = toml_string_in(table, "dns");
+ if (dns.ok) {
+ if (inet_pton(AF_INET, dns.u.s, &udp4->dns_addr.s_addr) < 0) {
+ log_err("Failed to parse DNS address %s.", ip.u.s);
+ goto fail_dns;
+ }
+
+ free(dns.u.s);
+ }
+
+ free(ip.u.s);
+
+ return 0;
+
+ fail_dns:
+ free(dns.u.s);
+ fail_addr:
+ free(ip.u.s);
+ fail_ip:
+ return -1;
+}
+
+static int toml_udp6(toml_table_t * table,
+ struct ipcp_config * conf)
{
+ struct in6_addr ip6;
+ struct in6_addr dns6;
toml_datum_t ip;
toml_datum_t port;
toml_datum_t dns;
- *conf = udp_default_conf;
+ *conf = udp6_default_conf;
+ ip6 = conf->udp6.ip_addr;
+ dns6 = conf->udp6.dns_addr;
ip = toml_string_in(table, "ip");
if (!ip.ok) {
@@ -172,18 +228,18 @@ static int toml_udp(toml_table_t * table,
goto fail_ip;
}
- if (inet_pton (AF_INET, ip.u.s, &conf->udp.ip_addr) != 1) {
+ if (inet_pton (AF_INET6, ip.u.s, &ip6.s6_addr) != 1) {
log_err("Failed to parse IPv4 address %s.", ip.u.s);
goto fail_addr;
}
port = toml_int_in(table, "port");
if (port.ok)
- conf->udp.port = port.u.i;
+ conf->udp6.port = port.u.i;
dns = toml_string_in(table, "dns");
if (dns.ok) {
- if (inet_pton(AF_INET, dns.u.s, &conf->udp.dns_addr) < 0) {
+ if (inet_pton(AF_INET6, dns.u.s, &dns6.s6_addr) < 0) {
log_err("Failed to parse DNS address %s.", ip.u.s);
goto fail_dns;
}
@@ -214,26 +270,160 @@ static int toml_broadcast(toml_table_t * table,
return 0;
}
+#define BETWEEN(a, b, c) ((a) >= (b) && (a) <= (c))
+#define DHT(conf, x) (conf)->dht.params.x
+static int toml_dir(toml_table_t * table,
+ struct dir_config * conf)
+{
+ toml_datum_t dir;
+ toml_datum_t alpha;
+ toml_datum_t t_expire;
+ toml_datum_t t_refresh;
+ toml_datum_t t_replicate;
+ toml_datum_t k;
+
+ dir = toml_string_in(table, "directory");
+ if (dir.ok) {
+ log_dbg("Found directory type: %s", dir.u.s);
+ if (strlen(dir.u.s) > DATUMSZ) {
+ log_err("Directory name too long: %s", dir.u.s);
+ free(dir.u.s);
+ return -1;
+ }
+ if (strcmp(dir.u.s, "DHT") == 0)
+ conf->pol = DIR_DHT;
+ else if (strcmp(dir.u.s, "dht") == 0)
+ conf->pol = DIR_DHT;
+ else {
+ log_err("Unknown directory type: %s", dir.u.s);
+ free(dir.u.s);
+ return -EINVAL;
+ }
+ free(dir.u.s);
+ }
+
+ switch(conf->pol) {
+ case DIR_DHT:
+ log_info("Using DHT directory policy.");
+ alpha = toml_int_in(table, "dht_alpha");
+ if (alpha.ok) {
+ if (!BETWEEN(alpha.u.i,
+ DHT_ALPHA_MIN, DHT_ALPHA_MAX)) {
+ log_err("Invalid alpha value: %ld",
+ (long) alpha.u.i);
+ return -EINVAL;
+ }
+ DHT(conf, alpha) = alpha.u.i;
+ }
+ t_expire = toml_int_in(table, "dht_t_expire");
+ if (t_expire.ok) {
+ if (!BETWEEN(t_expire.u.i,
+ DHT_T_EXPIRE_MIN, DHT_T_EXPIRE_MAX)) {
+ log_err("Invalid expire time: %ld",
+ (long) t_expire.u.i);
+ return -EINVAL;
+ }
+ DHT(conf, t_expire) = t_expire.u.i;
+ }
+ t_refresh = toml_int_in(table, "dht_t_refresh");
+ if (t_refresh.ok) {
+ if (!BETWEEN(t_refresh.u.i,
+ DHT_T_REFRESH_MIN, DHT_T_REFRESH_MAX)) {
+ log_err("Invalid refresh time: %ld",
+ (long) t_refresh.u.i);
+ return -EINVAL;
+ }
+ DHT(conf, t_refresh) = t_refresh.u.i;
+ }
+ t_replicate = toml_int_in(table, "dht_t_replicate");
+ if (t_replicate.ok) {
+ if (!BETWEEN(t_replicate.u.i,
+ DHT_T_REPLICATE_MIN, DHT_T_REPLICATE_MAX)) {
+ log_err("Invalid replication time: %ld",
+ (long) t_replicate.u.i);
+ return -EINVAL;
+ }
+ DHT(conf, t_replicate) = t_replicate.u.i;
+ }
+ k = toml_int_in(table, "dht_k");
+ if (k.ok) {
+ if (!BETWEEN(k.u.i, DHT_K_MIN, DHT_K_MAX)) {
+ log_err("Invalid replication factor: %ld",
+ (long) k.u.i);
+ return -EINVAL;
+ }
+ DHT(conf, k) = k.u.i;
+ }
+ break;
+ default:
+ assert(false);
+ break;
+ }
+
+ return 0;
+}
+
static int toml_routing(toml_table_t * table,
struct dt_config * conf)
{
toml_datum_t routing;
+ toml_datum_t t_recalc;
+ toml_datum_t t_update;
+ toml_datum_t t_timeo;
routing = toml_string_in(table, "routing");
if (routing.ok) {
- if (strcmp(routing.u.s, "link-state") == 0)
- conf->routing_type = ROUTING_LINK_STATE;
- else if (strcmp(routing.u.s, "lfa") == 0)
- conf->routing_type = ROUTING_LINK_STATE_LFA;
- else if (strcmp(routing.u.s, "ecmp") == 0)
- conf->routing_type = ROUTING_LINK_STATE_ECMP;
- else
- conf->routing_type = ROUTING_INVALID;
+ if (strcmp(routing.u.s, "link-state") == 0) {
+ conf->routing.pol = ROUTING_LINK_STATE;
+ conf->routing.ls.pol = LS_SIMPLE;
+ } else if (strcmp(routing.u.s, "lfa") == 0) {
+ conf->routing.pol = ROUTING_LINK_STATE;
+ conf->routing.ls.pol = LS_LFA;
+ } else if (strcmp(routing.u.s, "ecmp") == 0) {
+ conf->routing.pol = ROUTING_LINK_STATE;
+ conf->routing.ls.pol = LS_ECMP;
+ } else {
+ conf->routing.pol = ROUTING_INVALID;
+ return -EINVAL;
+ }
free(routing.u.s);
}
- if (conf->routing_type == ROUTING_INVALID)
- return -1;
+ switch (conf->routing.pol) {
+ case ROUTING_LINK_STATE:
+ log_info("Using Link State routing policy.");
+ t_recalc = toml_int_in(table, "ls_t_recalc");
+ if (t_recalc.ok) {
+ if (t_recalc.u.i < 1) {
+ log_err("Invalid ls_t_recalc value: %ld",
+ (long) t_recalc.u.i);
+ return -EINVAL;
+ }
+ conf->routing.ls.t_recalc = t_recalc.u.i;
+ }
+ t_update = toml_int_in(table, "ls_t_update");
+ if (t_update.ok) {
+ if (t_update.u.i < 1) {
+ log_err("Invalid ls_t_update value: %ld",
+ (long) t_update.u.i);
+ return -EINVAL;
+ }
+ conf->routing.ls.t_update = t_update.u.i;
+ }
+ t_timeo = toml_int_in(table, "ls_t_timeo");
+ if (t_timeo.ok) {
+ if (t_timeo.u.i < 1) {
+ log_err("Invalid ls_t_timeo value: %ld",
+ (long) t_timeo.u.i);
+ return -EINVAL;
+ }
+ conf->routing.ls.t_timeo = t_timeo.u.i;
+ }
+ break;
+ default:
+ log_err("Invalid routing policy: %d", conf->routing.pol);
+ return -EINVAL;
+ }
return 0;
}
@@ -311,12 +501,12 @@ static int toml_dt(toml_table_t * table,
static int toml_unicast(toml_table_t * table,
struct ipcp_config * conf)
{
-
-
*conf = uni_default_conf;
- if (toml_hash(table, &conf->layer_info) < 0)
+ if (toml_dir(table, &conf->unicast.dir) < 0) {
+ log_err("Invalid directory configuration.");
return -1;
+ }
if (toml_dt(table, &conf->unicast.dt) < 0) {
log_err("Invalid DT configuration.");
@@ -333,6 +523,7 @@ static int toml_unicast(toml_table_t * table,
return -1;
}
+
return 0;
}
@@ -504,8 +695,11 @@ static int toml_ipcp(toml_table_t * table,
case IPCP_ETH_LLC:
ret = toml_eth_llc(table, conf);
break;
- case IPCP_UDP:
- ret = toml_udp(table, conf);
+ case IPCP_UDP4:
+ ret = toml_udp4(table, conf);
+ break;
+ case IPCP_UDP6:
+ ret = toml_udp6(table, conf);
break;
case IPCP_BROADCAST:
ret = toml_broadcast(table, conf);
@@ -563,7 +757,7 @@ static int toml_ipcp_list(toml_table_t * table,
}
info.type = type;
- strcpy(info.name,key);
+ strcpy(info.name, key);
conf.type = type;
ret = toml_ipcp(toml_table_in(table, key), &info, &conf);
@@ -691,18 +885,63 @@ static int toml_prog_list(toml_array_t * progs,
return ret;
}
+static int cp_chk_path(char * buf,
+ char * path)
+{
+ char * rp;
+
+ assert(path != NULL);
+
+ rp = realpath(path, NULL);
+ if (rp == NULL) {
+ log_err("Failed to check path %s: %s.", path, strerror(errno));
+ goto fail_rp;
+ }
+
+ if (strlen(rp) > NAME_PATH_SIZE) {
+ log_err("File path too long: %s.", rp);
+ goto fail_len;
+ }
+
+ strcpy(buf, rp);
+ free(rp);
+ free(path);
+
+ return 0;
+
+ fail_len:
+ free(rp);
+ fail_rp:
+ free(path);
+ return -1;
+}
+
static int toml_name(toml_table_t * table,
const char * name)
{
- toml_array_t * progs;
- toml_array_t * args;
- toml_datum_t lb;
+ toml_array_t * progs;
+ toml_array_t * args;
+ toml_datum_t lb;
+ toml_datum_t ssec;
+ toml_datum_t scrt;
+ toml_datum_t skey;
+ toml_datum_t csec;
+ toml_datum_t ccrt;
+ toml_datum_t ckey;
+
struct name_info info = {
.pol_lb = LB_SPILL
};
log_dbg("Found service name %s in configuration file.", name);
+ if (strlen(name) > NAME_SIZE) {
+ log_err("Name too long: %s", name);
+ return -1;
+ }
+
+ strcpy(info.name, name);
+
lb = toml_string_in(table, "lb");
if (lb.ok) {
if (strcmp(lb.u.s, "spill") == 0)
@@ -718,8 +957,29 @@ static int toml_name(toml_table_t * table,
log_err("Invalid load-balancing policy for %s.", name);
return -1;
}
+ ssec = toml_string_in(table, "server_sec_file");
+ if (ssec.ok && cp_chk_path(info.s.sec, ssec.u.s) < 0)
+ return -1;
- strcpy(info.name, name);
+ scrt = toml_string_in(table, "server_crt_file");
+ if (scrt.ok && cp_chk_path(info.s.crt, scrt.u.s) < 0)
+ return -1;
+
+ skey = toml_string_in(table, "server_key_file");
+ if (skey.ok && cp_chk_path(info.s.key, skey.u.s) < 0)
+ return -1;
+
+ csec = toml_string_in(table, "client_sec_file");
+ if (csec.ok && cp_chk_path(info.c.sec, csec.u.s) < 0)
+ return -1;
+
+ ccrt = toml_string_in(table, "client_crt_file");
+ if (ccrt.ok && cp_chk_path(info.c.crt, ccrt.u.s) < 0)
+ return -1;
+
+ ckey = toml_string_in(table, "client_key_file");
+ if (ckey.ok && cp_chk_path(info.c.key, ckey.u.s) < 0)
+ return -1;
if (name_create(&info) < 0) {
log_err("Failed to create name %s.", name);
@@ -762,23 +1022,24 @@ static int toml_toplevel(toml_table_t * table,
toml_table_t * subtable;
subtable = toml_table_in(table, key);
-
- if (strcmp(key, "local") == 0)
+ if (strcmp(key, "name") == 0)
+ return toml_name_list(subtable);
+ else if (strcmp(key, "local") == 0)
return toml_ipcp_list(subtable, IPCP_LOCAL);
else if (strcmp(key, "eth-dix") == 0)
return toml_ipcp_list(subtable, IPCP_ETH_DIX);
else if (strcmp(key, "eth-llc") == 0)
return toml_ipcp_list(subtable, IPCP_ETH_LLC);
- else if (strcmp(key, "udp") == 0)
- return toml_ipcp_list(subtable, IPCP_UDP);
+ else if (strcmp(key, "udp4") == 0)
+ return toml_ipcp_list(subtable, IPCP_UDP4);
+ else if (strcmp(key, "udp6") == 0)
+ return toml_ipcp_list(subtable, IPCP_UDP6);
else if (strcmp(key, "broadcast") == 0)
return toml_ipcp_list(subtable, IPCP_BROADCAST);
else if (strcmp(key, "unicast") == 0)
return toml_ipcp_list(subtable, IPCP_UNICAST);
- else if (strcmp(key, "name") == 0)
- return toml_name_list(subtable);
-
- log_err("Unkown toplevel key: %s.", key);
+ else
+ log_err("Unkown toplevel key: %s.", key);
return -1;
}
@@ -838,7 +1099,8 @@ int irm_configure(const char * path)
rp = realpath(path, NULL);
if (rp == NULL) {
- log_err("Failed to resolve path for %s", path);
+ log_err("Failed to check path for %s: %s.",
+ path, strerror(errno));
goto fail_resolve;
}
diff --git a/src/irmd/configfile.h b/src/irmd/configfile.h
index 3ccf53fd..62f522a4 100644
--- a/src/irmd/configfile.h
+++ b/src/irmd/configfile.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager / Configuration from file
*
diff --git a/src/irmd/ipcp.c b/src/irmd/ipcp.c
index 5a9a79d3..7eccfc80 100644
--- a/src/irmd/ipcp.c
+++ b/src/irmd/ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API to instruct IPCPs
*
@@ -83,28 +83,30 @@ ipcp_msg_t * send_recv_ipcp_msg(pid_t pid,
{
int sockfd;
uint8_t buf[SOCK_BUF_SIZE];
- char * sock_path;
+ char * spath;
ssize_t len;
- ipcp_msg_t * recv_msg;
struct timeval tv;
struct timespec tic;
struct timespec toc;
- bool dealloc = false;
+ bool may_fail = false;
if (kill(pid, 0) < 0)
return NULL;
- sock_path = ipcp_sock_path(pid);
- if (sock_path == NULL)
+ spath = sock_path(pid, IPCP_SOCK_PATH_PREFIX);
+ if (spath == NULL) {
+ log_err("Failed to get IPCP socket path for pid %d.", pid);
return NULL;
+ }
- sockfd = client_socket_open(sock_path);
+ sockfd = client_socket_open(spath);
if (sockfd < 0) {
- free(sock_path);
+ log_err("Failed to open client socket at %s.", spath);
+ free(spath);
return NULL;
}
- free(sock_path);
+ free(spath);
len = ipcp_msg__get_packed_size(msg);
if (len == 0 || len >= SOCK_BUF_SIZE) {
@@ -127,6 +129,7 @@ ipcp_msg_t * send_recv_ipcp_msg(pid_t pid,
tv.tv_usec = (REG_TIMEOUT % 1000) * 1000;
break;
case IPCP_MSG_CODE__IPCP_QUERY:
+ may_fail = true; /* name not always in Layer */
tv.tv_sec = QUERY_TIMEOUT / 1000;
tv.tv_usec = (QUERY_TIMEOUT % 1000) * 1000;
break;
@@ -139,7 +142,7 @@ ipcp_msg_t * send_recv_ipcp_msg(pid_t pid,
tv.tv_usec = (FLOW_ALLOC_TIMEOUT % 1000) * 1000;
break;
case IPCP_MSG_CODE__IPCP_FLOW_DEALLOC:
- dealloc = true;
+ may_fail = true;
tv.tv_sec = 0; /* FIX DEALLOC: don't wait for dealloc */
tv.tv_usec = 500;
break;
@@ -167,17 +170,15 @@ ipcp_msg_t * send_recv_ipcp_msg(pid_t pid,
pthread_cleanup_pop(true); /* close socket */
if (len > 0)
- recv_msg = ipcp_msg__unpack(NULL, len, buf);
- else {
- if (errno == EAGAIN && !dealloc) {
- int diff = ts_diff_ms(&tic, &toc);
- log_warn("IPCP %s timed out after %d ms.",
- str_ipcp_cmd(msg->code), diff);
- }
- return NULL;
+ return ipcp_msg__unpack(NULL, len, buf);
+
+ if (errno == EAGAIN && !may_fail) {
+ int diff = ts_diff_ms(&toc, &tic);
+ log_warn("IPCP %s timed out after %d ms.",
+ str_ipcp_cmd(msg->code), diff);
}
- return recv_msg;
+ return NULL;
}
int ipcp_bootstrap(pid_t pid,
@@ -420,6 +421,8 @@ int ipcp_flow_join(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.has_hash = true;
msg.hash.data = (uint8_t *) dst.data;
msg.hash.len = dst.len;
@@ -441,6 +444,38 @@ int ipcp_flow_join(const struct flow_info * flow,
return ret;
}
+int ipcp_flow_update(const struct flow_info * flow,
+ const buffer_t data)
+{
+ ipcp_msg_t msg = IPCP_MSG__INIT;
+ ipcp_msg_t * recv_msg;
+ int ret;
+
+ msg.code = IPCP_MSG_CODE__IPCP_FLOW_UPDATE;
+ msg.has_flow_id = true;
+ msg.flow_id = flow->id;
+ msg.has_pk = true;
+ msg.pk.data = data.data;
+ msg.pk.len = data.len;
+
+ recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
+ if (recv_msg == NULL) {
+ log_err("Did not receive message.");
+ return -EIPCP;
+ }
+
+ if (!recv_msg->has_result) {
+ log_err("Message has no result");
+ ipcp_msg__free_unpacked(recv_msg, NULL);
+ return -EIPCP;
+ }
+
+ ret = recv_msg->result;
+ ipcp_msg__free_unpacked(recv_msg, NULL);
+
+ return ret;
+}
+
int ipcp_flow_alloc(const struct flow_info * flow,
const buffer_t dst,
const buffer_t data)
@@ -454,6 +489,8 @@ int ipcp_flow_alloc(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.qosspec = qos_spec_s_to_msg(&flow->qs);
msg.has_hash = true;
msg.hash.data = (uint8_t *) dst.data;
@@ -494,9 +531,11 @@ int ipcp_flow_alloc_resp(const struct flow_info * flow,
msg.flow_id = flow->id;
msg.has_pid = true;
msg.pid = flow->n_pid;
+ msg.has_uid = true;
+ msg.uid = flow->uid;
msg.has_response = true;
msg.response = response;
- msg.has_pk = true;
+ msg.has_pk = response == 0;
msg.pk.data = data.data;
msg.pk.len = data.len;
diff --git a/src/irmd/ipcp.h b/src/irmd/ipcp.h
index b7413cd2..8d06623c 100644
--- a/src/irmd/ipcp.h
+++ b/src/irmd/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The API for the IRM to instruct IPCPs
*
@@ -68,4 +68,7 @@ int ipcp_flow_dealloc(pid_t pid,
int flow_id,
time_t timeo);
+int ipcp_flow_update(const struct flow_info * flow,
+ const buffer_t data);
+
#endif /* OUROBOROS_IRMD_IPCP_H */
diff --git a/src/irmd/irmd.h b/src/irmd/irmd.h
index cf8f6953..f88378ad 100644
--- a/src/irmd/irmd.h
+++ b/src/irmd/irmd.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager
*
@@ -39,7 +39,7 @@ int connect_ipcp(pid_t pid,
const char * component,
qosspec_t qs);
-int name_create(const struct name_info * info);
+int name_create(struct name_info * info);
int name_reg(const char * name,
pid_t pid);
diff --git a/src/irmd/main.c b/src/irmd/main.c
index bc13fa7c..19be4ab9 100644
--- a/src/irmd/main.c
+++ b/src/irmd/main.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager
*
@@ -22,8 +22,10 @@
#if defined(__linux__) || defined(__CYGWIN__)
#define _DEFAULT_SOURCE
+#define _GNU_SOURCE
#else
-#define _POSIX_C_SOURCE 200809L
+#define _DEFAULT_SOURCE
+#define _BSD_SOURCE
#endif
#include "config.h"
@@ -34,14 +36,17 @@
#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
#include <ouroboros/flow.h>
+#include <ouroboros/fqueue.h>
#include <ouroboros/hash.h>
#include <ouroboros/irm.h>
#include <ouroboros/list.h>
#include <ouroboros/lockfile.h>
#include <ouroboros/logs.h>
+#include <ouroboros/protobuf.h>
#include <ouroboros/pthread.h>
+#include <ouroboros/random.h>
#include <ouroboros/rib.h>
-#include <ouroboros/shm_rdrbuff.h>
+#include <ouroboros/ssm_pool.h>
#include <ouroboros/sockets.h>
#include <ouroboros/time.h>
#include <ouroboros/tpm.h>
@@ -50,18 +55,26 @@
#include "irmd.h"
#include "ipcp.h"
+#include "oap.h"
#include "reg/reg.h"
#include "configfile.h"
-#include <sys/socket.h>
-#include <sys/un.h>
+#include <dirent.h>
+#include <grp.h>
+#include <pwd.h>
#include <signal.h>
+#include <spawn.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
+#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
-#include <spawn.h>
+#include <sys/un.h>
+#ifdef __APPLE__
+#include <sys/types.h>
+#include <unistd.h>
+#endif
#ifdef HAVE_LIBGCRYPT
#include <gcrypt.h>
@@ -71,11 +84,18 @@
#define SHM_SAN_HOLDOFF 1000 /* ms */
#define IPCP_HASH_LEN(p) hash_len((p)->dir_hash_algo)
#define BIND_TIMEOUT 10 /* ms */
+#define TIMESYNC_SLACK 100 /* ms */
+#define OAP_SEEN_TIMER 20 /* s */
#define DEALLOC_TIME 300 /* s */
-#define MSGBUFSZ 2048
+#define REKEY_BATCH 64 /* flows re-keyed per timer pass */
+#define REKEY_RESP_TIMEO 20 /* s; give-up on a re-key RESPONSE */
+#define DIRECT_MPL 20 /* ms */
+/* bytes; in-process, bounded only by PUP/GSPP. */
+#define DIRECT_MTU 65000
enum irm_state {
IRMD_NULL = 0,
+ IRMD_INIT,
IRMD_RUNNING,
IRMD_SHUTDOWN
};
@@ -88,13 +108,38 @@ struct cmd {
int fd;
};
+/* In-flight Tier-2 re-key, owned solely by the re-key worker thread. */
+struct rekey_ctx {
+ struct list_head next;
+
+ int flow_id;
+ void * ctx; /* OAP client ctx (opaque) */
+ struct timespec deadline; /* reap if no RESPONSE by then */
+};
+
+enum rekey_evt_type {
+ REKEY_INIT = 0, /* start an exchange for flow_id */
+ REKEY_REQ, /* a REQUEST arrived for flow_id */
+ REKEY_RESP, /* a RESPONSE arrived for flow_id */
+ REKEY_DIRECT /* in-process re-key, direct flow */
+};
+
+struct rekey_evt {
+ struct list_head next;
+
+ enum rekey_evt_type type;
+ int flow_id;
+ pid_t n_1_pid; /* INIT: flow's lower IPCP */
+ buffer_t buf; /* RESP: owned RESPONSE payload */
+};
+
struct {
bool log_stdout; /* log to stdout */
#ifdef HAVE_TOML
char * cfg_file; /* configuration file path */
#endif
struct lockfile * lf; /* single irmd per system */
- struct shm_rdrbuff * rdrb; /* rdrbuff for packets */
+ struct ssm_pool * gspp; /* pool for packets */
int sockfd; /* UNIX socket */
@@ -109,6 +154,13 @@ struct {
pthread_t irm_sanitize; /* clean up irmd resources */
pthread_t acceptor; /* accept new commands */
+
+ struct {
+ pthread_t worker; /* Tier-2 re-key orchestrator */
+ struct list_head inbox; /* re-key events for worker */
+ pthread_cond_t cond; /* inbox signal condvar */
+ pthread_mutex_t mtx; /* inbox lock */
+ } rk;
} irmd;
static enum irm_state irmd_get_state(void)
@@ -173,8 +225,11 @@ static pid_t spawn_ipcp(struct ipcp_info * info)
case IPCP_BROADCAST:
exec_name = IPCP_BROADCAST_EXEC;
break;
- case IPCP_UDP:
- exec_name = IPCP_UDP_EXEC;
+ case IPCP_UDP4:
+ exec_name = IPCP_UDP4_EXEC;
+ break;
+ case IPCP_UDP6:
+ exec_name = IPCP_UDP6_EXEC;
break;
case IPCP_ETH_LLC:
exec_name = IPCP_ETH_LLC_EXEC;
@@ -217,7 +272,7 @@ static pid_t spawn_ipcp(struct ipcp_info * info)
}
info->pid = pid;
- info->state = IPCP_BOOT;
+ info->state = IPCP_INIT;
return 0;
}
@@ -312,7 +367,7 @@ int bootstrap_ipcp(pid_t pid,
goto fail;
}
- if (conf->type == IPCP_UDP)
+ if (conf->type == IPCP_UDP4 || conf->type == IPCP_UDP6)
conf->layer_info.dir_hash_algo = (enum pol_dir_hash) HASH_MD5;
if (ipcp_bootstrap(pid, conf, &layer)) {
@@ -320,7 +375,7 @@ int bootstrap_ipcp(pid_t pid,
goto fail;
}
- info.state = IPCP_BOOTSTRAPPED;
+ info.state = IPCP_BOOT;
if (reg_set_layer_for_ipcp(&info, &layer) < 0) {
log_err("Failed to set layer info for IPCP.");
@@ -352,6 +407,8 @@ int enroll_ipcp(pid_t pid,
goto fail;
}
+ info.state = IPCP_BOOT;
+
if (reg_set_layer_for_ipcp(&info, &layer) < 0) {
log_err("Failed to set layer info for IPCP.");
goto fail;
@@ -425,6 +482,71 @@ static int disconnect_ipcp(pid_t pid,
return 0;
}
+static void name_update_sec_paths(struct name_info * info)
+{
+ char * srv_dir = OUROBOROS_SRV_CRT_DIR;
+ char * cli_dir = OUROBOROS_CLI_CRT_DIR;
+
+ assert(info != NULL);
+
+ if (strlen(info->s.sec) == 0)
+ sprintf(info->s.sec, "%s/%s/sec.conf", srv_dir, info->name);
+
+ if (strlen(info->s.crt) == 0)
+ sprintf(info->s.crt, "%s/%s/crt.pem", srv_dir, info->name);
+
+ if (strlen(info->s.key) == 0)
+ sprintf(info->s.key, "%s/%s/key.pem", srv_dir, info->name);
+
+ if (strlen(info->c.sec) == 0)
+ sprintf(info->c.sec, "%s/%s/sec.conf", cli_dir, info->name);
+
+ if (strlen(info->c.crt) == 0)
+ sprintf(info->c.crt, "%s/%s/crt.pem", cli_dir, info->name);
+
+ if (strlen(info->c.key) == 0)
+ sprintf(info->c.key, "%s/%s/key.pem", cli_dir, info->name);
+}
+
+int name_create(struct name_info * info)
+{
+ int ret;
+
+ assert(info != NULL);
+
+ name_update_sec_paths(info);
+
+ ret = reg_create_name(info);
+ if (ret == -EEXIST) {
+ log_info("Name %s already exists.", info->name);
+ return 0;
+ }
+
+ if (ret < 0) {
+ log_err("Failed to create name %s.", info->name);
+ return -1;
+ }
+
+ log_info("Created new name: %s.", info->name);
+
+ return 0;
+}
+
+static int name_destroy(const char * name)
+{
+
+ assert(name != NULL);
+
+ if (reg_destroy_name(name) < 0) {
+ log_err("Failed to destroy name %s.", name);
+ return -1;
+ }
+
+ log_info("Destroyed name: %s.", name);
+
+ return 0;
+}
+
int bind_program(char ** exec,
const char * name,
uint8_t flags)
@@ -448,10 +570,8 @@ int bind_program(char ** exec,
if (!reg_has_name(name)) {
ni.pol_lb = LB_SPILL;
strcpy(ni.name, name);
- if (reg_create_name(&ni) < 0) {
- log_err("Failed to create name %s.", name);
+ if (name_create(&ni) < 0)
goto fail_name;
- }
}
if (reg_bind_prog(name, exec, flags) < 0) {
@@ -497,10 +617,8 @@ int bind_process(pid_t pid,
if (!reg_has_name(name)) {
ni.pol_lb = LB_SPILL;
strcpy(ni.name, name);
- if (reg_create_name(&ni) < 0) {
- log_err("Failed to create name %s.", name);
+ if (name_create(&ni) < 0)
goto fail;
- }
}
if (reg_bind_proc(name, pid) < 0) {
@@ -529,18 +647,21 @@ static int unbind_program(const char * prog,
if (name == NULL) {
if (reg_destroy_prog(prog) < 0) {
log_err("Failed to unbind %s.", prog);
- return -1;
+ goto fail;
}
log_info("Program %s unbound.", prog);
} else {
if (reg_unbind_prog(name, prog) < 0) {
log_err("Failed to unbind %s from %s", prog, name);
- return -1;
+ goto fail;
}
log_info("Name %s unbound for %s.", name, prog);
}
return 0;
+
+ fail:
+ return -1;
}
static int unbind_process(pid_t pid,
@@ -549,18 +670,21 @@ static int unbind_process(pid_t pid,
if (name == NULL) {
if (reg_destroy_proc(pid) < 0) {
log_err("Failed to unbind %d.", pid);
- return -1;
+ goto fail;
}
log_info("Process %d unbound.", pid);
} else {
if (reg_unbind_proc(name, pid) < 0) {
log_err("Failed to unbind %d from %s", pid, name);
- return -1;
+ goto fail;
}
log_info("Name %s unbound for process %d.", name, pid);
}
return 0;
+
+ fail:
+ return -1;
}
static int list_ipcps(ipcp_list_msg_t *** ipcps,
@@ -581,43 +705,6 @@ static int list_ipcps(ipcp_list_msg_t *** ipcps,
return -1;
}
-int name_create(const struct name_info * info)
-{
- int ret;
-
- assert(info != NULL);
-
- ret = reg_create_name(info);
- if (ret == -EEXIST) {
- log_info("Name %s already exists.", info->name);
- return 0;
- }
-
- if (ret < 0) {
- log_err("Failed to create name %s.", info->name);
- return -1;
- }
-
- log_info("Created new name: %s.", info->name);
-
- return 0;
-}
-
-static int name_destroy(const char * name)
-{
-
- assert(name != NULL);
-
- if (reg_destroy_name(name) < 0) {
- log_err("Failed to destroy name %s.", name);
- return -1;
- }
-
- log_info("Destroyed name: %s.", name);
-
- return 0;
-}
-
static int list_names(name_info_msg_t *** names,
size_t * n_names)
{
@@ -669,8 +756,7 @@ int name_reg(const char * name,
if (ipcp_reg(pid, hash)) {
log_err("Could not register " HASH_FMT32 " with IPCP %d.",
HASH_VAL32(hash.data), pid);
- freebuf(hash);
- return -1;
+ goto fail_hash;
}
log_info("Registered %s with IPCP %d as " HASH_FMT32 ".",
@@ -679,6 +765,10 @@ int name_reg(const char * name,
freebuf(hash);
return 0;
+
+ fail_hash:
+ freebuf(hash);
+ return -1;
}
static int name_unreg(const char * name,
@@ -713,8 +803,7 @@ static int name_unreg(const char * name,
if (ipcp_unreg(pid, hash)) {
log_err("Could not unregister %s with IPCP %d.", name, pid);
- freebuf(hash);
- return -1;
+ goto fail_hash;
}
log_info("Unregistered %s from %d.", name, pid);
@@ -722,20 +811,59 @@ static int name_unreg(const char * name,
freebuf(hash);
return 0;
+
+ fail_hash:
+ freebuf(hash);
+ return -1;
+}
+
+static int get_peer_ids(int fd,
+ uid_t * uid,
+ gid_t * gid,
+ pid_t * pid)
+{
+#if defined(__linux__)
+ struct ucred ucred;
+ socklen_t len;
+
+ len = sizeof(ucred);
+
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0)
+ goto fail;
+
+ *uid = ucred.uid;
+ *gid = ucred.gid;
+ if (pid != NULL)
+ *pid = ucred.pid;
+#else
+ if (getpeereid(fd, uid, gid) < 0)
+ goto fail;
+
+ if (pid != NULL)
+ *pid = -1; /* no portable SO_PEERCRED.pid equivalent */
+#endif
+ return 0;
+ fail:
+ return -1;
}
static int proc_announce(const struct proc_info * info)
{
+ if (reg_prepare_pool(info->uid, info->gid) < 0) {
+ log_err("Failed to prepare pool for uid %d.", info->uid);
+ goto fail;
+ }
+
if (reg_create_proc(info) < 0) {
log_err("Failed to add process %d.", info->pid);
- goto fail_proc;
+ goto fail;
}
log_info("Process added: %d (%s).", info->pid, info->prog);
return 0;
- fail_proc:
+ fail:
return -1;
}
@@ -749,12 +877,6 @@ static int proc_exit(pid_t pid)
return 0;
}
-static void __cleanup_pkp(void * pkp)
-{
- if (pkp != NULL)
- crypt_dh_pkp_destroy(pkp);
-}
-
static void __cleanup_flow(void * flow)
{
reg_destroy_flow(((struct flow_info *) flow)->id);
@@ -762,65 +884,44 @@ static void __cleanup_flow(void * flow)
static int flow_accept(struct flow_info * flow,
buffer_t * data,
- struct timespec * abstime)
+ struct timespec * abstime,
+ struct crypt_sk * sk)
{
- uint8_t buf[MSGBUFSZ];
- buffer_t lpk; /* local public key */
- buffer_t rpk; /* remote public key */
- void * pkp; /* my public/private key pair */
- ssize_t key_len;
- uint8_t * s;
- int err;
+ buffer_t req_hdr;
+ buffer_t resp_hdr;
+ buffer_t peer_crt = BUF_INIT;
+ char name[NAME_SIZE + 1];
+ struct name_info info;
+ int err;
- /* piggyback of user data not yet implemented */
- assert(data != NULL && data->len == 0 && data->data == NULL);
+ assert(data != NULL && BUF_IS_EMPTY(data));
+
+ clrbuf(req_hdr);
+ clrbuf(resp_hdr);
if (!reg_has_proc(flow->n_pid)) {
log_err("Unknown process %d calling accept.", flow->n_pid);
err = -EINVAL;
- goto fail;
- }
-
- s = malloc(SYMMKEYSZ);
- if (s == NULL) {
- log_err("Failed to malloc symmkey.");
- err = -ENOMEM;
- goto fail;
- }
-
- key_len = crypt_dh_pkp_create(&pkp, buf);
- if (key_len < 0) {
- log_err("Failed to generate key pair.");
- err = -ECRYPT;
- goto fail_pkp;
+ goto fail_flow;
}
- lpk.data = buf;
- lpk.len = (size_t) key_len;
-
- log_dbg("Generated ephemeral keys for %d.", flow->n_pid);
-
if (reg_create_flow(flow) < 0) {
log_err("Failed to create flow.");
err = -EBADF;
goto fail_flow;
}
- if (reg_prepare_flow_accept(flow, &lpk) < 0) {
+ if (reg_prepare_flow_accept(flow) < 0) {
log_err("Failed to prepare accept.");
err = -EBADF;
goto fail_wait;
}
pthread_cleanup_push(__cleanup_flow, flow);
- pthread_cleanup_push(__cleanup_pkp, pkp);
- pthread_cleanup_push(free, s);
- err = reg_wait_flow_accepted(flow, &rpk, abstime);
+ err = reg_wait_flow_accepted(flow, &req_hdr, abstime);
pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
if (err == -ETIMEDOUT) {
log_err("Flow accept timed out.");
@@ -835,45 +936,78 @@ static int flow_accept(struct flow_info * flow,
assert(err == 0);
- if (flow->qs.cypher_s != 0) { /* crypto requested */
- if (crypt_dh_derive(pkp, rpk, s) < 0) {
- log_err("Failed to derive secret for %d.", flow->id);
- err = -ECRYPT;
- goto fail_derive;
- }
- freebuf(rpk);
- data->data = s;
- data->len = SYMMKEYSZ;
- s= NULL;
- } else {
- clrbuf(lpk);
+ if (reg_get_name_for_flow_id(name, flow->id) < 0) {
+ log_err("Failed to get name for flow %d.", flow->id);
+ err = -EIPCP;
+ goto fail_oap;
}
- if (ipcp_flow_alloc_resp(flow, 0, lpk) < 0) {
+ if (reg_get_name_info(name, &info) < 0) {
+ log_err("Failed to get name info for %s.", name);
+ err = -ENAME;
+ goto fail_oap;
+ }
+
+ log_dbg("IPCP %d accepting flow %d for %s.",
+ flow->n_pid, flow->id, name);
+
+ flow->uid = reg_get_proc_uid(flow->n_pid);
+
+ err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk,
+ false, NULL, &peer_crt);
+ if (err == -EREPLAY) {
+ log_warn("Dropping replayed alloc request for %s.", name);
+ goto fail_replay;
+ }
+ if (err < 0) {
+ log_err("OAP processing failed for %s.", name);
+ goto fail_oap;
+ }
+
+ if (reg_flow_is_direct(flow->id)) {
+ if (reg_respond_flow_direct(flow->id, &resp_hdr) < 0) {
+ log_err("Failed to respond to direct flow.");
+ goto fail_resp;
+ }
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, false, peer_crt);
+ log_info("Flow %d accepted (direct) by %d for %s.",
+ flow->id, flow->n_pid, name);
+ } else if (ipcp_flow_alloc_resp(flow, 0, resp_hdr) < 0) {
log_err("Failed to respond to flow allocation.");
- err = -EIPCP;
- goto fail_alloc_resp;
+ goto fail_resp;
+ } else {
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, false, peer_crt);
+ log_info("Flow %d accepted by %d for %s (uid %d).",
+ flow->id, flow->n_pid, name, flow->uid);
}
- crypt_dh_pkp_destroy(pkp);
- free(s);
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
return 0;
- fail_derive:
- freebuf(rpk);
- clrbuf(lpk);
- ipcp_flow_alloc_resp(flow, err, lpk);
- fail_alloc_resp:
- flow->state = FLOW_NULL;
+ fail_oap:
+ if (!reg_flow_is_direct(flow->id))
+ ipcp_flow_alloc_resp(flow, err, resp_hdr);
+ fail_replay:
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
fail_wait:
reg_destroy_flow(flow->id);
fail_flow:
- crypt_dh_pkp_destroy(pkp);
- fail_pkp:
- free(s);
- fail:
return err;
+
+ fail_resp:
+ flow->state = FLOW_NULL;
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
+ reg_destroy_flow(flow->id);
+ return -EIPCP;
}
static int flow_join(struct flow_info * flow,
@@ -883,17 +1017,20 @@ static int flow_join(struct flow_info * flow,
struct ipcp_info ipcp;
struct layer_info layer;
buffer_t hash;
- buffer_t pbuf = {NULL, 0}; /* nothing to piggyback */
+ buffer_t pbuf = BUF_INIT; /* nothing to piggyback */
int err;
- log_info("Allocating flow for %d to %s.", flow->n_pid, dst);
-
if (reg_create_flow(flow) < 0) {
log_err("Failed to create flow.");
err = -EBADF;
goto fail_flow;
}
+ flow->uid = reg_get_proc_uid(flow->n_pid);
+
+ log_info("Allocating flow for %d to %s (uid %d).",
+ flow->n_pid, dst, flow->uid);
+
strcpy(layer.name, dst);
if (reg_get_ipcp_by_layer(&ipcp, &layer) < 0) {
log_err("Failed to get IPCP for layer %s.", dst);
@@ -901,6 +1038,8 @@ static int flow_join(struct flow_info * flow,
goto fail_ipcp;
}
+ flow->n_1_pid = ipcp.pid;
+
hash.len = hash_len((enum hash_algo) layer.dir_hash_algo);
hash.data = malloc(hash.len);
if (hash.data == NULL) {
@@ -909,6 +1048,8 @@ static int flow_join(struct flow_info * flow,
goto fail_ipcp;
}
+ str_hash((enum hash_algo) layer.dir_hash_algo, hash.data, dst);
+
reg_prepare_flow_alloc(flow);
if (ipcp_flow_join(flow, hash)) {
@@ -936,6 +1077,7 @@ static int flow_join(struct flow_info * flow,
goto fail_alloc;
}
+ assert(pbuf.data == NULL && pbuf.len == 0);
assert(err == 0);
freebuf(hash);
@@ -954,7 +1096,7 @@ static int get_ipcp_by_dst(const char * dst,
pid_t * pid,
buffer_t * hash)
{
- ipcp_list_msg_t ** ipcps;
+ ipcp_list_msg_t ** ipcps = NULL;
int n;
int i;
int err = -EIPCP;
@@ -1007,128 +1149,7 @@ static int get_ipcp_by_dst(const char * dst,
return err;
}
-static int flow_alloc(struct flow_info * flow,
- const char * dst,
- buffer_t * data,
- struct timespec * abstime)
-{
- uint8_t buf[MSGBUFSZ];
- buffer_t lpk ={NULL, 0}; /* local public key */
- buffer_t rpk; /* remote public key */
- void * pkp = NULL; /* my public/private key pair */
- uint8_t * s = NULL;
- buffer_t hash;
- int err;
- /* piggyback of user data not yet implemented */
- assert(data != NULL && data->len == 0 && data->data == NULL);
-
- log_info("Allocating flow for %d to %s.", flow->n_pid, dst);
-
-
- if (flow->qs.cypher_s > 0) {
- ssize_t key_len;
-
- s = malloc(SYMMKEYSZ);
- if (s == NULL) {
- log_err("Failed to malloc symmetric key");
- err = -ENOMEM;
- goto fail_malloc;
- }
-
- key_len = crypt_dh_pkp_create(&pkp, buf);
- if (key_len < 0) {
- log_err("Failed to generate key pair.");
- err = -ECRYPT;
- goto fail_pkp;
- }
-
- lpk.data = buf;
- lpk.len = (size_t) key_len;
-
- log_dbg("Generated ephemeral keys for %d.", flow->n_pid);
- }
-
- if (reg_create_flow(flow) < 0) {
- log_err("Failed to create flow.");
- err = -EBADF;
- goto fail_flow;
- }
-
- if (get_ipcp_by_dst(dst, &flow->n_1_pid, &hash) < 0) {
- log_err("Failed to find IPCP for %s.", dst);
- err = -EIPCP;
- goto fail_ipcp;
- }
-
- reg_prepare_flow_alloc(flow);
-
- if (ipcp_flow_alloc(flow, hash, lpk)) {
- log_err("Flow allocation %d failed.", flow->id);
- err = -ENOTALLOC;
- goto fail_alloc;
- }
-
- pthread_cleanup_push(__cleanup_flow, flow);
- pthread_cleanup_push(__cleanup_pkp, pkp);
- pthread_cleanup_push(free, hash.data);
- pthread_cleanup_push(free, s);
-
- err = reg_wait_flow_allocated(flow, &rpk, abstime);
-
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
- pthread_cleanup_pop(false);
-
- if (err == -ETIMEDOUT) {
- log_err("Flow allocation timed out.");
- goto fail_alloc;
- }
-
- if (err == -1) {
- log_dbg("Flow allocation terminated.");
- err = -EPIPE;
- goto fail_alloc;
- }
-
- assert(err == 0);
-
- if (flow->qs.cypher_s != 0) { /* crypto requested */
- if (crypt_dh_derive(pkp, rpk, s) < 0) {
- log_err("Failed to derive secret for %d.", flow->id);
- err = -ECRYPT;
- goto fail_derive;
- }
- crypt_dh_pkp_destroy(pkp);
- freebuf(rpk);
- data->data = s;
- data->len = SYMMKEYSZ;
- s = NULL;
- }
-
- freebuf(hash);
- free(s);
-
- return 0;
-
- fail_derive:
- freebuf(rpk);
- flow->state = FLOW_DEALLOCATED;
- fail_alloc:
- freebuf(hash);
- fail_ipcp:
- reg_destroy_flow(flow->id);
- fail_flow:
- if (flow->qs.cypher_s > 0)
- crypt_dh_pkp_destroy(pkp);
- fail_pkp:
- free(s);
- fail_malloc:
- return err;
-}
-
-static int wait_for_accept(enum hash_algo algo,
- const uint8_t * hash)
+static int wait_for_accept(const char * name)
{
struct timespec timeo = TIMESPEC_INIT_MS(IRMD_REQ_ARR_TIMEOUT);
struct timespec abstime;
@@ -1138,25 +1159,23 @@ static int wait_for_accept(enum hash_algo algo,
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- ret = reg_wait_flow_accepting(algo, hash, &abstime);
+ ret = reg_wait_flow_accepting(name, &abstime);
if (ret == -ETIMEDOUT) {
- if (reg_get_exec(algo, hash, &exec) < 0) {
- log_dbg("No program bound to " HASH_FMT32 ".",
- HASH_VAL32(hash));
+ if (reg_get_exec(name, &exec) < 0) {
+ log_dbg("No program bound for %s.", name);
goto fail;
}
- log_info("Autostarting %s.", exec[0]);
-
if (spawn_program(exec) < 0) {
- log_dbg("Failed to autostart " HASH_FMT32 ".",
- HASH_VAL32(hash));
+ log_err("Failed to start %s for %s.", exec[0], name);
goto fail_spawn;
}
+ log_info("Starting %s for %s.", exec[0], name);
+
ts_add(&abstime, &timeo, &abstime);
- ret = reg_wait_flow_accepting(algo, hash, &abstime);
+ ret = reg_wait_flow_accepting(name, &abstime);
if (ret == -ETIMEDOUT)
goto fail_spawn;
@@ -1179,10 +1198,11 @@ static int flow_req_arr(struct flow_info * flow,
struct layer_info layer;
enum hash_algo algo;
int ret;
+ char name[NAME_SIZE + 1];
info.pid = flow->n_1_pid;
- log_info("Flow req arrived from IPCP %d for " HASH_FMT32 ".",
+ log_dbg("Flow req arrived from IPCP %d for " HASH_FMT32 ".",
info.pid, HASH_VAL32(hash));
if (reg_get_ipcp(&info, &layer) < 0) {
@@ -1193,10 +1213,17 @@ static int flow_req_arr(struct flow_info * flow,
algo = (enum hash_algo) layer.dir_hash_algo;
- ret = wait_for_accept(algo, hash);
+ if (reg_get_name_for_hash(name, algo, hash) < 0) {
+ log_warn("No name for " HASH_FMT32 ".", HASH_VAL32(hash));
+ ret = -ENAME;
+ goto fail;
+ }
+
+ log_info("Flow request arrived for %s.", name);
+
+ ret = wait_for_accept(name);
if (ret < 0) {
- log_err("No activeprocess for " HASH_FMT32 ".",
- HASH_VAL32(hash));
+ log_err("No active process for %s.", name);
goto fail;
}
@@ -1214,13 +1241,216 @@ static int flow_req_arr(struct flow_info * flow,
return ret;
}
+#ifndef DISABLE_DIRECT_IPC
+static int flow_alloc_direct(const char * dst,
+ struct flow_info * flow,
+ buffer_t * data,
+ struct timespec * abstime,
+ struct crypt_sk * sk,
+ struct name_info * info)
+{
+ struct flow_info acc; /* server side flow */
+ buffer_t req_hdr = BUF_INIT;
+ buffer_t resp_hdr = BUF_INIT;
+ buffer_t no_crt = BUF_INIT;
+ void * ctx;
+ int err;
+
+ acc.id = wait_for_accept(dst);
+ if (acc.id < 0) {
+ log_dbg("No accepting process for %s.", dst);
+ return -EAGAIN;
+ }
+
+ if (oap_cli_prepare(&ctx, info, &req_hdr, *data, false) < 0) {
+ log_err("Failed to prepare OAP for %s.", dst);
+ return -EBADF;
+ }
+
+ acc.n_1_pid = flow->n_pid;
+ acc.mpl = DIRECT_MPL;
+ acc.mtu = DIRECT_MTU;
+ acc.qs = flow->qs;
+ acc.state = FLOW_ALLOCATED;
+
+ err = reg_prepare_flow_direct(&acc, &req_hdr, flow->uid);
+ if (err == -EPERM) {
+ log_dbg("UID mismatch, falling back.");
+ oap_ctx_free(ctx);
+ freebuf(req_hdr);
+ return -EPERM;
+ }
+
+ if (err < 0) {
+ log_err("Failed to prepare direct flow.");
+ oap_ctx_free(ctx);
+ freebuf(req_hdr);
+ return -EBADF;
+ }
+
+ err = reg_wait_flow_direct(acc.id, &resp_hdr, abstime);
+ if (err < 0) {
+ log_err("Timeout waiting for OAP response.");
+ oap_ctx_free(ctx);
+ return -ETIMEDOUT;
+ }
+
+ err = oap_cli_complete(ctx, info, resp_hdr, data, sk, NULL, NULL);
+ if (err < 0) {
+ log_err("OAP completion failed for %s.", dst);
+ freebuf(resp_hdr);
+ return err;
+ }
+
+ flow->id = acc.id;
+ flow->n_1_pid = acc.n_pid;
+ flow->mpl = DIRECT_MPL;
+ flow->mtu = DIRECT_MTU;
+ flow->state = FLOW_ALLOCATED;
+
+ /* Mark encrypted for re-key; the acceptor caches the cert. */
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(acc.id, true, no_crt);
+
+ log_info("Flow %d allocated (direct) for %d to %s.",
+ flow->id, flow->n_pid, dst);
+
+ freebuf(resp_hdr);
+
+ return 0;
+}
+#endif /* DISABLE_DIRECT_IPC */
+
+static int flow_alloc(const char * dst,
+ struct flow_info * flow,
+ buffer_t * data,
+ struct timespec * abstime,
+ struct crypt_sk * sk)
+{
+ buffer_t req_hdr = BUF_INIT;
+ buffer_t resp_hdr = BUF_INIT;
+ buffer_t hash = BUF_INIT;
+ buffer_t peer_crt = BUF_INIT;
+ struct name_info info;
+ void * ctx;
+ int err;
+
+ /* piggyback of user data not yet implemented */
+ assert(data != NULL && BUF_IS_EMPTY(data));
+
+ /* Look up name_info for dst */
+ if (reg_get_name_info(dst, &info) < 0) {
+ log_err("Failed to get name info for %s.", dst);
+ err = -ENAME;
+ goto fail_flow;
+ }
+
+ flow->uid = reg_get_proc_uid(flow->n_pid);
+
+ log_info("Allocating flow for %d to %s (uid %d).",
+ flow->n_pid, dst, flow->uid);
+
+#ifndef DISABLE_DIRECT_IPC
+ err = flow_alloc_direct(dst, flow, data, abstime, sk, &info);
+ if (err == 0)
+ return 0;
+
+ if (err != -EPERM && err != -EAGAIN)
+ goto fail_flow;
+#endif
+ if (reg_create_flow(flow) < 0) {
+ log_err("Failed to create flow.");
+ err = -EBADF;
+ goto fail_flow;
+ }
+
+ reg_set_name_for_flow_id(dst, flow->id);
+
+ if (get_ipcp_by_dst(dst, &flow->n_1_pid, &hash) < 0) {
+ log_err("Failed to find IPCP for %s.", dst);
+ err = -EIPCP;
+ goto fail_ipcp;
+ }
+
+ if (reg_prepare_flow_alloc(flow) < 0) {
+ log_err("Failed to prepare flow allocation.");
+ err = -EBADF;
+ goto fail_prepare;
+ }
+
+ if (oap_cli_prepare(&ctx, &info, &req_hdr, *data, false) < 0) {
+ log_err("Failed to prepare OAP request for %s.", dst);
+ err = -EBADF;
+ goto fail_prepare;
+ }
+
+ if (ipcp_flow_alloc(flow, hash, req_hdr)) {
+ log_err("Flow allocation %d failed.", flow->id);
+ err = -EIPCP;
+ goto fail_alloc;
+ }
+
+ pthread_cleanup_push(__cleanup_flow, flow);
+ pthread_cleanup_push(free, hash.data);
+
+ err = reg_wait_flow_allocated(flow, &resp_hdr, abstime);
+
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+
+ if (err == -ETIMEDOUT) {
+ log_err("Flow allocation timed out.");
+ goto fail_wait;
+ }
+
+ log_dbg("Response for flow %d to %s.", flow->id, dst);
+
+ if (err < 0) {
+ log_warn("Allocation rejected: %s (%d).", dst, err);
+ goto fail_peer;
+ }
+
+ err = oap_cli_complete(ctx, &info, resp_hdr, data, sk, NULL, &peer_crt);
+ if (err < 0) {
+ log_err("OAP completion failed for %s.", dst);
+ goto fail_complete;
+ }
+
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, true, peer_crt);
+
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
+ freebuf(hash);
+
+ return 0;
+
+ fail_complete:
+ freebuf(peer_crt);
+ ctx = NULL; /* free'd on complete */
+ fail_peer:
+ flow->state = FLOW_DEALLOCATED;
+ fail_wait:
+ freebuf(resp_hdr);
+ fail_alloc:
+ freebuf(req_hdr);
+ oap_ctx_free(ctx);
+ fail_prepare:
+ freebuf(hash);
+ fail_ipcp:
+ reg_destroy_flow(flow->id);
+ fail_flow:
+ return err;
+}
+
static int flow_alloc_reply(struct flow_info * flow,
int response,
buffer_t * data)
{
- flow->state = response ? FLOW_DEALLOCATED : FLOW_ALLOCATED;
+ flow->state = response != 0 ? FLOW_DEALLOCATED : FLOW_ALLOCATED;
- if (reg_respond_alloc(flow, data) < 0) {
+ if (reg_respond_alloc(flow, data, response) < 0) {
log_err("Failed to reply to flow %d.", flow->id);
flow->state = FLOW_DEALLOCATED;
return -EBADF;
@@ -1232,11 +1462,17 @@ static int flow_alloc_reply(struct flow_info * flow,
static int flow_dealloc(struct flow_info * flow,
struct timespec * ts)
{
- log_info("Deallocating flow %d for process %d (timeout: %zd s).",
+ log_info("Deallocating flow %d for process %d (timeout: %ld s).",
flow->id, flow->n_pid, ts->tv_sec);
reg_dealloc_flow(flow);
+ if (reg_flow_is_direct(flow->id)) {
+ if (flow->state == FLOW_DEALLOCATED)
+ reg_destroy_flow(flow->id);
+ return 0;
+ }
+
if (ipcp_flow_dealloc(flow->n_1_pid, flow->id, ts->tv_sec) < 0) {
log_err("Failed to request dealloc from %d.", flow->n_1_pid);
return -EIPCP;
@@ -1259,6 +1495,741 @@ static int flow_dealloc_resp(struct flow_info * flow)
return 0;
}
+/*
+ * Inbox producers. Any thread may post; the worker drains. INIT carries
+ * the flow's lower IPCP pid; RESP transfers ownership of buf.
+ */
+static void rekey_post(enum rekey_evt_type type,
+ int flow_id,
+ pid_t n_1_pid,
+ buffer_t * buf)
+{
+ struct rekey_evt * evt;
+
+ evt = malloc(sizeof(*evt));
+ if (evt == NULL) {
+ log_err("Failed to malloc re-key event for flow %d.", flow_id);
+ if (type == REKEY_INIT || type == REKEY_DIRECT)
+ reg_flow_clear_in_flight(flow_id);
+ else
+ reg_flow_rekey_arr_done(flow_id, type == REKEY_REQ);
+
+ if (buf != NULL)
+ freebuf(*buf);
+
+ return;
+ }
+
+ list_head_init(&evt->next);
+ evt->type = type;
+ evt->flow_id = flow_id;
+ evt->n_1_pid = n_1_pid;
+ clrbuf(evt->buf);
+ if (buf != NULL) {
+ evt->buf = *buf;
+ clrbuf(*buf);
+ }
+
+ pthread_mutex_lock(&irmd.rk.mtx);
+
+ list_add_tail(&evt->next, &irmd.rk.inbox);
+ pthread_cond_signal(&irmd.rk.cond);
+
+ pthread_mutex_unlock(&irmd.rk.mtx);
+}
+
+static void rekey_post_init(int flow_id,
+ pid_t n_1_pid)
+{
+ rekey_post(REKEY_INIT, flow_id, n_1_pid, NULL);
+}
+
+static void rekey_post_resp(int flow_id,
+ buffer_t * buf)
+{
+ rekey_post(REKEY_RESP, flow_id, 0, buf);
+}
+
+static void rekey_post_req(int flow_id,
+ pid_t n_1_pid,
+ buffer_t * buf)
+{
+ rekey_post(REKEY_REQ, flow_id, n_1_pid, buf);
+}
+
+static void rekey_post_direct(int flow_id)
+{
+ rekey_post(REKEY_DIRECT, flow_id, 0, NULL);
+}
+
+/* Worker-only: find an in-flight entry by flow_id. */
+static struct rekey_ctx * rekey_find(struct list_head * tbl,
+ int flow_id)
+{
+ struct list_head * p;
+
+ list_for_each(p, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ if (e->flow_id == flow_id)
+ return e;
+ }
+
+ return NULL;
+}
+
+/* Worker-only: drop an entry, freeing its OAP ctx. */
+static void rekey_drop(struct rekey_ctx * e)
+{
+ if (e->ctx != NULL)
+ oap_ctx_free(e->ctx);
+
+ list_del(&e->next);
+ free(e);
+}
+
+/* Resolve a flow's registered name info; < 0 if the flow or name is gone. */
+static int rekey_name_info(int flow_id,
+ struct name_info * info)
+{
+ char name[NAME_SIZE + 1];
+
+ if (reg_get_name_for_flow_id(name, flow_id) < 0)
+ return -1;
+
+ return reg_get_name_info(name, info);
+}
+
+/* Flow-update relay payload: a 1-byte type prefix on an opaque body. */
+enum flow_upd_type {
+ FLOW_UPD_REKEY_REQ = 0,
+ FLOW_UPD_REKEY_RESP = 1,
+};
+
+/* Prepend the update type to body; caller frees out on success. */
+static int flow_upd_wrap(buffer_t * out,
+ uint8_t type,
+ const buffer_t * body)
+{
+ out->len = body->len + 1;
+ out->data = malloc(out->len);
+ if (out->data == NULL)
+ return -ENOMEM;
+
+ out->data[0] = type;
+ memcpy(out->data + 1, body->data, body->len);
+
+ return 0;
+}
+
+/* Cleanup handlers — the re-key worker is cancelled at shutdown. */
+static void rk_free_evt(void * o)
+{
+ struct rekey_evt * evt = o;
+
+ freebuf(evt->buf);
+ free(evt);
+}
+
+static void rk_freebuf(void * o)
+{
+ freebuf(*(buffer_t *) o);
+}
+
+static void rk_clear_in_flight(void * o)
+{
+ reg_flow_clear_in_flight(*(int *) o);
+}
+
+static void rk_clear_key(void * o)
+{
+ crypt_secure_clear(o, SYMMKEYSZ);
+}
+
+static void rekey_do_initiate(struct list_head * tbl,
+ int flow_id,
+ pid_t n_1_pid)
+{
+ struct rekey_ctx * e;
+ struct flow_info info;
+ struct name_info name;
+ buffer_t req = BUF_INIT;
+ buffer_t upd = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ void * ctx = NULL;
+ int ret;
+
+ e = rekey_find(tbl, flow_id);
+ if (e != NULL)
+ rekey_drop(e); /* Replace in-flight entries */
+
+ if (rekey_name_info(flow_id, &name) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ goto fail;
+ }
+
+ if (oap_cli_prepare(&ctx, &name, &req, data, true) < 0) {
+ log_err("Failed to prepare re-key for flow %d.", flow_id);
+ goto fail;
+ }
+
+ memset(&info, 0, sizeof(info));
+ info.id = flow_id;
+ info.n_1_pid = n_1_pid;
+
+ if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_REQ, &req) < 0) {
+ log_err("Failed to wrap re-key request for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ pthread_cleanup_push(rk_clear_in_flight, &flow_id);
+ pthread_cleanup_push(oap_ctx_free, ctx);
+ pthread_cleanup_push(rk_freebuf, &req);
+ pthread_cleanup_push(rk_freebuf, &upd);
+ ret = ipcp_flow_update(&info, upd);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ freebuf(upd);
+ if (ret < 0) {
+ log_err("Failed to send re-key request for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ e = malloc(sizeof(*e));
+ if (e == NULL) {
+ log_err("Failed to malloc re-key ctx for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ list_head_init(&e->next);
+ e->flow_id = flow_id;
+ e->ctx = ctx;
+ clock_gettime(PTHREAD_COND_CLOCK, &e->deadline);
+ e->deadline.tv_sec += REKEY_RESP_TIMEO;
+
+ list_add(&e->next, tbl);
+
+ log_dbg("Re-key request sent for flow %d.", flow_id);
+
+ freebuf(req);
+
+ return;
+
+ fail_ctx:
+ oap_ctx_free(ctx);
+ freebuf(req);
+ fail:
+ reg_flow_clear_in_flight(flow_id);
+}
+
+/* Worker-only: complete the exchange, install the pending seed. */
+static void rekey_do_complete(struct list_head * tbl,
+ int flow_id,
+ buffer_t buf)
+{
+ struct rekey_ctx * e;
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ uint8_t newgen;
+
+ e = rekey_find(tbl, flow_id);
+ if (e == NULL) {
+ log_dbg("Stale re-key RESPONSE for flow %d.", flow_id);
+ return;
+ }
+
+ /* A concurrent responder already parked a seed; don't overwrite. */
+ if (reg_flow_rekey_pending(flow_id)) {
+ log_dbg("Re-key already pending for flow %d.", flow_id);
+ goto finish;
+ }
+
+ if (rekey_name_info(flow_id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ goto finish;
+ }
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow_id, &crt);
+
+ /* oap_cli_complete frees the ctx on every path. */
+ if (oap_cli_complete(e->ctx, &info, buf, &data, &sk, &crt, NULL) < 0) {
+ log_warn("Failed to complete re-key for flow %d.", flow_id);
+ e->ctx = NULL;
+ goto finish_clear;
+ }
+
+ e->ctx = NULL;
+
+ if (data.len != 1) {
+ log_warn("Re-key reply malformed for flow %d.", flow_id);
+ goto finish_clear;
+ }
+
+ newgen = *(uint8_t *) data.data;
+
+ if (newgen >= 16) {
+ log_warn("Re-key gen %u out of range for flow %d.",
+ newgen, flow_id);
+ goto finish_clear;
+ }
+
+ if (reg_flow_store_pending(flow_id, kbuf, newgen, true) < 0)
+ log_warn("Flow %d gone during re-key.", flow_id);
+ else
+ reg_notify_flow(flow_id, FLOW_UPD);
+
+ log_dbg("Re-key completed for flow %d (gen %u).", flow_id, newgen);
+
+ finish_clear:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ freebuf(data);
+ finish:
+ freebuf(crt);
+ rekey_drop(e);
+ reg_flow_clear_in_flight(flow_id);
+}
+
+/* Worker-only: reap entries whose RESPONSE never arrived. */
+static void rekey_reap_expired(struct list_head * tbl)
+{
+ struct list_head * p;
+ struct list_head * h;
+ struct timespec now;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ list_for_each_safe(p, h, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ if (ts_diff_ns(&e->deadline, &now) > 0)
+ continue;
+
+ log_warn("Re-key timed out for flow %d.", e->flow_id);
+ reg_flow_clear_in_flight(e->flow_id);
+ rekey_drop(e);
+ }
+}
+
+/* Responder side: process request, install pending seed, send response. */
+static int rekey_respond(struct flow_info * flow,
+ buffer_t * pk)
+{
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t rsp = BUF_INIT;
+ buffer_t upd = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ uint8_t newgen;
+ int epoch;
+ int err;
+
+ epoch = reg_flow_get_epoch(flow->id);
+ if (epoch < 0) {
+ log_warn("Re-key for unknown flow %d.", flow->id);
+ return -EBADF;
+ }
+
+ /* Collision: we are driving our own exchange; let it win. */
+ if (reg_flow_rekey_should_yield(flow->id)) {
+ log_dbg("Yielding to own re-key for flow %d.", flow->id);
+ return 0;
+ }
+
+ if (rekey_name_info(flow->id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow->id);
+ return -ENAME;
+ }
+
+ if (reg_flow_rekey_pending(flow->id)) {
+ log_dbg("Duplicate re-key request for flow %d.", flow->id);
+ return 0;
+ }
+
+ newgen = (uint8_t) ((epoch + 1) & 0x0F);
+ data.data = &newgen;
+ data.len = 1;
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow->id, &crt);
+
+ err = oap_srv_process(&info, *pk, &rsp, &data, &sk, true, &crt, NULL);
+ if (err < 0) {
+ /* data still points to stack newgen; don't free it. */
+ log_err("Failed to process re-key OAP for flow %d.", flow->id);
+ goto finish;
+ }
+
+ /* On success oap_srv_process repointed data to client output. */
+ freebuf(data);
+
+ if (reg_flow_store_pending(flow->id, kbuf, newgen, false) < 0) {
+ log_warn("Flow %d gone during re-key.", flow->id);
+ err = -EBADF;
+ goto finish;
+ }
+
+ reg_notify_flow(flow->id, FLOW_UPD);
+
+ if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_RESP, &rsp) == 0) {
+ pthread_cleanup_push(rk_clear_key, kbuf);
+ pthread_cleanup_push(rk_freebuf, &rsp);
+ pthread_cleanup_push(rk_freebuf, &crt);
+ pthread_cleanup_push(rk_freebuf, &upd);
+ if (ipcp_flow_update(flow, upd) < 0)
+ log_err("Failed to send re-key response for flow %d.",
+ flow->id);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ freebuf(upd);
+ }
+
+ err = 0;
+ finish:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ freebuf(rsp);
+ freebuf(crt);
+
+ return err;
+}
+
+/*
+ * Worker-only: re-key a direct (loopback) flow, the exchange runs in-process:
+ * build a client request, then derive the shared seed, and hand the one seed
+ * to both apps with RB_REKEY.
+ */
+static void rekey_do_direct(int flow_id)
+{
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t req = BUF_INIT;
+ buffer_t rsp = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ void * ctx = NULL;
+ uint8_t newgen;
+ int epoch;
+
+ epoch = reg_flow_get_epoch(flow_id);
+ if (epoch < 0) {
+ log_warn("Re-key for unknown flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ if (rekey_name_info(flow_id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ if (oap_cli_prepare(&ctx, &info, &req, data, true) < 0) {
+ log_err("Failed to prepare re-key for flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ newgen = (uint8_t) ((epoch + 1) & 0x0F);
+ data.data = &newgen;
+ data.len = 1;
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow_id, &crt);
+
+ if (oap_srv_process(&info, req, &rsp, &data, &sk, true,
+ &crt, NULL) < 0) {
+ /* data still points to stack newgen; don't free it. */
+ log_err("Failed to process re-key OAP for flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ goto out;
+ }
+
+ /* On success oap_srv_process repointed data to its output. */
+ freebuf(data);
+
+ if (reg_flow_store_pending_direct(flow_id, kbuf, newgen) < 0) {
+ log_warn("Flow %d gone during re-key.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ goto out;
+ }
+
+ reg_notify_flow_peers(flow_id, FLOW_UPD);
+
+ log_dbg("Re-key completed (direct) for flow %d (gen %u).",
+ flow_id, newgen);
+ out:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ oap_ctx_free(ctx);
+ freebuf(req);
+ freebuf(rsp);
+ freebuf(crt);
+}
+
+/* Route one snapshot entry to the wire or in-process re-key path. */
+static void rekey_dispatch(struct list_head * tbl,
+ const struct rekey_info * ri)
+{
+ if (ri->direct)
+ rekey_do_direct(ri->flow_id);
+ else
+ rekey_do_initiate(tbl, ri->flow_id, ri->n_1_pid);
+}
+
+static int flow_update_arr(struct flow_info * flow,
+ buffer_t * pk)
+{
+ uint8_t type;
+ bool is_req;
+
+ if (pk->len < 1)
+ return -EINVAL;
+
+ type = pk->data[0];
+
+ switch (type) {
+ case FLOW_UPD_REKEY_REQ:
+ is_req = true;
+ break;
+ case FLOW_UPD_REKEY_RESP:
+ is_req = false;
+ break;
+ default:
+ log_warn("Unknown flow update type %u.", type);
+ return -EINVAL;
+ }
+
+ /* Drop floods/spoofs before allocating a worker event. */
+ if (!reg_flow_rekey_arr_admit(flow->id, flow->n_1_pid, is_req))
+ return 0;
+
+ /* Strip the type byte, keeping the malloc base for hand-off. */
+ memmove(pk->data, pk->data + 1, pk->len - 1);
+ pk->len -= 1;
+
+ /* Defer to worker; an inline RESP send deadlocks loopback. */
+ if (is_req)
+ rekey_post_req(flow->id, flow->n_1_pid, pk);
+ else
+ rekey_post_resp(flow->id, pk);
+
+ return 0;
+}
+
+static int flow_update(struct flow_info * flow,
+ uid_t uid,
+ pid_t cpid,
+ bool rekey,
+ struct crypt_sk * sk,
+ bool * has_key,
+ bool * initiator)
+{
+ uint8_t seed[SYMMKEYSZ];
+ uint8_t epoch;
+ int rc;
+
+ *has_key = false;
+ *initiator = false;
+
+ if (rekey) {
+ pid_t n_1_pid;
+
+ if (!reg_flow_owned_by(flow->id, uid))
+ return -EPERM;
+
+ /* Direct flows re-key in-process; no lower IPCP carrier. */
+ if (reg_flow_is_direct(flow->id)) {
+ if (reg_flow_rekey_begin(flow->id))
+ rekey_post_direct(flow->id);
+
+ return 0;
+ }
+
+ /* Watermark re-key: the app can't know its lower IPCP. */
+ n_1_pid = reg_flow_get_n_1_pid(flow->id);
+ if (n_1_pid <= 0)
+ return 0;
+
+ /* One exchange per flow; the latch arbitrates collisions. */
+ if (reg_flow_rekey_begin(flow->id))
+ rekey_post_init(flow->id, n_1_pid);
+
+ return 0;
+ }
+
+ rc = reg_flow_take_pending(flow->id, uid, cpid, seed, &epoch,
+ initiator);
+ if (rc == -EPERM)
+ return -EPERM;
+
+ if (rc != 0)
+ return 0;
+
+ memcpy(sk->key, seed, SYMMKEYSZ);
+ sk->epoch = epoch;
+ *has_key = true;
+
+ crypt_secure_clear(seed, SYMMKEYSZ);
+
+ log_dbg("Delivered re-key seed for flow %d (gen %u).",
+ flow->id, epoch);
+
+ return 0;
+}
+
+static void rekey_table_cleanup(void * o)
+{
+ struct list_head * tbl = o;
+ struct list_head * p;
+ struct list_head * h;
+
+ list_for_each_safe(p, h, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ rekey_drop(e);
+ }
+}
+
+static struct rekey_evt * rekey_event_wait(const struct timespec * dl)
+{
+ struct rekey_evt * evt = NULL;
+ int ret = 0;
+
+ pthread_mutex_lock(&irmd.rk.mtx);
+ pthread_cleanup_push(__cleanup_mutex_unlock, &irmd.rk.mtx);
+
+ while (list_is_empty(&irmd.rk.inbox) && ret != -ETIMEDOUT)
+ ret = -pthread_cond_timedwait(&irmd.rk.cond, &irmd.rk.mtx, dl);
+
+ if (!list_is_empty(&irmd.rk.inbox)) {
+ evt = list_first_entry(&irmd.rk.inbox, struct rekey_evt, next);
+ list_del(&evt->next);
+ }
+
+ pthread_cleanup_pop(true);
+
+ return evt;
+}
+
+static struct timespec rekey_deadline(struct list_head * tbl,
+ struct timespec next)
+{
+ struct timespec deadline = next;
+ struct list_head * p;
+
+ list_for_each(p, tbl) {
+ struct rekey_ctx * e;
+ e = list_entry(p, struct rekey_ctx, next);
+ if (ts_diff_ns(&e->deadline, &deadline) < 0)
+ deadline = e->deadline;
+ }
+
+ return deadline;
+}
+
+static void rekey_handle_evt(struct list_head * tbl,
+ struct rekey_evt * evt)
+{
+ struct flow_info rinfo;
+
+ pthread_cleanup_push(rk_free_evt, evt);
+
+ switch (evt->type) {
+ case REKEY_INIT:
+ rekey_do_initiate(tbl, evt->flow_id, evt->n_1_pid);
+ break;
+ case REKEY_REQ:
+ memset(&rinfo, 0, sizeof(rinfo));
+ rinfo.id = evt->flow_id;
+ rinfo.n_1_pid = evt->n_1_pid;
+ rekey_respond(&rinfo, &evt->buf);
+ reg_flow_rekey_arr_done(evt->flow_id, true);
+ break;
+ case REKEY_RESP:
+ rekey_do_complete(tbl, evt->flow_id, evt->buf);
+ reg_flow_rekey_arr_done(evt->flow_id, false);
+ break;
+ case REKEY_DIRECT:
+ rekey_do_direct(evt->flow_id);
+ break;
+ default:
+ break;
+ }
+
+ pthread_cleanup_pop(true);
+}
+
+/* On the periodic tick, dispatch all flows due for re-keying. */
+static void rekey_run_periodic(struct list_head * tbl,
+ struct timespec * next)
+{
+ struct rekey_info snap[REKEY_BATCH];
+ struct timespec now;
+ int n;
+ int i;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ if (ts_diff_ns(next, &now) > 0)
+ return;
+
+ n = reg_flow_snapshot_rekey_due(snap, REKEY_BATCH);
+ for (i = 0; i < n; ++i)
+ rekey_dispatch(tbl, &snap[i]);
+
+ clock_gettime(PTHREAD_COND_CLOCK, next);
+ next->tv_sec += OAP_REKEY_TIMER;
+}
+
+/*
+ * Single worker owning all in-flight Tier-2 re-keys. It drains the
+ * inbox, runs the periodic snapshot, and reaps timed-out exchanges.
+ * The table is touched only here, so it needs no lock.
+ */
+static void * rekey_worker(void * o)
+{
+ struct list_head table;
+ struct timespec next;
+
+ (void) o;
+
+ list_head_init(&table);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &next);
+ next.tv_sec += OAP_REKEY_TIMER;
+
+ pthread_cleanup_push(rekey_table_cleanup, &table);
+
+ while (true) {
+ struct rekey_evt * evt;
+ struct timespec deadline;
+
+ deadline = rekey_deadline(&table, next);
+
+ evt = rekey_event_wait(&deadline);
+
+ if (evt != NULL)
+ rekey_handle_evt(&table, evt);
+
+ rekey_run_periodic(&table, &next);
+
+ rekey_reap_expired(&table);
+ }
+
+ pthread_cleanup_pop(true);
+
+ return (void *) 0;
+}
+
static void * acceptloop(void * o)
{
int csockfd;
@@ -1308,25 +2279,34 @@ static void * acceptloop(void * o)
return (void *) 0;
}
-static void free_msg(void * o)
+static void __cleanup_irm_msg(void * o)
{
irm_msg__free_unpacked((irm_msg_t *) o, NULL);
}
-static irm_msg_t * do_command_msg(irm_msg_t * msg)
+static irm_msg_t * do_command_msg(irm_msg_t * msg,
+ int fd)
{
- struct ipcp_config conf;
- struct ipcp_info ipcp;
- struct flow_info flow;
- struct proc_info proc;
- struct name_info name;
- struct timespec * abstime;
- struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT);
- struct timespec now;
- struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */
- int res;
- irm_msg_t * ret_msg;
- buffer_t data;
+ struct ipcp_config conf;
+ struct ipcp_info ipcp;
+ struct flow_info flow;
+ struct proc_info proc;
+ struct name_info name;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ]; /* stack buffer for OAP */
+ uint8_t * hbuf = NULL; /* heap copy for response */
+ struct timespec * abstime;
+ struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT);
+ struct timespec now;
+ struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */
+ int res;
+ bool has_key = false;
+ bool initiator = false;
+ uid_t uid;
+ gid_t gid;
+ pid_t cpid;
+ irm_msg_t * ret_msg;
+ buffer_t data;
memset(&flow, 0, sizeof(flow));
@@ -1351,7 +2331,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
ret_msg->code = IRM_MSG_CODE__IRM_REPLY;
- pthread_cleanup_push(free_msg, ret_msg);
+ pthread_cleanup_push(__cleanup_irm_msg, ret_msg);
switch (msg->code) {
case IRM_MSG_CODE__IRM_CREATE_IPCP:
@@ -1380,7 +2360,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
res = disconnect_ipcp(msg->pid, msg->dst, msg->comp);
break;
case IRM_MSG_CODE__IRM_BIND_PROGRAM:
- /* Make exec NULL terminated instead of empty string terminated */
+ /* Terminate with NULL instead of "" */
free(msg->exec[msg->n_exec - 1]);
msg->exec[msg->n_exec - 1] = NULL;
res = bind_program(msg->exec, msg->name, msg->opts);
@@ -1391,7 +2371,11 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
case IRM_MSG_CODE__IRM_PROC_ANNOUNCE:
proc.pid = msg->pid;
strcpy(proc.prog, msg->prog);
- res = proc_announce(&proc);
+ res = get_peer_ids(fd, &proc.uid, &proc.gid, NULL);
+ if (res < 0)
+ log_err("Failed to get UID/GID for pid %d.", msg->pid);
+ else
+ res = proc_announce(&proc);
break;
case IRM_MSG_CODE__IRM_PROC_EXIT:
res = proc_exit(msg->pid);
@@ -1406,8 +2390,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
res = list_ipcps(&ret_msg->ipcps, &ret_msg->n_ipcps);
break;
case IRM_MSG_CODE__IRM_CREATE_NAME:
- strcpy(name.name, msg->names[0]->name);
- name.pol_lb = msg->names[0]->pol_lb;
+ name = name_info_msg_to_s(msg->name_info);
res = name_create(&name);
break;
case IRM_MSG_CODE__IRM_DESTROY_NAME:
@@ -1423,18 +2406,37 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
res = name_unreg(msg->name, msg->pid);
break;
case IRM_MSG_CODE__IRM_FLOW_ACCEPT:
+ tpm_wait_work(irmd.tpm);
data.len = msg->pk.len;
data.data = msg->pk.data;
msg->has_pk = false;
assert(data.len > 0 ? data.data != NULL : data.data == NULL);
flow = flow_info_msg_to_s(msg->flow_info);
- res = flow_accept(&flow, &data, abstime);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_symmkey = data.len != 0;
- ret_msg->symmkey.data = data.data;
- ret_msg->symmkey.len = data.len;
+ sk.key = kbuf;
+ res = flow_accept(&flow, &data, abstime, &sk);
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ ret_msg->has_pk = data.len != 0;
+ ret_msg->pk.data = data.data;
+ ret_msg->pk.len = data.len;
+ ret_msg->has_cipher_nid = true;
+ ret_msg->cipher_nid = sk.nid;
+ if (sk.nid == NID_undef)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
}
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
break;
case IRM_MSG_CODE__IRM_FLOW_ALLOC:
data.len = msg->pk.len;
@@ -1443,13 +2445,31 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
assert(data.len > 0 ? data.data != NULL : data.data == NULL);
flow = flow_info_msg_to_s(msg->flow_info);
abstime = abstime == NULL ? &max : abstime;
- res = flow_alloc(&flow, msg->dst, &data, abstime);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_symmkey = data.len != 0;
- ret_msg->symmkey.data = data.data;
- ret_msg->symmkey.len = data.len;
+ sk.key = kbuf;
+ res = flow_alloc(msg->dst, &flow, &data, abstime, &sk);
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ ret_msg->has_pk = data.len != 0;
+ ret_msg->pk.data = data.data;
+ ret_msg->pk.len = data.len;
+ ret_msg->has_cipher_nid = true;
+ ret_msg->cipher_nid = sk.nid;
+ if (sk.nid == NID_undef)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
}
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
break;
case IRM_MSG_CODE__IRM_FLOW_JOIN:
assert(msg->pk.len == 0 && msg->pk.data == NULL);
@@ -1457,7 +2477,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
abstime = abstime == NULL ? &max : abstime;
res = flow_join(&flow, msg->dst, abstime);
if (res == 0)
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
break;
case IRM_MSG_CODE__IRM_FLOW_DEALLOC:
flow = flow_info_msg_to_s(msg->flow_info);
@@ -1488,6 +2508,51 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
flow = flow_info_msg_to_s(msg->flow_info);
res = flow_alloc_reply(&flow, msg->response, &data);
break;
+ case IRM_MSG_CODE__IPCP_FLOW_UPDATE_ARR:
+ data.len = msg->pk.len;
+ data.data = msg->pk.data;
+ msg->pk.data = NULL; /* pass data */
+ msg->pk.len = 0;
+ flow = flow_info_msg_to_s(msg->flow_info);
+ res = flow_update_arr(&flow, &data);
+ freebuf(data);
+ break;
+ case IRM_MSG_CODE__IRM_FLOW_UPDATE:
+ flow = flow_info_msg_to_s(msg->flow_info);
+ if (get_peer_ids(fd, &uid, &gid, &cpid) < 0) {
+ res = -EPERM;
+ break;
+ }
+
+ if (cpid <= 0) /* non-Linux: fall back to asserted pid */
+ cpid = flow.n_pid;
+
+ sk.key = kbuf;
+ res = flow_update(&flow, uid, cpid, msg->rekey, &sk, &has_key,
+ &initiator);
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ if (!has_key)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
+ }
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
+ ret_msg->has_generation = true;
+ ret_msg->generation = sk.epoch;
+ ret_msg->has_rk_initiator = true;
+ ret_msg->rk_initiator = initiator;
+ break;
default:
log_err("Don't know that message code.");
res = -1;
@@ -1502,20 +2567,30 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg)
else
ret_msg->result = res;
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+
return ret_msg;
}
+/* Wipe the session key from a reply before its buffers are freed. */
+static void clear_msg_key(irm_msg_t * msg)
+{
+ if (msg != NULL && msg->has_sym_key)
+ crypt_secure_clear(msg->sym_key.data, msg->sym_key.len);
+}
+
static void * mainloop(void * o)
{
- int sfd;
- irm_msg_t * msg;
- buffer_t buffer;
+ int sfd;
+ irm_msg_t * msg;
+ buffer_t buffer;
(void) o;
while (true) {
irm_msg_t * ret_msg;
struct cmd * cmd;
+ bool had_key;
pthread_mutex_lock(&irmd.cmd_lock);
@@ -1540,12 +2615,12 @@ static void * mainloop(void * o)
continue;
}
- tpm_dec(irmd.tpm);
+ tpm_begin_work(irmd.tpm);
pthread_cleanup_push(__cleanup_close_ptr, &sfd);
- pthread_cleanup_push(free_msg, msg);
+ pthread_cleanup_push(__cleanup_irm_msg, msg);
- ret_msg = do_command_msg(msg);
+ ret_msg = do_command_msg(msg, sfd);
pthread_cleanup_pop(true);
pthread_cleanup_pop(false);
@@ -1556,12 +2631,12 @@ static void * mainloop(void * o)
}
if (ret_msg->result == -EPIPE) {
- log_dbg("Terminated command: application closed socket.");
+ log_dbg("Terminated command: remote closed socket.");
goto fail;
}
if (ret_msg->result == -EIRMD) {
- log_dbg("Terminated command: IRMd not in running state.");
+ log_dbg("Terminated command: IRMd not running.");
goto fail;
}
@@ -1579,6 +2654,9 @@ static void * mainloop(void * o)
irm_msg__pack(ret_msg, buffer.data);
+ had_key = ret_msg->has_sym_key;
+ clear_msg_key(ret_msg);
+
irm_msg__free_unpacked(ret_msg, NULL);
pthread_cleanup_push(__cleanup_close_ptr, &sfd);
@@ -1593,57 +2671,27 @@ static void * mainloop(void * o)
strerror(errno));
}
+ if (had_key)
+ crypt_secure_clear(buffer.data, buffer.len);
+
pthread_cleanup_pop(true);
pthread_cleanup_pop(true);
- tpm_inc(irmd.tpm);
+ tpm_end_work(irmd.tpm);
continue;
fail:
+ clear_msg_key(ret_msg);
irm_msg__free_unpacked(ret_msg, NULL);
fail_msg:
close(sfd);
- tpm_inc(irmd.tpm);
+ tpm_end_work(irmd.tpm);
continue;
}
return (void *) 0;
}
-static void irm_fini(void)
-{
-#ifdef HAVE_FUSE
- struct timespec wait = TIMESPEC_INIT_MS(1);
- int retries = 5;
-#endif
- if (irmd_get_state() != IRMD_NULL)
- log_warn("Unsafe destroy.");
-
- tpm_destroy(irmd.tpm);
-
- close(irmd.sockfd);
-
- if (unlink(IRM_SOCK_PATH))
- log_dbg("Failed to unlink %s.", IRM_SOCK_PATH);
-
- if (irmd.rdrb != NULL)
- shm_rdrbuff_destroy(irmd.rdrb);
-
- if (irmd.lf != NULL)
- lockfile_destroy(irmd.lf);
-
- pthread_mutex_destroy(&irmd.cmd_lock);
- pthread_cond_destroy(&irmd.cmd_cond);
- pthread_rwlock_destroy(&irmd.state_lock);
-
-#ifdef HAVE_FUSE
- while (rmdir(FUSE_PREFIX) < 0 && retries-- > 0)
- nanosleep(&wait, NULL);
- if (retries < 0)
- log_err("Failed to remove " FUSE_PREFIX);
-#endif
-}
-
#ifdef HAVE_FUSE
static void destroy_mount(char * mnt)
{
@@ -1672,7 +2720,7 @@ static void destroy_mount(char * mnt)
static int ouroboros_reset(void)
{
- shm_rdrbuff_purge();
+ ssm_pool_gspp_purge();
lockfile_destroy(irmd.lf);
return 0;
@@ -1693,10 +2741,8 @@ static void cleanup_pid(pid_t pid)
}
destroy_mount(mnt);
-
-#else
- (void) pid;
#endif
+ ssm_pool_reclaim_orphans(irmd.gspp, pid);
}
void * irm_sanitize(void * o)
@@ -1719,10 +2765,89 @@ void * irm_sanitize(void * o)
return (void *) 0;
}
+static int irm_load_store(char * dpath,
+ bool anchor)
+{
+ struct stat st;
+ struct dirent * dent;
+ DIR * dir;
+ void * crt;
+ int ret;
+
+ if (stat(dpath, &st) == -1) {
+ log_dbg("Store directory %s not found.", dpath);
+ return 0;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ log_err("%s is not a directory.", dpath);
+ goto fail_dir;
+ }
+
+ /* loop through files in directory and load certificates */
+ dir = opendir(dpath);
+ if (dir == NULL) {
+ log_err("Failed to open %s.", dpath);
+ goto fail_dir;
+ }
+
+ while ((dent = readdir(dir)) != NULL) {
+ char path[NAME_PATH_SIZE + 1];
+
+ if (strcmp(dent->d_name, ".") == 0 ||
+ strcmp(dent->d_name, "..") == 0)
+ continue;
+
+ snprintf(path, sizeof(path), "%s/%s", dpath,
+ dent->d_name);
+
+ if (stat(path, &st) == -1) {
+ log_dbg("Failed to stat %s.", path);
+ continue;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ log_dbg("%s is not a regular file.", path);
+ goto fail_file;
+ }
+
+ if (crypt_load_crt_file(path, &crt) < 0) {
+ log_err("Failed to load certificate from %s.", path);
+ goto fail_file;
+ }
+
+ ret = anchor ? oap_auth_add_ca_crt(crt)
+ : oap_auth_add_chain_crt(crt);
+ if (ret < 0) {
+ log_err("Failed to add certificate from %s to store.",
+ path);
+ goto fail_crt_add;
+ }
+
+ log_dbg("Loaded certificate: %s.", path);
+
+ crypt_free_crt(crt);
+ }
+
+ closedir(dir);
+
+ log_info("Loaded certificates from %s.", dpath);
+
+ return 0;
+
+ fail_crt_add:
+ crypt_free_crt(crt);
+ fail_file:
+ closedir(dir);
+ fail_dir:
+ return -1;
+}
static int irm_init(void)
{
struct stat st;
+ struct group * grp;
+ gid_t gid;
pthread_condattr_t cattr;
#ifdef HAVE_FUSE
mode_t mask;
@@ -1790,6 +2915,29 @@ static int irm_init(void)
list_head_init(&irmd.cmds);
+ if (pthread_mutex_init(&irmd.rk.mtx, NULL)) {
+ log_err("Failed to initialize mutex.");
+ goto fail_rk_mtx;
+ }
+
+ if (pthread_condattr_init(&cattr)) {
+ log_err("Failed to initialize condattr.");
+ goto fail_rk_mtx;
+ }
+
+#ifndef __APPLE__
+ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
+#endif
+ if (pthread_cond_init(&irmd.rk.cond, &cattr)) {
+ log_err("Failed to initialize condvar.");
+ pthread_condattr_destroy(&cattr);
+ goto fail_rk_cond;
+ }
+
+ pthread_condattr_destroy(&cattr);
+
+ list_head_init(&irmd.rk.inbox);
+
if (stat(SOCK_PATH, &st) == -1) {
if (mkdir(SOCK_PATH, 0777)) {
log_err("Failed to create sockets directory.");
@@ -1808,17 +2956,45 @@ static int irm_init(void)
goto fail_sock_path;
}
- if ((irmd.rdrb = shm_rdrbuff_create()) == NULL) {
- log_err("Failed to create rdrbuff.");
- goto fail_rdrbuff;
+ grp = getgrnam("ouroboros");
+ if (grp == NULL) {
+ log_warn("ouroboros group not found, using gid %d.", getgid());
+ gid = getgid();
+ } else {
+ gid = grp->gr_gid;
}
+ irmd.gspp = ssm_pool_create(getuid(), gid);
+ if (irmd.gspp == NULL) {
+ log_err("Failed to create GSPP.");
+ goto fail_pool;
+ }
+
+ if (ssm_pool_mlock(irmd.gspp) < 0)
+ log_warn("Failed to mlock pool.");
+
irmd.tpm = tpm_create(IRMD_MIN_THREADS, IRMD_ADD_THREADS,
mainloop, NULL);
if (irmd.tpm == NULL) {
log_err("Failed to greate thread pool.");
goto fail_tpm_create;
}
+
+ if (oap_auth_init() < 0) {
+ log_err("Failed to initialize OAP module.");
+ goto fail_oap;
+ }
+
+ if (irm_load_store(OUROBOROS_CA_CRT_DIR, true) < 0) {
+ log_err("Failed to load CA certificates.");
+ goto fail_load_store;
+ }
+
+ if (irm_load_store(OUROBOROS_CHAIN_DIR, false) < 0) {
+ log_err("Failed to load intermediate certificates.");
+ goto fail_load_store;
+ }
+
#ifdef HAVE_FUSE
mask = umask(0);
@@ -1844,6 +3020,8 @@ static int irm_init(void)
gcry_control(GCRYCTL_INITIALIZATION_FINISHED);
#endif
+ irmd_set_state(IRMD_INIT);
+
return 0;
#ifdef HAVE_LIBGCRYPT
@@ -1851,15 +3029,22 @@ static int irm_init(void)
#ifdef HAVE_FUSE
rmdir(FUSE_PREFIX);
#endif
- tpm_destroy(irmd.tpm);
#endif
+ fail_load_store:
+ oap_auth_fini();
+ fail_oap:
+ tpm_destroy(irmd.tpm);
fail_tpm_create:
- shm_rdrbuff_destroy(irmd.rdrb);
- fail_rdrbuff:
+ ssm_pool_destroy(irmd.gspp);
+ fail_pool:
close(irmd.sockfd);
fail_sock_path:
unlink(IRM_SOCK_PATH);
fail_stat:
+ pthread_cond_destroy(&irmd.rk.cond);
+ fail_rk_cond:
+ pthread_mutex_destroy(&irmd.rk.mtx);
+ fail_rk_mtx:
pthread_cond_destroy(&irmd.cmd_cond);
fail_cmd_cond:
pthread_mutex_destroy(&irmd.cmd_lock);
@@ -1872,6 +3057,72 @@ static int irm_init(void)
return -1;
}
+static void irm_fini(void)
+{
+ struct list_head * p;
+ struct list_head * h;
+#ifdef HAVE_FUSE
+ struct timespec wait = TIMESPEC_INIT_MS(1);
+ int retries = 5;
+#endif
+ if (irmd_get_state() != IRMD_INIT)
+ log_warn("Unsafe destroy.");
+
+ oap_auth_fini();
+
+ tpm_destroy(irmd.tpm);
+
+ close(irmd.sockfd);
+
+ if (unlink(IRM_SOCK_PATH))
+ log_dbg("Failed to unlink %s.", IRM_SOCK_PATH);
+
+ ssm_pool_destroy(irmd.gspp);
+
+ if (irmd.lf != NULL)
+ lockfile_destroy(irmd.lf);
+
+ pthread_mutex_lock(&irmd.cmd_lock);
+
+ list_for_each_safe(p, h, &irmd.cmds) {
+ struct cmd * cmd = list_entry(p, struct cmd, next);
+ list_del(&cmd->next);
+ close(cmd->fd);
+ free(cmd);
+ }
+
+ pthread_mutex_unlock(&irmd.cmd_lock);
+
+ pthread_mutex_lock(&irmd.rk.mtx);
+
+ list_for_each_safe(p, h, &irmd.rk.inbox) {
+ struct rekey_evt * evt;
+ evt = list_entry(p, struct rekey_evt, next);
+ list_del(&evt->next);
+ freebuf(evt->buf);
+ free(evt);
+ }
+
+ pthread_mutex_unlock(&irmd.rk.mtx);
+
+ pthread_mutex_destroy(&irmd.cmd_lock);
+ pthread_cond_destroy(&irmd.cmd_cond);
+ pthread_mutex_destroy(&irmd.rk.mtx);
+ pthread_cond_destroy(&irmd.rk.cond);
+ pthread_rwlock_destroy(&irmd.state_lock);
+
+#ifdef HAVE_FUSE
+ while (rmdir(FUSE_PREFIX) < 0 && retries-- > 0)
+ nanosleep(&wait, NULL);
+
+ if (retries < 0)
+ log_err("Failed to remove " FUSE_PREFIX);
+#endif
+ assert(list_is_empty(&irmd.cmds));
+
+ irmd.state = IRMD_NULL;
+}
+
static void usage(void)
{
printf("Usage: irmd \n"
@@ -1885,28 +3136,36 @@ static void usage(void)
static int irm_start(void)
{
+ irmd_set_state(IRMD_RUNNING);
+
if (tpm_start(irmd.tpm))
goto fail_tpm_start;
- irmd_set_state(IRMD_RUNNING);
-
if (pthread_create(&irmd.irm_sanitize, NULL, irm_sanitize, NULL))
goto fail_irm_sanitize;
if (pthread_create(&irmd.acceptor, NULL, acceptloop, NULL))
goto fail_acceptor;
+ if (OAP_REKEY_TIMER > 0) {
+ if (pthread_create(&irmd.rk.worker, NULL, rekey_worker, NULL))
+ goto fail_rekey_worker;
+ }
+
log_info("Ouroboros IPC Resource Manager daemon started...");
return 0;
+ fail_rekey_worker:
+ pthread_cancel(irmd.acceptor);
+ pthread_join(irmd.acceptor, NULL);
fail_acceptor:
pthread_cancel(irmd.irm_sanitize);
pthread_join(irmd.irm_sanitize, NULL);
fail_irm_sanitize:
- irmd_set_state(IRMD_NULL);
tpm_stop(irmd.tpm);
fail_tpm_start:
+ irmd_set_state(IRMD_INIT);
return -1;
}
@@ -1939,6 +3198,11 @@ static void irm_sigwait(sigset_t sigset)
static void irm_stop(void)
{
+ if (OAP_REKEY_TIMER > 0) {
+ pthread_cancel(irmd.rk.worker);
+ pthread_join(irmd.rk.worker, NULL);
+ }
+
pthread_cancel(irmd.acceptor);
pthread_cancel(irmd.irm_sanitize);
@@ -1947,7 +3211,7 @@ static void irm_stop(void)
tpm_stop(irmd.tpm);
- irmd_set_state(IRMD_NULL);
+ irmd_set_state(IRMD_INIT);
}
static void irm_argparse(int argc,
@@ -1964,10 +3228,8 @@ static void irm_argparse(int argc,
argc--;
argv++;
} else if (strcmp(*argv, "--version") == 0) {
- printf("Ouroboros version %d.%d.%d\n",
- OUROBOROS_VERSION_MAJOR,
- OUROBOROS_VERSION_MINOR,
- OUROBOROS_VERSION_PATCH);
+ printf("Ouroboros version %s\n",
+ OUROBOROS_VERSION_STRING);
exit(EXIT_SUCCESS);
#ifdef HAVE_TOML
} else if (strcmp (*argv, "--config") == 0) {
@@ -1998,8 +3260,8 @@ static void * kill_dash_nine(void * o)
slept += intv;
}
- log_dbg("I am become Death, destroyer of hung processes.");
-
+ log_dbg("I guess I’ll have to shut you down for good this time,");
+ log_dbg("already tried a SIGQUIT, so now it’s KILL DASH 9.");
#ifdef IRMD_KILL_ALL_PROCESSES
reg_kill_all_proc(SIGKILL);
nanosleep(&ts, NULL);
@@ -2058,11 +3320,18 @@ int main(int argc,
if (geteuid() != 0) {
printf("IPC Resource Manager must be run as root.\n");
- exit(EXIT_FAILURE);
+ goto fail_irm_init;
}
- if (irm_init() < 0)
+ if (crypt_secure_malloc_init(IRMD_SECMEM_MAX) < 0) {
+ log_err("Failed to initialize secure memory allocation.");
+ goto fail_secmem;
+ }
+
+ if (irm_init() < 0) {
+ log_err("Failed to initialize IRMd.");
goto fail_irm_init;
+ }
if (reg_init() < 0) {
log_err("Failed to initialize registry.");
@@ -2071,12 +3340,15 @@ int main(int argc,
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
- if (irm_start() < 0)
+ if (irm_start() < 0) {
+ log_err("Failed to start IRMd.");
goto fail_irm_start;
+ }
#ifdef HAVE_TOML
if (irm_configure(irmd.cfg_file) < 0) {
- irmd_set_state(IRMD_NULL);
+ log_err("Failed to load IRMd configuration.");
+ irmd_set_state(IRMD_SHUTDOWN);
ret = EXIT_FAILURE;
}
#endif
@@ -2094,6 +3366,10 @@ int main(int argc,
irm_fini();
+ crypt_secure_malloc_fini();
+
+ crypt_cleanup();
+
log_info("Ouroboros IPC Resource Manager daemon exited. Bye.");
log_fini();
@@ -2105,5 +3381,8 @@ int main(int argc,
fail_reg:
irm_fini();
fail_irm_init:
+ crypt_secure_malloc_fini();
+ crypt_cleanup();
+ fail_secmem:
exit(EXIT_FAILURE);
}
diff --git a/src/irmd/oap.h b/src/irmd/oap.h
new file mode 100644
index 00000000..86f11e21
--- /dev/null
+++ b/src/irmd/oap.h
@@ -0,0 +1,84 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Ouroboros Allocation Protocol (OAP) Component
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_OAP_H
+#define OUROBOROS_IRMD_OAP_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <ouroboros/utils.h>
+
+#include <stdbool.h>
+
+/* OAP authentication state (in oap/auth.c) */
+int oap_auth_init(void);
+
+void oap_auth_fini(void);
+
+int oap_auth_add_ca_crt(void * crt);
+
+int oap_auth_add_chain_crt(void * crt);
+
+/*
+* Prepare OAP request header for server, returns context
+* Passes client data for srv, returns srv data for client
+* rekey forces ephemeral server-encap KEX (no client-encap; preserves FS/PCS)
+*/
+int oap_cli_prepare(void ** ctx,
+ const struct name_info * info,
+ buffer_t * req_buf,
+ buffer_t data,
+ bool rekey);
+
+/*
+ * Server processes header, creates response header, returns secret key.
+ * data is in/out: input=srv data to send, output=cli data received.
+ * rekey drops the cert and verifies against cached_crt; peer_crt (or NULL)
+ * receives a copy of the peer cert to cache at the initial handshake.
+ */
+int oap_srv_process(const struct name_info * info,
+ buffer_t req_buf,
+ buffer_t * rsp_buf,
+ buffer_t * data,
+ struct crypt_sk * sk,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
+
+/*
+ * Complete OAP, returns secret key and server data, frees ctx.
+ * cached_crt verifies a cert-less re-key; peer_crt (or NULL) receives a
+ * copy of the peer cert to cache at the initial handshake.
+ */
+int oap_cli_complete(void * ctx,
+ const struct name_info * info,
+ buffer_t rsp_buf,
+ buffer_t * data,
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
+
+/* Free OAP state (on failure before complete) */
+void oap_ctx_free(void * ctx);
+
+#endif /* OUROBOROS_IRMD_OAP_H */
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c
new file mode 100644
index 00000000..f70f9df1
--- /dev/null
+++ b/src/irmd/oap/auth.c
@@ -0,0 +1,558 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Authentication, replay detection, and validation
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define _DEFAULT_SOURCE
+#else
+ #define _POSIX_C_SOURCE 200809L
+#endif
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/time.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * Replay cache: three timestamp-generation hash buckets. A header's bucket
+ * is gen(T) = T / OAP_REPLAY_TIMER, taken mod 3. Staleness bounds a valid T
+ * to generations {G-1, G, G+1} (G is now's generation; a within-slack future
+ * stamp can reach G+1), which are distinct mod 3; the aliasing generation
+ * G-3 is always rejected as too old first. Each bucket is an open-addressed
+ * hash set whose slots are live iff slot.gen == bucket.gen, so a stale bucket
+ * clears in O(1) by bumping its gen. Overflow fails closed (reject), never
+ * evicts, so a flood cannot displace a genuine entry into a replayable state.
+ */
+#define OAP_REPLAY_GENS 3
+
+struct oap_replay_slot {
+ uint64_t gen; /* live iff == bucket gen; 0 = never used */
+ uint64_t ts;
+ uint8_t id[OAP_ID_SIZE];
+};
+
+struct oap_replay_bucket {
+ uint64_t gen;
+ size_t count;
+ struct oap_replay_slot * slots;
+};
+
+static struct {
+ struct auth_ctx * ca_ctx;
+ struct {
+ size_t mask; /* slots per bucket - 1 */
+ size_t cap; /* fail-closed threshold */
+ struct oap_replay_bucket bucket[OAP_REPLAY_GENS];
+ pthread_mutex_t mtx;
+ } replay;
+} oap_auth;
+
+/* FNV-1a over id || ts; the table mask reduces it to a slot index. */
+static size_t replay_hash(const uint8_t * id,
+ uint64_t ts)
+{
+ uint64_t hh = 14695981039346656037ULL;
+ size_t i;
+
+ for (i = 0; i < OAP_ID_SIZE; i++) {
+ hh ^= id[i];
+ hh *= 1099511628211ULL;
+ }
+
+ for (i = 0; i < sizeof(ts); i++) {
+ hh ^= (uint8_t) (ts >> (i * 8));
+ hh *= 1099511628211ULL;
+ }
+
+ return (size_t) hh;
+}
+
+int oap_auth_init(void)
+{
+ size_t m = 1;
+ int i;
+
+ oap_auth.ca_ctx = auth_create_ctx();
+ if (oap_auth.ca_ctx == NULL) {
+ log_err("Failed to create OAP auth context.");
+ goto fail_ctx;
+ }
+
+ while (m < (size_t) OAP_REPLAY_MAX * 2)
+ m <<= 1;
+
+ oap_auth.replay.mask = m - 1;
+ oap_auth.replay.cap = OAP_REPLAY_MAX;
+
+ for (i = 0; i < OAP_REPLAY_GENS; i++) {
+ struct oap_replay_bucket * b = &oap_auth.replay.bucket[i];
+ b->gen = 0;
+ b->count = 0;
+ b->slots = calloc(m, sizeof(*b->slots));
+ if (b->slots == NULL) {
+ log_err("Failed to alloc OAP replay bucket.");
+ goto fail_bucket;
+ }
+ }
+
+ if (pthread_mutex_init(&oap_auth.replay.mtx, NULL)) {
+ log_err("Failed to init OAP replay mutex.");
+ goto fail_bucket;
+ }
+
+ return 0;
+
+ fail_bucket:
+ for (i = 0; i < OAP_REPLAY_GENS; i++)
+ free(oap_auth.replay.bucket[i].slots);
+
+ auth_destroy_ctx(oap_auth.ca_ctx);
+ fail_ctx:
+ return -1;
+}
+
+void oap_auth_fini(void)
+{
+ int i;
+
+ pthread_mutex_lock(&oap_auth.replay.mtx);
+
+ for (i = 0; i < OAP_REPLAY_GENS; i++) {
+ free(oap_auth.replay.bucket[i].slots);
+ oap_auth.replay.bucket[i].slots = NULL;
+ }
+
+ pthread_mutex_unlock(&oap_auth.replay.mtx);
+ pthread_mutex_destroy(&oap_auth.replay.mtx);
+
+ auth_destroy_ctx(oap_auth.ca_ctx);
+}
+
+int oap_auth_add_ca_crt(void * crt)
+{
+ return auth_add_crt_to_store(oap_auth.ca_ctx, crt);
+}
+
+int oap_auth_add_chain_crt(void * crt)
+{
+ return auth_add_crt_to_chain(oap_auth.ca_ctx, crt);
+}
+
+/* HKDF info = LABEL (incl. NUL separator) || request-hash [|| response-hash] */
+#define OAP_BIND_LABEL "o7s-oap-bind"
+#define OAP_KC_LABEL "o7s-oap-kc"
+#define OAP_HS_LABEL "o7s-oap-hs"
+
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t cat = BUF_INIT;
+ uint8_t * p;
+ ssize_t len;
+
+ assert(out != NULL);
+ assert(out->data != NULL);
+
+ cat.len = kex.len + data.len + crt.len;
+ if (cat.len == 0)
+ return -EINVAL;
+
+ cat.data = malloc(cat.len);
+ if (cat.data == NULL)
+ return -ENOMEM;
+
+ p = cat.data;
+ if (kex.len > 0) {
+ memcpy(p, kex.data, kex.len);
+ p += kex.len;
+ }
+
+ if (data.len > 0) {
+ memcpy(p, data.data, data.len);
+ p += data.len;
+ }
+
+ if (crt.len > 0)
+ memcpy(p, crt.data, crt.len);
+
+ len = md_digest(md_nid, cat, out->data);
+
+ freebuf(cat);
+
+ if (len < 0)
+ return -ECRYPT;
+
+ out->len = (size_t) len;
+
+ return 0;
+}
+
+/* HKDF-expand sk->key with info into out; -ECRYPT on failure. */
+static int oap_hkdf_expand(const struct crypt_sk * sk,
+ buffer_t info,
+ uint8_t * out,
+ size_t outlen)
+{
+ buffer_t prk;
+ buffer_t okm;
+
+ prk.len = SYMMKEYSZ;
+ prk.data = sk->key;
+ okm.len = outlen;
+ okm.data = out;
+
+ if (crypt_hkdf_expand(prk, info, okm) < 0)
+ return -ECRYPT;
+
+ return 0;
+}
+
+/* info = label || H(req) */
+#define OAP_HS_INFO_SZ (sizeof(OAP_HS_LABEL) + MAX_HASH_SIZE)
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out)
+{
+ uint8_t info_buf[OAP_HS_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_HS_LABEL);
+ memcpy(info_buf, OAP_HS_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, SYMMKEYSZ);
+}
+
+/* info = label || H(req) || H(resp) || cipher_nid || kdf_nid */
+#define OAP_BIND_INFO_SZ \
+ (sizeof(OAP_BIND_LABEL) + 2 * MAX_HASH_SIZE + 2 * sizeof(uint16_t))
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid)
+{
+ uint8_t info_buf[OAP_BIND_INFO_SZ];
+ uint8_t tmp[SYMMKEYSZ];
+ uint16_t suite[2];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_BIND_LABEL);
+ memcpy(info_buf, OAP_BIND_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ suite[0] = hton16((uint16_t) sk->nid);
+ suite[1] = hton16((uint16_t) kdf_nid);
+ memcpy(info_buf + len, suite, sizeof(suite));
+ len += sizeof(suite);
+
+ info.len = len;
+ info.data = info_buf;
+
+ if (oap_hkdf_expand(sk, info, tmp, SYMMKEYSZ) < 0)
+ return -ECRYPT;
+
+ memcpy(sk->key, tmp, SYMMKEYSZ);
+ crypt_secure_clear(tmp, SYMMKEYSZ);
+
+ return 0;
+}
+
+/* info = label || H(req) || H(resp) */
+#define OAP_KC_INFO_SZ (sizeof(OAP_KC_LABEL) + 2 * MAX_HASH_SIZE)
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen)
+{
+ uint8_t info_buf[OAP_KC_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (outlen > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_KC_LABEL);
+ memcpy(info_buf, OAP_KC_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, outlen);
+}
+
+#define TIMESYNC_SLACK 100 /* ms */
+#define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0)
+int oap_check_hdr(const struct oap_hdr * hdr)
+{
+ struct oap_replay_bucket * b;
+ struct oap_replay_slot * slots;
+ struct timespec now;
+ uint64_t stamp;
+ uint64_t cur;
+ uint64_t gen;
+ uint8_t * id;
+ size_t h;
+ ssize_t delta;
+ int ret = 0;
+
+ assert(hdr != NULL);
+
+ stamp = hdr->timestamp;
+ id = hdr->id.data;
+
+ clock_gettime(CLOCK_REALTIME, &now);
+
+ cur = TS_TO_UINT64(now);
+
+ delta = (ssize_t)(cur - stamp) / MILLION;
+ if (delta < -TIMESYNC_SLACK) {
+ log_err_id(id, "OAP header from %zd ms into future.", -delta);
+ return -EAUTH;
+ }
+
+ if (delta > OAP_REPLAY_TIMER * 1000) {
+ log_err_id(id, "OAP header too old (%zd ms).", delta);
+ return -EAUTH;
+ }
+
+ gen = stamp / ((uint64_t) OAP_REPLAY_TIMER * BILLION);
+
+ pthread_mutex_lock(&oap_auth.replay.mtx);
+
+ b = &oap_auth.replay.bucket[gen % OAP_REPLAY_GENS];
+
+ /* Rotate a stale bucket in O(1): its old-gen slots become free. */
+ if (b->gen != gen) {
+ b->gen = gen;
+ b->count = 0;
+ }
+
+ slots = b->slots;
+
+ h = replay_hash(id, stamp) & oap_auth.replay.mask;
+ while (slots[h].gen == gen) {
+ if (slots[h].ts == stamp && ID_IS_EQUAL(slots[h].id, id)) {
+ log_warn_id(id, "OAP header already known.");
+ ret = -EREPLAY;
+ goto out;
+ }
+
+ h = (h + 1) & oap_auth.replay.mask;
+ }
+
+ /* Empty slot found; fail closed when the window is at capacity. */
+ if (b->count >= oap_auth.replay.cap) {
+ log_warn_id(id, "OAP replay cache full; rejecting.");
+ ret = -EAUTH;
+ goto out;
+ }
+
+ slots[h].gen = gen;
+ slots[h].ts = stamp;
+ memcpy(slots[h].id, id, OAP_ID_SIZE);
+ b->count++;
+ out:
+ pthread_mutex_unlock(&oap_auth.replay.mtx);
+
+ return ret;
+}
+
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt)
+{
+ void * crt;
+ void * pk = NULL;
+ void * pin = NULL;
+ buffer_t crt_der; /* cert source: wire, else cached (re-key) */
+ buffer_t sign; /* Signed region */
+ uint8_t * id = peer_hdr->id.data;
+ int ret;
+
+ assert(name != NULL);
+ assert(cfg != NULL);
+ assert(local_hdr != NULL);
+ assert(peer_hdr != NULL);
+
+ if (memcmp(peer_hdr->id.data, local_hdr->id.data, OAP_ID_SIZE) != 0) {
+ log_err_id(id, "OAP ID mismatch in flow allocation.");
+ goto fail_check;
+ }
+
+ /* Re-key drops the wire cert; fall back to the cached peer cert. */
+ crt_der = peer_hdr->crt;
+ if (crt_der.len == 0 && cached_crt != NULL)
+ crt_der = *cached_crt;
+
+ if (crt_der.len == 0) {
+ if (cfg->a.req) {
+ log_err_id(id, "Peer did not provide a certificate.");
+ goto fail_check;
+ }
+ log_dbg_id(id, "No crt provided.");
+ name[0] = '\0';
+ return 0;
+ }
+
+ if (crypt_load_crt_der(crt_der, &crt) < 0) {
+ log_err_id(id, "Failed to load crt.");
+ goto fail_check;
+ }
+
+ log_dbg_id(id, "Loaded peer crt.");
+
+ if (crypt_get_pubkey_crt(crt, &pk) < 0) {
+ log_err_id(id, "Failed to get pubkey from crt.");
+ goto fail_crt;
+ }
+
+ log_dbg_id(id, "Got public key from crt.");
+
+ if (cfg->a.cacert[0] != '\0') {
+ if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
+ log_err_id(id, "Failed to load pinned CA %s.",
+ cfg->a.cacert);
+ goto fail_crt;
+ }
+ }
+
+ ret = auth_verify_crt_pin(oap_auth.ca_ctx, crt, pin);
+ if (ret == -ENOENT) {
+ log_err_id(id, "Peer crt not issued by pinned CA %s.",
+ cfg->a.cacert);
+ goto fail_pin;
+ }
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to verify peer with CA store.");
+ goto fail_pin;
+ }
+
+ log_dbg_id(id, "Successfully verified peer crt.");
+
+ /* Digest pin: peer must sign with the configured digest */
+ if (crypt_pk_requires_md(pk) &&
+ cfg->d.nid != NID_undef && peer_hdr->md_nid != cfg->d.nid) {
+ log_err_id(id, "Peer did not sign with %s.",
+ md_nid_to_str(cfg->d.nid));
+ goto fail_pin;
+ }
+
+ /* Sealed responses verify over the reconstructed plaintext. */
+ sign = peer_hdr->sealed_pt.data != NULL ?
+ peer_hdr->sealed_pt : peer_hdr->hdr;
+ sign.len -= peer_hdr->sig.len;
+
+ if (auth_verify_sig(pk, peer_hdr->md_nid, sign, peer_hdr->sig) < 0) {
+ log_err_id(id, "Failed to verify signature.");
+ goto fail_pin;
+ }
+
+ ret = crypt_get_crt_name(crt, name);
+ if (ret < 0) {
+ if (ret == -ENAME)
+ log_err_id(id, "Certificate CN too long.");
+ else
+ log_err_id(id, "No name in certificate.");
+ goto fail_pin;
+ }
+
+ if (pin != NULL)
+ crypt_free_crt(pin);
+
+ crypt_free_key(pk);
+ crypt_free_crt(crt);
+
+ log_dbg_id(id, "Successfully authenticated peer.");
+
+ return 0;
+
+ fail_pin:
+ if (pin != NULL)
+ crypt_free_crt(pin);
+ fail_crt:
+ crypt_free_key(pk);
+ crypt_free_crt(crt);
+ fail_check:
+ return -EAUTH;
+}
diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h
new file mode 100644
index 00000000..72938b53
--- /dev/null
+++ b/src/irmd/oap/auth.h
@@ -0,0 +1,68 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Authentication functions
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_OAP_AUTH_H
+#define OUROBOROS_IRMD_OAP_AUTH_H
+
+#include <ouroboros/crypt.h>
+
+#include "hdr.h"
+
+int oap_check_hdr(const struct oap_hdr * hdr);
+
+/*
+ * name is set to the peer crt CN, "" if no crt was presented.
+ * cached_crt (or NULL) is the peer cert from the initial handshake, used
+ * to verify a cert-less re-key.
+ */
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt);
+
+/* Derive the handshake key that seals the response identity block. */
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out);
+
+/* resp_hash = H(kex || data || crt): binds the server response transcript. */
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out);
+
+/* Fold request + response transcript + negotiated suite into the key. */
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid);
+
+/* Server->client key-confirmation tag derived from the bound key. */
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen);
+
+#endif /* OUROBOROS_IRMD_OAP_AUTH_H */
diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c
new file mode 100644
index 00000000..689d67ca
--- /dev/null
+++ b/src/irmd/oap/cli.c
@@ -0,0 +1,672 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Client-side processing
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define _DEFAULT_SOURCE
+#else
+ #define _POSIX_C_SOURCE 200809L
+#endif
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/random.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+#include "io.h"
+#include "../oap.h"
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Client context between oap_cli_prepare and oap_cli_complete */
+struct oap_cli_ctx {
+ uint8_t __id[OAP_ID_SIZE];
+ buffer_t id;
+ uint8_t kex_buf[CRYPT_KEY_BUFSZ];
+ uint8_t req_hash[MAX_HASH_SIZE];
+ size_t req_hash_len;
+ int req_md_nid;
+ struct sec_config scfg;
+ struct oap_hdr local_hdr;
+ void * pkp; /* Ephemeral keypair */
+ uint8_t * key; /* For client-encap KEM */
+};
+
+#define OAP_CLI_CTX_INIT(s) \
+ do { s->id.len = OAP_ID_SIZE; s->id.data = s->__id; } while (0)
+
+/* Client-side credential loading, mocked in tests */
+
+#ifdef OAP_TEST_MODE
+extern int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+extern int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+extern int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * buf);
+#else
+
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ assert(info != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ return load_credentials(info->name, &info->c, pkp, crt);
+}
+
+int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ assert(info != NULL);
+ assert(cfg != NULL);
+
+ memset(cfg, 0, sizeof(*cfg));
+
+ /* A client authenticates the server by default, like an https client */
+ cfg->a.req = OAP_CLIENT_AUTH_DEFAULT;
+
+ return load_sec_config(info->name, info->c.sec, cfg);
+}
+
+int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk)
+{
+ char path[PATH_MAX];
+ const char * ext;
+
+ assert(name != NULL);
+ assert(cfg != NULL);
+ assert(pk != NULL);
+
+ ext = IS_HYBRID_KEM(cfg->x.str) ? "raw" : "pem";
+
+ snprintf(path, sizeof(path),
+ OUROBOROS_CLI_CRT_DIR "/%s/kex.srv.pub.%s", name, ext);
+
+ if (IS_HYBRID_KEM(cfg->x.str)) {
+ if (crypt_load_pubkey_raw_file(path, pk) < 0) {
+ log_err("Failed to load %s pubkey from %s.", ext, path);
+ return -1;
+ }
+ } else {
+ if (crypt_load_pubkey_file_to_der(path, pk) < 0) {
+ log_err("Failed to load %s pubkey from %s.", ext, path);
+ return -1;
+ }
+ }
+
+ log_dbg("Loaded %s pubkey from %s (%zu bytes).", ext, path, pk->len);
+
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
+
+static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s)
+{
+ struct sec_config * scfg = &s->scfg;
+ buffer_t * kex = &s->local_hdr.kex;
+ uint8_t * id = s->id.data;
+ ssize_t len;
+
+ /* Generate ephemeral keypair, send PK */
+ len = kex_pkp_create(scfg, &s->pkp, kex->data);
+ if (len < 0) {
+ log_err_id(id, "Failed to generate DHE keypair.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Generated ephemeral %s keys (%zd bytes).",
+ scfg->x.str, len);
+
+ return 0;
+}
+
+static int do_client_kex_prepare_kem_encap(const char * server_name,
+ struct oap_cli_ctx * s)
+{
+ struct sec_config * scfg = &s->scfg;
+ buffer_t * kex = &s->local_hdr.kex;
+ uint8_t * id = s->id.data;
+ buffer_t server_pk = BUF_INIT;
+ uint8_t key_buf[SYMMKEYSZ];
+ ssize_t len;
+
+ if (load_server_kem_pk(server_name, scfg, &server_pk) < 0) {
+ log_err_id(id, "Failed to load server KEM pk.");
+ return -ECRYPT;
+ }
+
+ if (IS_HYBRID_KEM(scfg->x.str))
+ len = kex_kem_encap_raw(server_pk, kex->data,
+ scfg->k.nid, key_buf);
+ else
+ len = kex_kem_encap(server_pk, kex->data,
+ scfg->k.nid, key_buf);
+
+ freebuf(server_pk);
+
+ if (len < 0) {
+ log_err_id(id, "Failed to encapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Client encaps: CT len=%zd.", len);
+
+ /* Store derived key */
+ s->key = crypt_secure_malloc(SYMMKEYSZ);
+ if (s->key == NULL) {
+ log_err_id(id, "Failed to allocate secure key.");
+ return -ENOMEM;
+ }
+ memcpy(s->key, key_buf, SYMMKEYSZ);
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ return 0;
+}
+
+static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s)
+{
+ struct sec_config * scfg = &s->scfg;
+ buffer_t * kex = &s->local_hdr.kex;
+ uint8_t * id = s->id.data;
+ ssize_t len;
+
+ /* Server encaps: generate keypair, send PK */
+ len = kex_pkp_create(scfg, &s->pkp, kex->data);
+ if (len < 0) {
+ log_err_id(id, "Failed to generate KEM keypair.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) len;
+ log_dbg_id(id, "Client PK for server encaps (%zd bytes).", len);
+
+ return 0;
+}
+
+static int do_client_kex_prepare(const char * server_name,
+ struct oap_cli_ctx * s)
+{
+ struct sec_config * scfg = &s->scfg;
+
+ if (!IS_KEX_ALGO_SET(scfg))
+ return 0;
+
+ if (IS_KEM_ALGORITHM(scfg->x.str)) {
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP)
+ return do_client_kex_prepare_kem_encap(server_name, s);
+ else
+ return do_client_kex_prepare_kem_decap(s);
+ }
+
+ return do_client_kex_prepare_dhe(s);
+}
+
+int oap_cli_prepare(void ** ctx,
+ const struct name_info * info,
+ buffer_t * req_buf,
+ buffer_t data,
+ bool rekey)
+{
+ struct oap_cli_ctx * s;
+ void * pkp = NULL;
+ void * crt = NULL;
+ buffer_t no_tag = BUF_INIT;
+ ssize_t ret;
+
+ assert(ctx != NULL);
+ assert(info != NULL);
+ assert(req_buf != NULL);
+
+ clrbuf(*req_buf);
+ *ctx = NULL;
+
+ /* Allocate ctx to carry between prepare and complete */
+ s = malloc(sizeof(*s));
+ if (s == NULL) {
+ log_err("Failed to allocate OAP client ctx.");
+ return -ENOMEM;
+ }
+
+ memset(s, 0, sizeof(*s));
+ OAP_CLI_CTX_INIT(s);
+
+ /* Generate session ID */
+ if (random_buffer(s->__id, OAP_ID_SIZE) < 0) {
+ log_err("Failed to generate OAP session ID.");
+ goto fail_id;
+ }
+
+ log_dbg_id(s->id.data, "Preparing OAP request for %s.", info->name);
+
+ /* Load client credentials */
+ if (load_cli_credentials(info, &pkp, &crt) < 0) {
+ log_err_id(s->id.data, "Failed to load credentials for %s.",
+ info->name);
+ goto fail_id;
+ }
+
+ /* Load security config */
+ if (load_cli_sec_config(info, &s->scfg) < 0) {
+ log_err_id(s->id.data, "Failed to load security config for %s.",
+ info->name);
+ goto fail_kex;
+ }
+
+ /* Re-key forces server-encap: client-encap forfeits FS/PCS. */
+ if (rekey && s->scfg.x.mode == KEM_MODE_CLIENT_ENCAP) {
+ s->scfg.x.mode = KEM_MODE_SERVER_ENCAP;
+ log_dbg_id(s->id.data, "Re-key forcing ephemeral server KEX.");
+ }
+
+ /* Re-key omits the cert; the server verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
+ 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) {
+ log_err_id(s->id.data, "Failed to prepare client KEX.");
+ goto fail_kex;
+ }
+
+ if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->scfg,
+ no_tag, NID_undef, NULL)) {
+ log_err_id(s->id.data, "Failed to create OAP request header.");
+ goto fail_hdr;
+ }
+
+ debug_oap_hdr_snd(&s->local_hdr);
+
+ /* Compute and store hash of request for verification in complete */
+ s->req_md_nid = s->scfg.d.nid != NID_undef ? s->scfg.d.nid : NID_sha384;
+ ret = md_digest(s->req_md_nid, s->local_hdr.hdr, s->req_hash);
+ if (ret < 0) {
+ log_err_id(s->id.data, "Failed to hash request.");
+ goto fail_hash;
+ }
+ s->req_hash_len = (size_t) ret;
+
+ /* Transfer ownership of request buffer */
+ *req_buf = s->local_hdr.hdr;
+ clrbuf(s->local_hdr.hdr);
+
+ /* oap_hdr_encode repoints id into hdr; restore to __id */
+ s->local_hdr.id = s->id;
+
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+
+ *ctx = s;
+
+ log_dbg_id(s->id.data, "OAP request prepared for %s.", info->name);
+
+ return 0;
+
+ fail_hash:
+ oap_hdr_fini(&s->local_hdr);
+ fail_hdr:
+ crypt_secure_free(s->key, SYMMKEYSZ);
+ crypt_free_key(s->pkp);
+ fail_kex:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ fail_id:
+ free(s);
+ return -ECRYPT;
+}
+
+void oap_ctx_free(void * ctx)
+{
+ struct oap_cli_ctx * s = ctx;
+
+ if (s == NULL)
+ return;
+
+ oap_hdr_fini(&s->local_hdr);
+
+ if (s->pkp != NULL)
+ crypt_free_key(s->pkp);
+
+ if (s->key != NULL)
+ crypt_secure_free(s->key, SYMMKEYSZ);
+
+ memset(s, 0, sizeof(*s));
+ free(s);
+}
+
+static int do_client_kex_complete_kem(struct oap_cli_ctx * s,
+ const struct oap_hdr * peer_hdr,
+ struct crypt_sk * sk)
+{
+ struct sec_config * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ uint8_t key_buf[SYMMKEYSZ];
+
+ if (scfg->x.mode == KEM_MODE_SERVER_ENCAP) {
+ buffer_t ct;
+
+ if (peer_hdr->kex.len == 0) {
+ log_err_id(id, "Server did not send KEM CT.");
+ return -ECRYPT;
+ }
+
+ ct.data = peer_hdr->kex.data;
+ ct.len = peer_hdr->kex.len;
+
+ if (kex_kem_decap(s->pkp, ct, scfg->k.nid, key_buf) < 0) {
+ log_err_id(id, "Failed to decapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "Client decapsulated server CT.");
+
+ } else if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
+ /* Key already derived during prepare */
+ memcpy(sk->key, s->key, SYMMKEYSZ);
+ sk->nid = scfg->c.nid;
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str,
+ scfg->c.str);
+ return 0;
+ }
+
+ memcpy(sk->key, key_buf, SYMMKEYSZ);
+ sk->nid = scfg->c.nid;
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
+
+ return 0;
+}
+
+static int do_client_kex_complete_dhe(struct oap_cli_ctx * s,
+ const struct oap_hdr * peer_hdr,
+ struct crypt_sk * sk)
+{
+ struct sec_config * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ uint8_t key_buf[SYMMKEYSZ];
+
+ /* DHE: derive from server's public key */
+ if (peer_hdr->kex.len == 0) {
+ log_err_id(id, "Server did not send DHE public key.");
+ return -ECRYPT;
+ }
+
+ if (kex_dhe_derive(scfg, s->pkp, peer_hdr->kex, key_buf) < 0) {
+ log_err_id(id, "Failed to derive DHE secret.");
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "DHE: derived shared secret.");
+
+ memcpy(sk->key, key_buf, SYMMKEYSZ);
+ sk->nid = scfg->c.nid;
+ crypt_secure_clear(key_buf, SYMMKEYSZ);
+
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
+
+ return 0;
+}
+
+
+static int do_client_kex_complete(struct oap_cli_ctx * s,
+ const struct oap_hdr * peer_hdr,
+ struct crypt_sk * sk)
+{
+ struct sec_config * scfg = &s->scfg;
+ uint8_t * id = s->id.data;
+ int cipher_nid;
+ int kdf_nid;
+
+ if (!IS_KEX_ALGO_SET(scfg))
+ return 0;
+
+ /* Save client's configured minimums */
+ cipher_nid = scfg->c.nid;
+ kdf_nid = scfg->k.nid;
+
+ /* Accept server's cipher choice */
+ if (peer_hdr->cipher_str == NULL) {
+ log_err_id(id, "Server did not provide cipher.");
+ return -ECRYPT;
+ }
+
+ SET_KEX_CIPHER(scfg, peer_hdr->cipher_str);
+ if (crypt_validate_nid(scfg->c.nid) < 0) {
+ log_err_id(id, "Server cipher '%s' not supported.",
+ peer_hdr->cipher_str);
+ return -ENOTSUP;
+ }
+
+ /* Verify server cipher >= client's minimum */
+ if (crypt_cipher_rank(scfg->c.nid) < crypt_cipher_rank(cipher_nid)) {
+ log_err_id(id, "Server cipher %s too weak.",
+ peer_hdr->cipher_str);
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "Accepted server cipher %s.",
+ peer_hdr->cipher_str);
+
+ /* Accept server's KDF for non-client-encap modes */
+ if (scfg->x.mode != KEM_MODE_CLIENT_ENCAP
+ && peer_hdr->kdf_nid != NID_undef) {
+ if (crypt_kdf_rank(peer_hdr->kdf_nid)
+ < crypt_kdf_rank(kdf_nid)) {
+ log_err_id(id, "Server KDF too weak.");
+ return -ECRYPT;
+ }
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
+ log_dbg_id(id, "Accepted server KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ }
+
+ /* Derive shared secret */
+ if (IS_KEM_ALGORITHM(scfg->x.str))
+ return do_client_kex_complete_kem(s, peer_hdr, sk);
+
+ return do_client_kex_complete_dhe(s, peer_hdr, sk);
+}
+
+int oap_cli_complete(void * ctx,
+ const struct name_info * info,
+ buffer_t rsp_buf,
+ buffer_t * data,
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
+{
+ struct oap_cli_ctx * s = ctx;
+ struct oap_hdr peer_hdr;
+ char peer[NAME_SIZE + 1];
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
+ uint8_t * id;
+ int rc;
+
+ assert(ctx != NULL);
+ assert(info != NULL);
+ assert(data != NULL);
+ assert(sk != NULL);
+
+ sk->nid = NID_undef;
+
+ clrbuf(*data);
+
+ memset(&peer_hdr, 0, sizeof(peer_hdr));
+
+ id = s->id.data;
+
+ log_dbg_id(id, "Completing OAP for %s.", info->name);
+
+ /* Decode response header using client's md_nid for hash length */
+ if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid, false) < 0) {
+ log_err_id(id, "Failed to decode OAP response header.");
+ goto fail_oap;
+ }
+
+ debug_oap_hdr_rcv(&peer_hdr);
+
+ /* Verify response ID matches request */
+ if (memcmp(peer_hdr.id.data, id, OAP_ID_SIZE) != 0) {
+ log_err_id(id, "OAP response ID mismatch.");
+ goto fail_oap;
+ }
+
+ /* Complete key exchange first; the sealed identity needs the secret */
+ if (do_client_kex_complete(s, &peer_hdr, sk) < 0) {
+ log_err_id(id, "Failed to complete key exchange.");
+ goto fail_oap;
+ }
+
+ req_hash.data = s->req_hash;
+ req_hash.len = s->req_hash_len;
+
+ /* Decrypt the sealed server identity (data+cert+sig) before auth */
+ if (sk->nid != NID_undef && peer_hdr.sealed.data != NULL) {
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_oap;
+ }
+
+ rc = oap_hdr_unseal(&peer_hdr, hs_key);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (rc < 0) {
+ log_err_id(id, "Failed to unseal server identity.");
+ goto fail_oap;
+ }
+ }
+
+ /* Authenticate server (cert + signature now in cleartext) */
+ if (oap_auth_peer(peer, &s->scfg, &s->local_hdr, &peer_hdr,
+ cached_crt) < 0) {
+ log_err_id(id, "Failed to authenticate server.");
+ goto fail_oap;
+ }
+
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_oap;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
+ /* Response must carry a transcript tag of the expected length */
+ if (peer_hdr.rsp_tag.len != s->req_hash_len) {
+ log_err_id(id, "Response transcript tag mismatch.");
+ goto fail_oap;
+ }
+
+ /* Verify peer certificate name matches expected destination */
+ if (peer_hdr.crt.len > 0 && strcmp(peer, info->name) != 0) {
+ log_err_id(id, "Peer crt for '%s' does not match '%s'.",
+ peer, info->name);
+ goto fail_oap;
+ }
+
+ if (sk->nid != NID_undef) {
+ /* Encrypted: bind the key and verify key confirmation */
+ resp_hash.data = resp_hash_buf;
+
+ if (oap_resp_hash(s->req_md_nid, peer_hdr.kex,
+ peer_hdr.data, peer_hdr.crt,
+ &resp_hash) < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_oap;
+ }
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ s->scfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_oap;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ s->req_hash_len) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_oap;
+ }
+
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, kc_buf,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Key confirmation mismatch.");
+ goto fail_oap;
+ }
+ } else {
+ /* Cleartext path is config-driven, never a wire downgrade */
+ assert(!IS_KEX_ALGO_SET(&s->scfg));
+ /* Unencrypted: verify request-echo integrity */
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, s->req_hash,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Response tag mismatch.");
+ goto fail_oap;
+ }
+ }
+
+ /* Copy piggybacked data from server response */
+ if (oap_hdr_copy_data(&peer_hdr, data) < 0) {
+ log_err_id(id, "Failed to copy server data.");
+ goto fail_oap;
+ }
+
+ log_info_id(id, "OAP completed for %s.", info->name);
+
+ freebuf(peer_hdr.sealed_pt);
+
+ oap_ctx_free(s);
+
+ return 0;
+
+ fail_oap:
+ freebuf(peer_hdr.sealed_pt);
+ oap_ctx_free(s);
+ return -ECRYPT;
+}
diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c
new file mode 100644
index 00000000..f8400b46
--- /dev/null
+++ b/src/irmd/oap/hdr.c
@@ -0,0 +1,755 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Header encoding, decoding, and debugging
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define _DEFAULT_SOURCE
+#else
+ #define _POSIX_C_SOURCE 200809L
+#endif
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/hash.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/rib.h>
+#include <ouroboros/time.h>
+
+#include "config.h"
+
+#include "hdr.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define OAP_SEAL_TAGSZ 16 /* AEAD tag on the sealed identity block */
+/* Sealed length prefix: data_len ‖ crt_len. */
+#define OAP_SEAL_LENSZ (sizeof(uint16_t) + sizeof(uint16_t))
+
+/* hs_key is single-use per handshake, so a fixed nonce is reuse-safe. */
+static const uint8_t oap_seal_nonce[12];
+
+int oap_hdr_decode(struct oap_hdr * oap_hdr,
+ buffer_t hdr,
+ int req_md_nid,
+ bool rekey)
+{
+ off_t offset;
+ uint16_t kex_len;
+ uint16_t ciph_nid;
+ size_t crt_len;
+ size_t data_len;
+ size_t hash_len;
+ size_t sig_len;
+
+ assert(oap_hdr != NULL);
+ memset(oap_hdr, 0, sizeof(*oap_hdr));
+
+ if (hdr.len < OAP_HDR_MIN_SIZE)
+ goto fail_decode;
+
+ /* Parse fixed header (36 bytes) */
+ oap_hdr->id.data = hdr.data;
+ oap_hdr->id.len = OAP_ID_SIZE;
+
+ offset = OAP_ID_SIZE;
+
+ oap_hdr->timestamp = ntoh64(*(uint64_t *)(hdr.data + offset));
+ offset += sizeof(uint64_t);
+
+ /* cipher NID */
+ ciph_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->nid = ciph_nid;
+ oap_hdr->cipher_str = crypt_nid_to_str(ciph_nid);
+ offset += sizeof(uint16_t);
+
+ /* kdf NID */
+ oap_hdr->kdf_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->kdf_str = md_nid_to_str(oap_hdr->kdf_nid);
+ offset += sizeof(uint16_t);
+
+ /* md NID (signature hash) */
+ oap_hdr->md_nid = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->md_str = md_nid_to_str(oap_hdr->md_nid);
+ offset += sizeof(uint16_t);
+
+ /*
+ * Validate NIDs: NID_undef is valid at parse time, else must be known.
+ * Note: md_nid=NID_undef only valid for PQC; enforced at sign/verify.
+ */
+ if (ciph_nid != NID_undef && crypt_validate_nid(ciph_nid) < 0)
+ goto fail_decode;
+
+ if (oap_hdr->kdf_nid != NID_undef &&
+ md_validate_nid(oap_hdr->kdf_nid) < 0)
+ goto fail_decode;
+ if (oap_hdr->md_nid != NID_undef &&
+ md_validate_nid(oap_hdr->md_nid) < 0)
+ goto fail_decode;
+
+ /* crt_len */
+ crt_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
+ offset += sizeof(uint16_t);
+
+ /* kex_len + flags */
+ kex_len = ntoh16(*(uint16_t *)(hdr.data + offset));
+ oap_hdr->kex.len = (size_t) (kex_len & OAP_KEX_LEN_MASK);
+ oap_hdr->kex_flags.fmt = (kex_len & OAP_KEX_FMT_BIT) ? 1 : 0;
+ oap_hdr->kex_flags.role = (kex_len & OAP_KEX_ROLE_BIT) ? 1 : 0;
+ offset += sizeof(uint16_t);
+
+ /* data_len */
+ data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
+ offset += sizeof(uint16_t);
+
+ assert((size_t) offset == OAP_HDR_MIN_SIZE);
+
+ /* Response includes rsp_tag when md_nid is set */
+ hash_len = (req_md_nid != NID_undef) ?
+ (size_t) md_len(req_md_nid) : 0;
+
+ /* Encrypted response: sealed block is data_len‖crt_len‖data‖crt‖sig. */
+ if (req_md_nid != NID_undef && ciph_nid != NID_undef) {
+ if (hdr.len < (size_t) offset + oap_hdr->kex.len + hash_len +
+ OAP_SEAL_TAGSZ + OAP_SEAL_LENSZ)
+ goto fail_decode;
+
+ oap_hdr->kex.data = hdr.data + offset;
+ offset += oap_hdr->kex.len;
+
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
+ offset += hash_len;
+
+ oap_hdr->sealed.data = hdr.data + offset;
+ oap_hdr->sealed.len = hdr.len - offset;
+
+ /* crt/data/sig lengths are sealed; set by oap_hdr_unseal. */
+ oap_hdr->crt.len = crt_len;
+ oap_hdr->data.len = data_len;
+
+ oap_hdr->hdr = hdr;
+
+ return 0;
+ }
+
+ /* Validate total length */
+ if (hdr.len < (size_t) offset + crt_len + oap_hdr->kex.len +
+ data_len + hash_len)
+ goto fail_decode;
+
+ /* Derive sig_len from remaining bytes */
+ sig_len = hdr.len - offset - crt_len - oap_hdr->kex.len -
+ data_len - hash_len;
+
+ /*
+ * Unsigned packets must not have trailing bytes. A re-key request
+ * is signed but cert-less (verified against the cached peer cert),
+ * so the rekey caller permits crt_len==0 with a signature.
+ */
+ if (crt_len == 0 && sig_len != 0 && !rekey)
+ goto fail_decode;
+
+ /* Parse variable fields */
+ oap_hdr->crt.data = hdr.data + offset;
+ oap_hdr->crt.len = crt_len;
+ offset += crt_len;
+
+ oap_hdr->kex.data = hdr.data + offset;
+ offset += oap_hdr->kex.len;
+
+ oap_hdr->data.data = hdr.data + offset;
+ oap_hdr->data.len = data_len;
+ offset += data_len;
+
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
+ offset += hash_len;
+
+ oap_hdr->sig.data = hdr.data + offset;
+ oap_hdr->sig.len = sig_len;
+
+ oap_hdr->hdr = hdr;
+
+ return 0;
+
+ fail_decode:
+ memset(oap_hdr, 0, sizeof(*oap_hdr));
+ return -1;
+}
+
+void oap_hdr_fini(struct oap_hdr * oap_hdr)
+{
+ assert(oap_hdr != NULL);
+
+ freebuf(oap_hdr->sealed_pt);
+ freebuf(oap_hdr->hdr);
+ memset(oap_hdr, 0, sizeof(*oap_hdr));
+}
+
+int oap_hdr_copy_data(const struct oap_hdr * hdr,
+ buffer_t * out)
+{
+ assert(hdr != NULL);
+ assert(out != NULL);
+
+ if (hdr->data.len == 0) {
+ clrbuf(*out);
+ return 0;
+ }
+
+ out->data = malloc(hdr->data.len);
+ if (out->data == NULL)
+ return -ENOMEM;
+
+ memcpy(out->data, hdr->data.data, hdr->data.len);
+ out->len = hdr->data.len;
+
+ return 0;
+}
+
+void oap_hdr_init(struct oap_hdr * hdr,
+ buffer_t id,
+ uint8_t * kex_buf,
+ buffer_t data,
+ uint16_t nid)
+{
+ assert(hdr != NULL);
+ assert(id.data != NULL && id.len == OAP_ID_SIZE);
+
+ memset(hdr, 0, sizeof(*hdr));
+
+ hdr->id = id;
+ hdr->kex.data = kex_buf;
+ hdr->kex.len = 0;
+ hdr->data = data;
+ hdr->nid = nid;
+}
+
+/* Write the 36-byte fixed header; stamp is already in network order. */
+static void write_oap_fixed(uint8_t * buf,
+ const struct oap_hdr * hdr,
+ const struct sec_config * scfg,
+ size_t crt_len,
+ size_t data_len,
+ uint64_t stamp)
+{
+ uint16_t v;
+ uint16_t kex_len;
+ off_t offset = 0;
+
+ memcpy(buf + offset, hdr->id.data, hdr->id.len);
+ offset += hdr->id.len;
+
+ memcpy(buf + offset, &stamp, sizeof(stamp));
+ offset += sizeof(stamp);
+
+ v = hton16(hdr->nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(scfg->k.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(scfg->d.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16((uint16_t) crt_len);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ kex_len = (uint16_t) hdr->kex.len;
+ if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(scfg->x.str)) {
+ if (IS_HYBRID_KEM(scfg->x.str))
+ kex_len |= OAP_KEX_FMT_BIT;
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP)
+ kex_len |= OAP_KEX_ROLE_BIT;
+ }
+
+ kex_len = hton16(kex_len);
+ memcpy(buf + offset, &kex_len, sizeof(kex_len));
+ offset += sizeof(kex_len);
+
+ v = hton16((uint16_t) data_len);
+ memcpy(buf + offset, &v, sizeof(v));
+}
+
+/*
+ * Pack lens ‖ data ‖ crt, sign prefix ‖ body, append the signature, then
+ * AEAD-seal lens ‖ data ‖ crt ‖ sig under prefix as AAD. The cert, app data
+ * and their sizes stay confidential; *out is the opaque sealed block. The
+ * signature rides inside the seal so it can't deanonymise the server.
+ */
+static int oap_seal_body(int nid,
+ const uint8_t * seal_key,
+ void * pkp,
+ int md_nid,
+ buffer_t prefix,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t sig = BUF_INIT;
+ buffer_t sign;
+ buffer_t aad;
+ buffer_t plain;
+ uint8_t * buf;
+ uint8_t * tmp;
+ uint16_t datalen;
+ uint16_t crtlen;
+ size_t body_len;
+ off_t offset;
+
+ datalen = hton16((uint16_t) data.len);
+ crtlen = hton16((uint16_t) crt.len);
+
+ body_len = OAP_SEAL_LENSZ + data.len + crt.len;
+
+ buf = malloc(prefix.len + body_len);
+ if (buf == NULL)
+ return -1;
+
+ memcpy(buf, prefix.data, prefix.len);
+ offset = (off_t) prefix.len;
+
+ memcpy(buf + offset, &datalen, sizeof(datalen));
+ offset += sizeof(datalen);
+
+ memcpy(buf + offset, &crtlen, sizeof(crtlen));
+ offset += sizeof(crtlen);
+
+ if (data.len != 0)
+ memcpy(buf + offset, data.data, data.len);
+
+ offset += data.len;
+
+ if (crt.len != 0)
+ memcpy(buf + offset, crt.data, crt.len);
+
+ /* Sign prefix ‖ lens ‖ data ‖ crt (plaintext, before sealing). */
+ sign.data = buf;
+ sign.len = prefix.len + body_len;
+
+ if (pkp != NULL && auth_sign(pkp, md_nid, sign, &sig) < 0)
+ goto fail_buf;
+
+ /* Append the signature so the seal covers lens ‖ data ‖ crt ‖ sig. */
+ if (sig.len != 0) {
+ tmp = realloc(buf, prefix.len + body_len + sig.len);
+ if (tmp == NULL)
+ goto fail_sig;
+
+ buf = tmp;
+ memcpy(buf + prefix.len + body_len, sig.data, sig.len);
+ }
+
+ aad.data = buf;
+ aad.len = prefix.len;
+ plain.data = buf + prefix.len;
+ plain.len = body_len + sig.len;
+
+ if (crypt_oneshot_seal(nid, seal_key, oap_seal_nonce,
+ aad, plain, out) < 0)
+ goto fail_sig;
+
+ free(buf);
+ freebuf(sig);
+
+ return 0;
+
+ fail_sig:
+ freebuf(sig);
+ fail_buf:
+ free(buf);
+ return -1;
+}
+
+/* Encode an identity-hidden response: wire = prefix ‖ oap_seal_body(...). */
+static int oap_hdr_encode_sealed(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)
+{
+ struct timespec now;
+ uint64_t stamp;
+ buffer_t der = BUF_INIT;
+ buffer_t sealed = BUF_INIT;
+ buffer_t prefix;
+ off_t offset;
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = hton64(TS_TO_UINT64(now));
+
+ if (crt != NULL && crypt_crt_der(crt, &der) < 0)
+ goto fail_der;
+
+ prefix.len = OAP_HDR_MIN_SIZE + hdr->kex.len + rsp_tag.len;
+ prefix.data = malloc(prefix.len);
+ if (prefix.data == NULL)
+ goto fail_der;
+
+ /* Cleartext crt_len/data_len are 0; real lengths prefix the seal. */
+ write_oap_fixed(prefix.data, hdr, scfg, 0, 0, stamp);
+ offset = OAP_HDR_MIN_SIZE;
+
+ if (hdr->kex.len != 0)
+ memcpy(prefix.data + offset, hdr->kex.data, hdr->kex.len);
+
+ offset += hdr->kex.len;
+
+ if (rsp_tag.len != 0)
+ memcpy(prefix.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
+
+ assert((size_t) offset == prefix.len);
+
+ if (oap_seal_body(hdr->nid, seal_key, pkp, scfg->d.nid,
+ prefix, hdr->data, der, &sealed) < 0)
+ goto fail_prefix;
+
+ hdr->hdr.len = prefix.len + sealed.len;
+ hdr->hdr.data = malloc(hdr->hdr.len);
+ if (hdr->hdr.data == NULL)
+ goto fail_sealed;
+
+ memcpy(hdr->hdr.data, prefix.data, prefix.len);
+ memcpy(hdr->hdr.data + prefix.len, sealed.data, sealed.len);
+
+ freebuf(sealed);
+ free(prefix.data);
+ freebuf(der);
+
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, false) < 0)
+ goto fail_decode;
+
+ return 0;
+
+ fail_decode:
+ oap_hdr_fini(hdr);
+ return -1;
+ fail_sealed:
+ freebuf(sealed);
+ fail_prefix:
+ free(prefix.data);
+ fail_der:
+ freebuf(der);
+ return -1;
+}
+
+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)
+{
+ struct timespec now;
+ uint64_t stamp;
+ buffer_t out;
+ buffer_t der = BUF_INIT;
+ buffer_t sig = BUF_INIT;
+ buffer_t sign;
+ off_t offset;
+
+ assert(hdr != NULL);
+ assert(hdr->id.data != NULL && hdr->id.len == OAP_ID_SIZE);
+ assert(scfg != NULL);
+
+ if (seal_key != NULL)
+ return oap_hdr_encode_sealed(hdr, pkp, crt, scfg, rsp_tag,
+ req_md_nid, seal_key);
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = hton64(TS_TO_UINT64(now));
+
+ if (crt != NULL && crypt_crt_der(crt, &der) < 0)
+ goto fail_der;
+
+ /* Fixed header (36 bytes) + variable fields + rsp_tag (rsp only) */
+ out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len +
+ rsp_tag.len;
+
+ out.data = malloc(out.len);
+ if (out.data == NULL)
+ goto fail_out;
+
+ write_oap_fixed(out.data, hdr, scfg, der.len, hdr->data.len, stamp);
+ offset = OAP_HDR_MIN_SIZE;
+
+ /* certificate (variable) */
+ if (der.len != 0)
+ memcpy(out.data + offset, der.data, der.len);
+
+ offset += der.len;
+
+ /* kex data (variable) */
+ if (hdr->kex.len != 0)
+ memcpy(out.data + offset, hdr->kex.data, hdr->kex.len);
+
+ offset += hdr->kex.len;
+
+ /* data (variable) */
+ if (hdr->data.len != 0)
+ memcpy(out.data + offset, hdr->data.data, hdr->data.len);
+
+ offset += hdr->data.len;
+
+ /* rsp_tag (variable, response only) */
+ if (rsp_tag.len != 0)
+ memcpy(out.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
+
+ assert((size_t) offset == out.len);
+
+ /* Sign the entire header (fixed + variable, excluding signature) */
+ sign.data = out.data;
+ sign.len = out.len;
+
+ if (pkp != NULL && auth_sign(pkp, scfg->d.nid, sign, &sig) < 0)
+ goto fail_sig;
+
+ hdr->hdr = out;
+
+ /* Append signature */
+ if (sig.len > 0) {
+ hdr->hdr.len += sig.len;
+ hdr->hdr.data = realloc(out.data, hdr->hdr.len);
+ if (hdr->hdr.data == NULL)
+ 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)
+ goto fail_decode;
+
+ freebuf(der);
+ freebuf(sig);
+
+ return 0;
+
+ fail_decode:
+ oap_hdr_fini(hdr);
+ fail_realloc:
+ freebuf(sig);
+ fail_sig:
+ freebuf(out);
+ fail_out:
+ freebuf(der);
+ fail_der:
+ return -1;
+}
+
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key)
+{
+ buffer_t pt = BUF_INIT;
+ buffer_t prefix;
+ uint8_t * recon;
+ size_t body_len;
+ size_t pt_len;
+ size_t data_len;
+ size_t crt_len;
+
+ assert(hdr != NULL);
+ assert(key != NULL);
+
+ if (hdr->sealed.data == NULL || hdr->sealed.len == 0)
+ return -EINVAL;
+
+ /* AAD prefix is fixed‖kex‖rsp_tag; sealed starts right after. */
+ prefix.data = hdr->hdr.data;
+ prefix.len = (size_t) (hdr->sealed.data - hdr->hdr.data);
+
+ if (crypt_oneshot_open(hdr->nid, key, oap_seal_nonce, prefix,
+ hdr->sealed, &pt) < 0)
+ return -ECRYPT;
+
+ pt_len = pt.len;
+
+ /* Plaintext = data_len ‖ crt_len ‖ data ‖ crt ‖ sig. */
+ if (pt_len < OAP_SEAL_LENSZ)
+ goto fail_auth;
+
+ data_len = (size_t) ntoh16(*(uint16_t *) pt.data);
+ crt_len = (size_t) ntoh16(*(uint16_t *)(pt.data + sizeof(uint16_t)));
+
+ body_len = OAP_SEAL_LENSZ + data_len + crt_len;
+ if (pt_len < body_len)
+ goto fail_auth;
+
+ /* Rebuild prefix ‖ lens ‖ data ‖ crt ‖ sig (whole signed region). */
+ recon = malloc(prefix.len + pt_len);
+ if (recon == NULL)
+ goto fail_mem;
+
+ memcpy(recon, prefix.data, prefix.len);
+ memcpy(recon + prefix.len, pt.data, pt_len);
+
+ freebuf(pt);
+
+ hdr->sealed_pt.data = recon;
+ hdr->sealed_pt.len = prefix.len + pt_len;
+
+ hdr->data.data = recon + prefix.len + OAP_SEAL_LENSZ;
+ hdr->data.len = data_len;
+ hdr->crt.data = recon + prefix.len + OAP_SEAL_LENSZ + data_len;
+ hdr->crt.len = crt_len;
+ hdr->sig.data = recon + prefix.len + body_len;
+ hdr->sig.len = pt_len - body_len;
+
+ return 0;
+
+ fail_mem:
+ freebuf(pt);
+ return -ENOMEM;
+ fail_auth:
+ freebuf(pt);
+ return -EAUTH;
+}
+
+#ifdef DEBUG_PROTO_OAP
+#define OAP_KEX_IS_KEM(hdr) ((hdr)->kex_flags.role | (hdr)->kex_flags.fmt)
+static void debug_oap_hdr(const struct oap_hdr * hdr)
+{
+ assert(hdr);
+
+ if (hdr->sealed.len > 0)
+ log_proto(" Sealed block: [%zu bytes] on wire",
+ hdr->sealed.len);
+
+ if (hdr->crt.len > 0)
+ log_proto(" crt: [%zu bytes]", hdr->crt.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" crt: <sealed>");
+ else
+ log_proto(" crt: <none>");
+
+ if (hdr->kex.len > 0) {
+ if (OAP_KEX_IS_KEM(hdr))
+ log_proto(" Key Exchange Data: [%zu bytes] [%s]",
+ hdr->kex.len,
+ hdr->kex_flags.role ?
+ "Client encaps" : "Server encaps");
+ else
+ log_proto(" Key Exchange Data: [%zu bytes]",
+ hdr->kex.len);
+ } else
+ log_proto(" Key Exchange Data: <none>");
+
+ if (hdr->cipher_str != NULL)
+ log_proto(" Cipher: %s", hdr->cipher_str);
+ else
+ log_proto(" Cipher: <none>");
+
+ if (hdr->kdf_str != NULL)
+ log_proto(" KDF: HKDF-%s", hdr->kdf_str);
+ else
+ log_proto(" KDF: <none>");
+
+ if (hdr->md_str != NULL)
+ log_proto(" Digest: %s", hdr->md_str);
+ else
+ log_proto(" Digest: <none>");
+
+ if (hdr->data.len > 0)
+ log_proto(" Data: [%zu bytes]", hdr->data.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Data: <sealed>");
+ else
+ log_proto(" Data: <none>");
+
+ if (hdr->rsp_tag.len > 0)
+ log_proto(" Rsp Tag: [%zu bytes]", hdr->rsp_tag.len);
+ else
+ log_proto(" Rsp Tag: <none>");
+
+ if (hdr->sig.len > 0)
+ log_proto(" Signature: [%zu bytes]", hdr->sig.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Signature: <sealed>");
+ else
+ log_proto(" Signature: <none>");
+}
+#endif
+
+void debug_oap_hdr_rcv(const struct oap_hdr * hdr)
+{
+#ifdef DEBUG_PROTO_OAP
+ struct tm * tm;
+ char tmstr[RIB_TM_STRLEN];
+ time_t stamp;
+
+ assert(hdr);
+
+ stamp = (time_t) hdr->timestamp / BILLION;
+
+ tm = gmtime(&stamp);
+ strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
+
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s <--",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
+
+ debug_oap_hdr(hdr);
+#else
+ (void) hdr;
+#endif
+}
+
+void debug_oap_hdr_snd(const struct oap_hdr * hdr)
+{
+#ifdef DEBUG_PROTO_OAP
+ struct tm * tm;
+ char tmstr[RIB_TM_STRLEN];
+ time_t stamp;
+
+ assert(hdr);
+
+ stamp = (time_t) hdr->timestamp / BILLION;
+
+ tm = gmtime(&stamp);
+ strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
+
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s -->",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
+
+ debug_oap_hdr(hdr);
+#else
+ (void) hdr;
+#endif
+}
diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h
new file mode 100644
index 00000000..1a599727
--- /dev/null
+++ b/src/irmd/oap/hdr.h
@@ -0,0 +1,195 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Header definitions and functions
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_OAP_HDR_H
+#define OUROBOROS_IRMD_OAP_HDR_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/utils.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define OAP_ID_SIZE (16)
+#define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 6 * sizeof(uint16_t))
+
+#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw */
+#define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 0=Server encaps, 1=Client encaps */
+#define OAP_KEX_LEN_MASK 0x3FFF /* bits 0-13: Length (0-16383 bytes) */
+
+#define OAP_KEX_ROLE(hdr) (hdr->kex_flags.role)
+#define OAP_KEX_FMT(hdr) (hdr->kex_flags.fmt)
+
+#define OAP_KEX_IS_X509_FMT(hdr) (((hdr)->kex_flags.fmt) == 0)
+#define OAP_KEX_IS_RAW_FMT(hdr) (((hdr)->kex_flags.fmt) == 1)
+
+/*
+ * Plaintext layout (request, and unencrypted/signed response). The
+ * signature covers the whole packet except itself.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | | |
+ * + + |
+ * | | |
+ * + id (128 bits) + |
+ * | Unique flow allocation ID | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + timestamp (64 bits) + |
+ * | UTC nanoseconds since epoch | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | cipher_nid (16 bits) | kdf_nid (16 bits) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | md_nid (16 bits) | crt_len (16 bits) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * |F|R| kex_len (14 bits) | data_len (16 bits) | | Signed
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Region
+ * | | |
+ * + certificate (variable) + |
+ * | X.509 certificate, DER encoded | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + kex_data (variable) + |
+ * | public key (DER/raw) or ciphertext (KEM) | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + data (variable) + |
+ * | Piggybacked application data | |
+ * + + |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | | |
+ * + rsp_tag (variable, response only) + |
+ * | key-confirm tag (enc), else H(request) | |
+ * | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | |
+ * + signature (variable) +
+ * | DSA signature over signed region |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Encrypted response - wire layout. The certificate, application data and
+ * signature are AEAD-sealed - hiding the server identity and the cert/data
+ * sizes; kex and rsp_tag move ahead of the sealed block as cleartext AAD.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | fixed header (36 bytes, see above) | |
+ * + id, timestamp, NIDs, crt_len=0, kex_len, data_len=0 + | AAD
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | kex_data (variable) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | rsp_tag (variable, response only) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | SEAL( data_len ‖ crt_len ‖ data ‖ crt ‖ sig ) | |
+ * + encrypted cert, app data and signature + | Sealed
+ * | + AEAD tag (128 bits) | | area
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ *
+ * cipher_nid: NID value for symmetric cipher (0 = none)
+ * kdf_nid: NID value for KDF function (0 = none)
+ * md_nid: NID value for signature hash (0 = PQC/no signature)
+ *
+ * kex_len field bit layout:
+ * F (bit 15): Format - 0 = X.509 DER, 1 = Raw/Hybrid
+ * R (bit 14): Role - 0 = Server encaps, 1 = Client encaps
+ * (R is ignored for non-KEM algorithms)
+ * Bits 0-13: Length (0-16383 bytes)
+ *
+ * Request: sig_len = total - 36 - crt_len - kex_len - data_len
+ * Response: sig_len = total - 36 - crt_len - kex_len - data_len - hash_len
+ * where hash_len = md_len(req_md_nid / sha384)
+ *
+ * The signed plaintext inside the seal is prefix ‖ data_len ‖ crt_len ‖
+ * data ‖ crt ‖ sig; the cleartext prefix (fixed ‖ kex ‖ rsp_tag) is the
+ * AEAD AAD. Cleartext crt_len/data_len are 0 - the real lengths are sealed,
+ * hiding the cert and data sizes; oap_hdr_unseal reads them to split.
+ */
+
+/* Parsed OAP header - buffers pointing to a single memory region */
+struct oap_hdr {
+ const char * cipher_str;
+ const char * kdf_str;
+ const char * md_str;
+ uint64_t timestamp;
+ uint16_t nid;
+ uint16_t kdf_nid;
+ uint16_t md_nid;
+ struct {
+ bool fmt; /* Format */
+ bool role; /* Role */
+ } kex_flags;
+
+ buffer_t id;
+ buffer_t crt;
+ buffer_t kex;
+ buffer_t data;
+ buffer_t rsp_tag; /* key-confirm tag / H(req), rsp only */
+ buffer_t sig;
+ buffer_t sealed; /* wire ciphertext ‖ tag (sealed rsp) */
+ buffer_t sealed_pt; /* prefix‖lens‖data‖crt‖sig, owned */
+ buffer_t hdr;
+};
+
+
+void oap_hdr_init(struct oap_hdr * hdr,
+ buffer_t id,
+ uint8_t * kex_buf,
+ buffer_t data,
+ uint16_t nid);
+
+void oap_hdr_fini(struct oap_hdr * oap_hdr);
+
+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);
+
+int oap_hdr_decode(struct oap_hdr * hdr,
+ buffer_t buf,
+ int req_md_nid,
+ bool rekey);
+
+/* Decrypt a sealed response identity block; fills data, crt and sig. */
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key);
+
+void debug_oap_hdr_rcv(const struct oap_hdr * hdr);
+
+void debug_oap_hdr_snd(const struct oap_hdr * hdr);
+
+int oap_hdr_copy_data(const struct oap_hdr * hdr,
+ buffer_t * out);
+
+#endif /* OUROBOROS_IRMD_OAP_HDR_H */
diff --git a/src/irmd/oap/internal.h b/src/irmd/oap/internal.h
new file mode 100644
index 00000000..4a156723
--- /dev/null
+++ b/src/irmd/oap/internal.h
@@ -0,0 +1,119 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP internal definitions
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_OAP_INTERNAL_H
+#define OUROBOROS_IRMD_OAP_INTERNAL_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/list.h>
+#include <ouroboros/name.h>
+#include <ouroboros/pthread.h>
+#include <ouroboros/utils.h>
+
+#include "hdr.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+int oap_check_hdr(const struct oap_hdr * hdr);
+
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr);
+
+int oap_negotiate_cipher(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg);
+
+#ifndef OAP_TEST_MODE
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt);
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg);
+#endif
+
+#ifndef OAP_TEST_MODE
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+
+int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+
+int load_server_kem_keypair(const char * name,
+ struct sec_config * cfg,
+ void ** pkp);
+#else
+extern int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+extern int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+extern int load_server_kem_keypair(const char * name,
+ struct sec_config * cfg,
+ void ** pkp);
+#endif
+
+int do_server_kex(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk);
+
+#ifndef OAP_TEST_MODE
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+
+int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+
+int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk);
+#else
+extern int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+extern int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+extern int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk);
+#endif
+
+int oap_client_kex_prepare(struct sec_config * scfg,
+ buffer_t server_pk,
+ buffer_t * kex,
+ uint8_t * key,
+ void ** ephemeral_pkp);
+
+int oap_client_kex_complete(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ void * pkp,
+ uint8_t * key);
+
+#endif /* OUROBOROS_IRMD_OAP_INTERNAL_H */
diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c
new file mode 100644
index 00000000..b5daa432
--- /dev/null
+++ b/src/irmd/oap/io.c
@@ -0,0 +1,158 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - File I/O for credentials and configuration
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define _DEFAULT_SOURCE
+#else
+ #define _POSIX_C_SOURCE 200809L
+#endif
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+
+#include "config.h"
+
+#include "io.h"
+
+#include <assert.h>
+#include <string.h>
+#include <sys/stat.h>
+
+/*
+ * Shared credential and configuration loading helpers
+ */
+
+#ifndef OAP_TEST_MODE
+
+static bool file_exists(const char * path)
+{
+ struct stat s;
+
+ if (stat(path, &s) == 0)
+ return true;
+
+ if (errno == ENOENT) {
+ log_dbg("File %s does not exist.", path);
+ return false;
+ }
+
+ /* Can't stat for another reason; assume present, fail on load */
+ log_warn("Failed to stat %s: %s.", path, strerror(errno));
+
+ return true;
+}
+
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt)
+{
+ assert(paths != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!file_exists(paths->crt) || !file_exists(paths->key)) {
+ log_info("No authentication certificates for %s.", name);
+ return 0;
+ }
+
+ if (crypt_load_crt_file(paths->crt, crt) < 0) {
+ log_err("Failed to load %s for %s.", paths->crt, name);
+ goto fail_crt;
+ }
+
+ if (crypt_load_privkey_file(paths->key, pkp) < 0) {
+ log_err("Failed to load %s for %s.", paths->key, name);
+ goto fail_key;
+ }
+
+ log_info("Loaded authentication certificates for %s.", name);
+
+ return 0;
+
+ fail_key:
+ crypt_free_crt(*crt);
+ *crt = NULL;
+ fail_crt:
+ return -EAUTH;
+}
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg)
+{
+ void * pin;
+
+ assert(name != NULL);
+ assert(cfg != NULL);
+
+ /* Load security config */
+ if (!file_exists(path))
+ log_dbg("No encryption %s for %s.", path, name);
+
+ if (load_sec_config_file(cfg, path) < 0) {
+ log_warn("Failed to load %s for %s.", path, name);
+ return -1;
+ }
+
+ if (cfg->a.cacert[0] != '\0') {
+ if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
+ log_err("Failed to load pinned CA %s for %s.",
+ cfg->a.cacert, name);
+ return -EAUTH;
+ }
+ crypt_free_crt(pin);
+ }
+
+ if (!IS_KEX_ALGO_SET(cfg)) {
+ log_info("Key exchange not configured for %s.", name);
+ return 0;
+ }
+#ifndef HAVE_OPENSSL_ML_KEM
+ if (IS_KEM_ALGORITHM(cfg->x.str)) {
+ log_err("PQC not available, can't use %s for %s.",
+ cfg->x.str, name);
+ return -ENOTSUP;
+ }
+#endif
+ if (crypt_kex_rank(cfg->x.nid) < 1) {
+ log_err("Key exchange not supported for %s.", name);
+ return -ENOTSUP;
+ }
+
+ if (crypt_cipher_rank(cfg->c.nid) < 1) {
+ log_err("Cipher not supported for %s.", name);
+ return -ECRYPT;
+ }
+
+ log_info("Encryption enabled for %s.", name);
+
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
diff --git a/src/irmd/oap/io.h b/src/irmd/oap/io.h
new file mode 100644
index 00000000..953e3898
--- /dev/null
+++ b/src/irmd/oap/io.h
@@ -0,0 +1,40 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Credential and configuration file I/O
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_OAP_IO_H
+#define OUROBOROS_IRMD_OAP_IO_H
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/name.h>
+
+#ifndef OAP_TEST_MODE
+int load_credentials(const char * name,
+ const struct name_sec_paths * paths,
+ void ** pkp,
+ void ** crt);
+
+int load_sec_config(const char * name,
+ const char * path,
+ struct sec_config * cfg);
+#endif
+
+#endif /* OUROBOROS_IRMD_OAP_IO_H */
diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c
new file mode 100644
index 00000000..5d631618
--- /dev/null
+++ b/src/irmd/oap/srv.c
@@ -0,0 +1,582 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * OAP - Server-side processing
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define _DEFAULT_SOURCE
+#else
+ #define _POSIX_C_SOURCE 200809L
+#endif
+
+#define OUROBOROS_PREFIX "irmd/oap"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/logs.h>
+
+#include "config.h"
+
+#include "auth.h"
+#include "hdr.h"
+#include "io.h"
+#include "oap.h"
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef OAP_TEST_MODE
+extern int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt);
+extern int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg);
+extern int load_server_kem_keypair(const char * name,
+ bool raw_fmt,
+ void ** pkp);
+#else
+
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ assert(info != NULL);
+ assert(pkp != NULL);
+ assert(crt != NULL);
+
+ return load_credentials(info->name, &info->s, pkp, crt);
+}
+
+int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ assert(info != NULL);
+ assert(cfg != NULL);
+
+ memset(cfg, 0, sizeof(*cfg));
+
+ /* Client auth stays opt-in (mTLS); enable with auth=required */
+ return load_sec_config(info->name, info->s.sec, cfg);
+}
+
+int load_server_kem_keypair(const char * name,
+ bool raw_fmt,
+ void ** pkp)
+{
+ char path[PATH_MAX];
+ const char * ext;
+
+ assert(name != NULL);
+ assert(pkp != NULL);
+
+ ext = raw_fmt ? "raw" : "pem";
+
+ snprintf(path, sizeof(path),
+ OUROBOROS_SRV_CRT_DIR "/%s/kex.key.%s", name, ext);
+
+ if (raw_fmt) {
+ if (crypt_load_privkey_raw_file(path, pkp) < 0) {
+ log_err("Failed to load %s keypair from %s.",
+ ext, path);
+ return -ECRYPT;
+ }
+ } else {
+ if (crypt_load_privkey_file(path, pkp) < 0) {
+ log_err("Failed to load %s keypair from %s.",
+ ext, path);
+ return -ECRYPT;
+ }
+ }
+
+ log_dbg("Loaded server KEM keypair from %s.", path);
+ return 0;
+}
+
+#endif /* OAP_TEST_MODE */
+
+static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr,
+ char * algo_buf)
+{
+ uint8_t * id = peer_hdr->id.data;
+ int ret;
+
+ if (OAP_KEX_IS_RAW_FMT(peer_hdr)) {
+ ret = kex_get_algo_from_pk_raw(peer_hdr->kex, algo_buf);
+ if (ret < 0) {
+ log_err_id(id, "Failed to get algo from raw key.");
+ return -ECRYPT;
+ }
+ } else {
+ ret = kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf);
+ if (ret < 0) {
+ log_err_id(id, "Failed to get algo from DER key.");
+ return -ECRYPT;
+ }
+ }
+
+ return 0;
+}
+
+static int negotiate_cipher(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg)
+{
+ uint8_t * id = peer_hdr->id.data;
+ int cli_nid;
+ int cli_rank;
+ int srv_rank;
+
+ /* Cipher: select the strongest of client and server */
+ if (peer_hdr->cipher_str != NULL)
+ cli_nid = (int) crypt_str_to_nid(peer_hdr->cipher_str);
+ else
+ cli_nid = NID_undef;
+
+ if (cli_nid != NID_undef && crypt_cipher_rank(cli_nid) < 0) {
+ log_err_id(id, "Unsupported cipher '%s'.",
+ peer_hdr->cipher_str);
+ return -ENOTSUP;
+ }
+
+ cli_rank = crypt_cipher_rank(cli_nid);
+ srv_rank = crypt_cipher_rank(scfg->c.nid);
+
+ if (cli_rank > srv_rank) {
+ SET_KEX_CIPHER_NID(scfg, cli_nid);
+ log_dbg_id(id, "Selected client cipher %s.", scfg->c.str);
+ } else if (srv_rank > 0) {
+ log_dbg_id(id, "Selected server cipher %s.", scfg->c.str);
+ } else {
+ log_err_id(id, "Encryption requested, no cipher.");
+ return -ECRYPT;
+ }
+
+ /* KDF: select the strongest of client and server */
+ if (peer_hdr->kdf_nid != NID_undef
+ && crypt_kdf_rank(peer_hdr->kdf_nid) < 0) {
+ log_err_id(id, "Unsupported KDF NID %d.",
+ peer_hdr->kdf_nid);
+ return -ENOTSUP;
+ }
+
+ cli_rank = crypt_kdf_rank(peer_hdr->kdf_nid);
+ srv_rank = crypt_kdf_rank(scfg->k.nid);
+
+ /* Client-encap KEM bakes KDF into ciphertext; verify min. */
+ if (OAP_KEX_ROLE(peer_hdr) == KEM_MODE_CLIENT_ENCAP) {
+ if (srv_rank > cli_rank) {
+ log_err_id(id, "Client KDF too weak.");
+ return -ECRYPT;
+ }
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
+ } else if (cli_rank > srv_rank) {
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
+ log_dbg_id(id, "Selected client KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ } else if (srv_rank > 0) {
+ log_dbg_id(id, "Selected server KDF %s.",
+ md_nid_to_str(scfg->k.nid));
+ }
+
+ if (IS_KEX_ALGO_SET(scfg))
+ log_info_id(id, "Negotiated %s + %s.",
+ scfg->x.str, scfg->c.str);
+ else
+ log_info_id(id, "No key exchange.");
+
+ return 0;
+}
+
+static int do_server_kem_decap(const struct name_info * info,
+ const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ struct crypt_sk * sk)
+{
+ buffer_t ct;
+ void * server_pkp = NULL;
+ int ret;
+ uint8_t * id = peer_hdr->id.data;
+
+ ret = load_server_kem_keypair(info->name,
+ peer_hdr->kex_flags.fmt,
+ &server_pkp);
+ if (ret < 0)
+ return ret;
+
+ ct.data = peer_hdr->kex.data;
+ ct.len = peer_hdr->kex.len;
+
+ ret = kex_kem_decap(server_pkp, ct, scfg->k.nid, sk->key);
+
+ crypt_free_key(server_pkp);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to decapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ log_dbg_id(id, "Client encaps: decapsulated CT.");
+
+ return 0;
+}
+
+static int do_server_kem_encap(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ buffer_t client_pk;
+ ssize_t ct_len;
+ uint8_t * id = peer_hdr->id.data;
+
+ client_pk.data = peer_hdr->kex.data;
+ client_pk.len = peer_hdr->kex.len;
+
+ if (IS_HYBRID_KEM(scfg->x.str))
+ ct_len = kex_kem_encap_raw(client_pk, kex->data,
+ scfg->k.nid, sk->key);
+ else
+ ct_len = kex_kem_encap(client_pk, kex->data,
+ scfg->k.nid, sk->key);
+
+ if (ct_len < 0) {
+ log_err_id(id, "Failed to encapsulate KEM.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) ct_len;
+
+ log_dbg_id(id, "Server encaps: generated CT, len=%zd.", ct_len);
+
+ return 0;
+}
+
+static int do_server_kex_kem(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ int ret;
+
+ scfg->x.mode = peer_hdr->kex_flags.role;
+
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
+ ret = do_server_kem_decap(info, peer_hdr, scfg, sk);
+ kex->len = 0;
+ } else {
+ ret = do_server_kem_encap(peer_hdr, scfg, kex, sk);
+ }
+
+ return ret;
+}
+
+static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ ssize_t key_len;
+ void * epkp;
+ int ret;
+ uint8_t * id = peer_hdr->id.data;
+
+ key_len = kex_pkp_create(scfg, &epkp, kex->data);
+ if (key_len < 0) {
+ log_err_id(id, "Failed to generate key pair.");
+ return -ECRYPT;
+ }
+
+ kex->len = (size_t) key_len;
+
+ log_dbg_id(id, "Generated %s ephemeral keys.", scfg->x.str);
+
+ ret = kex_dhe_derive(scfg, epkp, peer_hdr->kex, sk->key);
+ if (ret < 0) {
+ log_err_id(id, "Failed to derive secret.");
+ kex_pkp_destroy(epkp);
+ return -ECRYPT;
+ }
+
+ kex_pkp_destroy(epkp);
+
+ return 0;
+}
+
+int do_server_kex(const struct name_info * info,
+ struct oap_hdr * peer_hdr,
+ struct sec_config * scfg,
+ buffer_t * kex,
+ struct crypt_sk * sk)
+{
+ char algo_buf[KEX_ALGO_BUFSZ];
+ int srv_kex_nid;
+ uint8_t * id;
+
+ id = peer_hdr->id.data;
+
+ /* No KEX data from client */
+ if (peer_hdr->kex.len == 0) {
+ if (IS_KEX_ALGO_SET(scfg)) {
+ log_warn_id(id, "KEX requested without info.");
+ return -ECRYPT;
+ }
+ return 0;
+ }
+
+ if (negotiate_cipher(peer_hdr, scfg) < 0)
+ return -ECRYPT;
+
+ /* Save server's configured KEX before overwriting */
+ srv_kex_nid = scfg->x.nid;
+
+ if (OAP_KEX_ROLE(peer_hdr) != KEM_MODE_CLIENT_ENCAP) {
+ /* Server encapsulation or DHE: extract algo from DER PK */
+ if (get_algo_from_peer_key(peer_hdr, algo_buf) < 0)
+ return -ECRYPT;
+
+ SET_KEX_ALGO(scfg, algo_buf);
+
+ /* Reject if client KEX is weaker than server's */
+ if (crypt_kex_rank(scfg->x.nid)
+ < crypt_kex_rank(srv_kex_nid)) {
+ log_err_id(id, "Client KEX %s too weak.",
+ scfg->x.str);
+ return -ECRYPT;
+ }
+ }
+
+ /* Dispatch based on algorithm type */
+ if (IS_KEM_ALGORITHM(scfg->x.str))
+ return do_server_kex_kem(info, peer_hdr, scfg, kex, sk);
+ else
+ return do_server_kex_dhe(peer_hdr, scfg, kex, sk);
+}
+
+int oap_srv_process(const struct name_info * info,
+ buffer_t req_buf,
+ buffer_t * rsp_buf,
+ buffer_t * data,
+ struct crypt_sk * sk,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
+{
+ struct oap_hdr peer_hdr;
+ struct oap_hdr local_hdr;
+ struct sec_config scfg;
+ uint8_t kex_buf[CRYPT_KEY_BUFSZ];
+ uint8_t hash_buf[MAX_HASH_SIZE];
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ const uint8_t * seal_key = NULL;
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
+ buffer_t crt_der = BUF_INIT;
+ buffer_t rsp_tag = BUF_INIT;
+ ssize_t hash_ret;
+ char cli_name[NAME_SIZE + 1];
+ uint8_t * id;
+ void * pkp = NULL;
+ void * crt = NULL;
+ int req_md_nid;
+ int ret;
+
+ assert(info != NULL);
+ assert(rsp_buf != NULL);
+ assert(data != NULL);
+ assert(sk != NULL);
+
+ sk->nid = NID_undef;
+
+ memset(&peer_hdr, 0, sizeof(peer_hdr));
+ memset(&local_hdr, 0, sizeof(local_hdr));
+ clrbuf(*rsp_buf);
+
+ log_dbg("Processing OAP request for %s.", info->name);
+
+ if (load_srv_credentials(info, &pkp, &crt) < 0) {
+ log_err("Failed to load security keys for %s.", info->name);
+ goto fail_cred;
+ }
+
+ /* Re-key omits the cert; the peer verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
+ if (load_srv_sec_config(info, &scfg) < 0) {
+ log_err("Failed to load security config for %s.", info->name);
+ goto fail_kex;
+ }
+
+ /* Decode incoming header (NID_undef = request, no hash) */
+ if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef, rekey) < 0) {
+ log_err("Failed to decode OAP header.");
+ goto fail_auth;
+ }
+
+ debug_oap_hdr_rcv(&peer_hdr);
+
+ id = peer_hdr.id.data; /* Logging */
+
+ ret = oap_check_hdr(&peer_hdr);
+ if (ret == -EREPLAY) {
+ log_warn_id(id, "OAP header failed replay check.");
+ goto fail_replay;
+ }
+ if (ret < 0) {
+ log_err_id(id, "OAP header check failed.");
+ goto fail_auth;
+ }
+
+ oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef);
+
+ if (oap_auth_peer(cli_name, &scfg, &local_hdr, &peer_hdr,
+ cached_crt) < 0) {
+ log_err_id(id, "Failed to authenticate client.");
+ goto fail_auth;
+ }
+
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_auth;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
+ if (do_server_kex(info, &peer_hdr, &scfg, &local_hdr.kex, sk) < 0)
+ goto fail_kex;
+
+ sk->nid = scfg.c.nid;
+
+ /* Build response header with hash of client request */
+ local_hdr.nid = sk->nid;
+
+ /* Use client's md_nid, defaulting to SHA-384 for PQC */
+ req_md_nid = peer_hdr.md_nid != NID_undef ?
+ peer_hdr.md_nid : NID_sha384;
+
+ /* Compute request hash using client's md_nid */
+ hash_ret = md_digest(req_md_nid, req_buf, hash_buf);
+ if (hash_ret < 0) {
+ log_err_id(id, "Failed to hash request.");
+ goto fail_auth;
+ }
+ req_hash.data = hash_buf;
+ req_hash.len = (size_t) hash_ret;
+
+ rsp_tag = req_hash;
+
+ /* Bind the key to the transcript and confirm it to the client */
+ if (sk->nid != NID_undef) {
+ if (crt != NULL && crypt_crt_der(crt, &crt_der) < 0) {
+ log_err_id(id, "Failed to serialize cert.");
+ goto fail_auth;
+ }
+
+ resp_hash.data = resp_hash_buf;
+
+ ret = oap_resp_hash(req_md_nid, local_hdr.kex, *data,
+ crt_der, &resp_hash);
+
+ freebuf(crt_der);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_auth;
+ }
+
+ /* Derive the identity-seal key before bind mutates sk->key */
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_auth;
+ }
+
+ seal_key = hs_key;
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ scfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_auth;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ (size_t) hash_ret) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_auth;
+ }
+
+ rsp_tag.data = kc_buf;
+ }
+
+ ret = oap_hdr_encode(&local_hdr, pkp, crt, &scfg,
+ rsp_tag, req_md_nid, seal_key);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to create OAP response header.");
+ goto fail_auth;
+ }
+
+ debug_oap_hdr_snd(&local_hdr);
+
+ if (oap_hdr_copy_data(&peer_hdr, data) < 0) {
+ log_err_id(id, "Failed to copy client data.");
+ goto fail_data;
+ }
+
+ /* Transfer ownership of response buffer */
+ *rsp_buf = local_hdr.hdr;
+
+ log_info_id(id, "OAP request processed for %s.", info->name);
+
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+
+ return 0;
+
+ fail_data:
+ oap_hdr_fini(&local_hdr);
+ fail_auth:
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ fail_cred:
+ return -EAUTH;
+
+ fail_replay:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ return -EREPLAY;
+
+ fail_kex:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ return -ECRYPT;
+}
diff --git a/src/irmd/oap/tests/CMakeLists.txt b/src/irmd/oap/tests/CMakeLists.txt
new file mode 100644
index 00000000..b534cb72
--- /dev/null
+++ b/src/irmd/oap/tests/CMakeLists.txt
@@ -0,0 +1,64 @@
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
+
+get_filename_component(OAP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY)
+get_filename_component(OAP_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" DIRECTORY)
+get_filename_component(IRMD_SOURCE_DIR "${OAP_SOURCE_DIR}" DIRECTORY)
+get_filename_component(IRMD_BINARY_DIR "${OAP_BINARY_DIR}" DIRECTORY)
+
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
+ # Add new tests here
+ oap_test.c
+)
+
+create_test_sourcelist(${PARENT_DIR}_ml_dsa_tests test_suite_ml_dsa.c
+ # ML-DSA-specific tests
+ oap_test_ml_dsa.c
+)
+
+# OAP test needs io.c compiled with OAP_TEST_MODE
+set(OAP_TEST_SOURCES
+ ${OAP_SOURCE_DIR}/io.c
+ ${OAP_SOURCE_DIR}/hdr.c
+ ${OAP_SOURCE_DIR}/auth.c
+ ${OAP_SOURCE_DIR}/srv.c
+ ${OAP_SOURCE_DIR}/cli.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/common.c
+)
+
+# Regular test executable (ECDSA)
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests} ${OAP_TEST_SOURCES})
+set_source_files_properties(${OAP_TEST_SOURCES}
+ PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE"
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test ouroboros-irm)
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${IRMD_SOURCE_DIR}
+ ${IRMD_BINARY_DIR}
+)
+
+# ML-DSA test executable
+add_executable(${PARENT_DIR}_ml_dsa_test ${${PARENT_DIR}_ml_dsa_tests} ${OAP_TEST_SOURCES})
+set_source_files_properties(${OAP_TEST_SOURCES}
+ TARGET_DIRECTORY ${PARENT_DIR}_ml_dsa_test
+ PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE"
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_ml_dsa_test)
+target_link_libraries(${PARENT_DIR}_ml_dsa_test ouroboros-irm)
+target_include_directories(${PARENT_DIR}_ml_dsa_test PRIVATE
+ ${IRMD_SOURCE_DIR}
+ ${IRMD_BINARY_DIR}
+)
+
+add_dependencies(build_tests ${PARENT_DIR}_test ${PARENT_DIR}_ml_dsa_test)
+
+# Regular tests
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
+
+# ML-DSA tests
+ouroboros_register_tests(TARGET ${PARENT_DIR}_ml_dsa_test TESTS ${${PARENT_DIR}_ml_dsa_tests})
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c
new file mode 100644
index 00000000..49ea9187
--- /dev/null
+++ b/src/irmd/oap/tests/common.c
@@ -0,0 +1,619 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Common test helper functions for OAP tests
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#include "common.h"
+
+#include <ouroboros/crypt.h>
+
+#include "oap.h"
+
+#include <string.h>
+#include <stdio.h>
+
+int load_srv_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ (void) 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);
+
+ if (test_cfg.srv.kex == NID_undef)
+ return 0;
+
+ SET_KEX_ALGO_NID(cfg, test_cfg.srv.kex);
+ SET_KEX_CIPHER_NID(cfg, test_cfg.srv.cipher);
+ SET_KEX_KDF_NID(cfg, test_cfg.srv.kdf);
+ SET_KEX_KEM_MODE(cfg, test_cfg.srv.kem_mode);
+
+ return 0;
+}
+
+int load_cli_sec_config(const struct name_info * info,
+ struct sec_config * cfg)
+{
+ (void) 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);
+
+ if (test_cfg.cli.kex == NID_undef)
+ return 0;
+
+ SET_KEX_ALGO_NID(cfg, test_cfg.cli.kex);
+ SET_KEX_CIPHER_NID(cfg, test_cfg.cli.cipher);
+ SET_KEX_KDF_NID(cfg, test_cfg.cli.kdf);
+ SET_KEX_KEM_MODE(cfg, test_cfg.cli.kem_mode);
+
+ return 0;
+}
+
+int load_srv_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ (void) info;
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!test_cfg.srv.auth)
+ return 0;
+
+ return mock_load_credentials(pkp, crt);
+}
+
+int load_cli_credentials(const struct name_info * info,
+ void ** pkp,
+ void ** crt)
+{
+ (void) info;
+
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (!test_cfg.cli.auth)
+ return 0;
+
+ return mock_load_credentials(pkp, crt);
+}
+
+int oap_test_setup(struct oap_test_ctx * ctx,
+ const char * root_ca_str,
+ const char * im_ca_str)
+{
+ memset(ctx, 0, sizeof(*ctx));
+
+ strcpy(ctx->srv.info.name, "test-1.unittest.o7s");
+ strcpy(ctx->cli.info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail_init;
+ }
+
+ if (crypt_load_crt_str(root_ca_str, &ctx->root_ca) < 0) {
+ printf("Failed to load root CA cert.\n");
+ goto fail_root_ca;
+ }
+
+ if (crypt_load_crt_str(im_ca_str, &ctx->im_ca) < 0) {
+ printf("Failed to load intermediate CA cert.\n");
+ goto fail_im_ca;
+ }
+
+ if (oap_auth_add_ca_crt(ctx->root_ca) < 0) {
+ printf("Failed to add root CA cert to store.\n");
+ goto fail_add_ca;
+ }
+
+ if (oap_auth_add_ca_crt(ctx->im_ca) < 0) {
+ printf("Failed to add intermediate CA cert to store.\n");
+ goto fail_add_ca;
+ }
+
+ return 0;
+
+ fail_add_ca:
+ crypt_free_crt(ctx->im_ca);
+ fail_im_ca:
+ crypt_free_crt(ctx->root_ca);
+ fail_root_ca:
+ oap_auth_fini();
+ fail_init:
+ memset(ctx, 0, sizeof(*ctx));
+ return -1;
+}
+
+void oap_test_teardown(struct oap_test_ctx * ctx)
+{
+ struct crypt_sk res;
+ buffer_t dummy = BUF_INIT;
+
+ if (ctx->cli.state != NULL) {
+ res.key = ctx->cli.key;
+ oap_cli_complete(ctx->cli.state, &ctx->cli.info, dummy,
+ &ctx->data, &res, NULL, NULL);
+ ctx->cli.state = NULL;
+ }
+
+ freebuf(ctx->data);
+ freebuf(ctx->resp_hdr);
+ freebuf(ctx->req_hdr);
+ freebuf(ctx->srv_crt);
+ freebuf(ctx->cli_crt);
+
+ crypt_free_crt(ctx->im_ca);
+ crypt_free_crt(ctx->root_ca);
+
+ oap_auth_fini();
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+int oap_cli_prepare_ctx(struct oap_test_ctx * ctx)
+{
+ return oap_cli_prepare(&ctx->cli.state, &ctx->cli.info, &ctx->req_hdr,
+ ctx->data, ctx->rekey);
+}
+
+int oap_srv_process_ctx(struct oap_test_ctx * ctx)
+{
+ struct crypt_sk res = { .nid = NID_undef, .key = ctx->srv.key };
+ int ret;
+
+ ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr,
+ &ctx->resp_hdr, &ctx->data, &res, ctx->rekey,
+ ctx->rekey ? &ctx->srv_crt : NULL,
+ ctx->rekey ? NULL : &ctx->srv_crt);
+ if (ret == 0)
+ ctx->srv.nid = res.nid;
+
+ return ret;
+}
+
+int oap_cli_complete_ctx(struct oap_test_ctx * ctx)
+{
+ struct crypt_sk res = { .nid = NID_undef, .key = ctx->cli.key };
+ int ret;
+
+ ret = oap_cli_complete(ctx->cli.state, &ctx->cli.info, ctx->resp_hdr,
+ &ctx->data, &res,
+ ctx->rekey ? &ctx->cli_crt : NULL,
+ ctx->rekey ? NULL : &ctx->cli_crt);
+ ctx->cli.state = NULL;
+
+ if (ret == 0)
+ ctx->cli.nid = res.nid;
+
+ return ret;
+}
+
+int roundtrip_auth_only(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid != NID_undef || ctx.srv.nid != NID_undef) {
+ printf("Cipher should not be set for auth-only.\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 roundtrip_rekey(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ 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;
+ }
+
+ 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;
+ }
+
+ /* Re-key: cert dropped on the wire, verified against the cache. */
+ 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;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int roundtrip_rekey_badcache(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ 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.cli_crt.len == 0) {
+ printf("Server cert was not cached.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the cached cert: the re-key must fail closed. */
+ ctx.cli_crt.data[ctx.cli_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("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Re-key accepted a corrupted cached cert.\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 roundtrip_kex_only(void)
+{
+ struct name_info cli_info;
+ struct name_info srv_info;
+ struct crypt_sk res;
+ uint8_t cli_key[SYMMKEYSZ];
+ uint8_t srv_key[SYMMKEYSZ];
+ int cli_nid;
+ int srv_nid;
+ buffer_t req_hdr = BUF_INIT;
+ buffer_t resp_hdr = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ void * cli_state = NULL;
+
+ TEST_START();
+
+ memset(&cli_info, 0, sizeof(cli_info));
+ memset(&srv_info, 0, sizeof(srv_info));
+
+ strcpy(cli_info.name, "test-1.unittest.o7s");
+ strcpy(srv_info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ if (oap_cli_prepare(&cli_state, &cli_info, &req_hdr,
+ data, false) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ res.key = srv_key;
+
+ if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res,
+ false, NULL, NULL) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ srv_nid = res.nid;
+
+ res.key = cli_key;
+
+ if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res,
+ NULL, NULL) < 0) {
+ printf("Client complete failed.\n");
+ cli_state = NULL;
+ goto fail_cleanup;
+ }
+
+ cli_nid = res.nid;
+ cli_state = NULL;
+
+ if (memcmp(cli_key, srv_key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ if (cli_nid == NID_undef || srv_nid == NID_undef) {
+ printf("Cipher should be set for kex-only.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(resp_hdr);
+ freebuf(req_hdr);
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ if (cli_state != NULL) {
+ res.key = cli_key;
+ oap_cli_complete(cli_state, &cli_info, resp_hdr, &data,
+ &res, NULL, NULL);
+ }
+ freebuf(resp_hdr);
+ freebuf(req_hdr);
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int corrupted_request(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the request */
+ if (ctx.req_hdr.len > 100) {
+ ctx.req_hdr.data[50] ^= 0xFF;
+ ctx.req_hdr.data[51] ^= 0xAA;
+ ctx.req_hdr.data[52] ^= 0x55;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject corrupted request.\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 corrupted_response(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+ struct crypt_sk res;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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;
+ }
+
+ /* Corrupt the response */
+ if (ctx.resp_hdr.len > 100) {
+ ctx.resp_hdr.data[50] ^= 0xFF;
+ ctx.resp_hdr.data[51] ^= 0xAA;
+ ctx.resp_hdr.data[52] ^= 0x55;
+ }
+
+ res.key = ctx.cli.key;
+
+ if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr,
+ &ctx.data, &res, NULL, NULL) == 0) {
+ printf("Client should reject corrupted response.\n");
+ ctx.cli.state = NULL;
+ goto fail_cleanup;
+ }
+
+ ctx.cli.state = NULL;
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int truncated_request(const char * root_ca,
+ const char * im_ca_str)
+{
+ struct oap_test_ctx ctx;
+ size_t orig_len;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Truncate the request buffer */
+ orig_len = ctx.req_hdr.len;
+ ctx.req_hdr.len = orig_len / 2;
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject truncated request.\n");
+ ctx.req_hdr.len = orig_len;
+ goto fail_cleanup;
+ }
+
+ ctx.req_hdr.len = orig_len;
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h
new file mode 100644
index 00000000..c47096fb
--- /dev/null
+++ b/src/irmd/oap/tests/common.h
@@ -0,0 +1,113 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Common test helper functions for OAP tests
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef IRMD_TESTS_COMMON_H
+#define IRMD_TESTS_COMMON_H
+
+#include <ouroboros/utils.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <test/test.h>
+
+#include <stdbool.h>
+
+/* Per-side security configuration for tests */
+struct test_sec_cfg {
+ int kex; /* KEX algorithm NID */
+ int cipher; /* Cipher NID for encryption */
+ int kdf; /* KDF NID for key derivation */
+ int md; /* Digest NID for signatures */
+ 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 */
+extern struct test_cfg {
+ struct test_sec_cfg srv;
+ struct test_sec_cfg cli;
+} test_cfg;
+
+/* Each test file defines this with its own certificates */
+extern int mock_load_credentials(void ** pkp,
+ void ** crt);
+
+/* Per-side test context */
+struct oap_test_side {
+ struct name_info info;
+ struct flow_info flow;
+ uint8_t key[SYMMKEYSZ];
+ int nid;
+ void * state;
+};
+
+/* Test context - holds all common state for OAP tests */
+struct oap_test_ctx {
+ struct oap_test_side srv;
+ struct oap_test_side cli;
+
+ buffer_t req_hdr;
+ buffer_t resp_hdr;
+ buffer_t data;
+ void * root_ca;
+ void * im_ca;
+
+ /* Re-key (tier iii): drop the cert, verify against the cache. */
+ bool rekey;
+ buffer_t srv_crt; /* client cert cached by server */
+ buffer_t cli_crt; /* server cert cached by client */
+};
+
+int oap_test_setup(struct oap_test_ctx * ctx,
+ const char * root_ca_str,
+ const char * im_ca_str);
+
+void oap_test_teardown(struct oap_test_ctx * ctx);
+
+int oap_cli_prepare_ctx(struct oap_test_ctx * ctx);
+
+int oap_srv_process_ctx(struct oap_test_ctx * ctx);
+
+int oap_cli_complete_ctx(struct oap_test_ctx * ctx);
+
+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);
+
+int roundtrip_rekey_badcache(const char * root_ca,
+ const char * im_ca_str);
+
+int roundtrip_kex_only(void);
+
+int corrupted_request(const char * root_ca,
+ const char * im_ca_str);
+
+int corrupted_response(const char * root_ca,
+ const char * im_ca_str);
+
+int truncated_request(const char * root_ca,
+ const char * im_ca_str);
+
+#endif /* IRMD_TESTS_COMMON_H */
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
new file mode 100644
index 00000000..fc10150b
--- /dev/null
+++ b/src/irmd/oap/tests/oap_test.c
@@ -0,0 +1,1926 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests of Ouroboros Allocation Protocol (OAP)
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #ifndef _DEFAULT_SOURCE
+ #define _DEFAULT_SOURCE
+ #endif
+#else
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include "config.h"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <ouroboros/random.h>
+#include <ouroboros/time.h>
+
+#include <test/test.h>
+#include <test/certs/ecdsa.h>
+
+#include "oap.h"
+#include "oap/auth.h"
+#include "common.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HAVE_OPENSSL
+#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[];
+
+struct test_cfg test_cfg;
+
+/* Mock load - called by load_*_credentials in common.c */
+int mock_load_credentials(void ** pkp,
+ void ** crt)
+{
+ *crt = NULL;
+
+ if (crypt_load_privkey_str(server_pkp_ec, pkp) < 0)
+ goto fail_privkey;
+
+ if (crypt_load_crt_str(signed_server_crt_ec, crt) < 0)
+ goto fail_crt;
+
+ return 0;
+
+ fail_crt:
+ crypt_free_key(*pkp);
+ fail_privkey:
+ *pkp = NULL;
+ return -1;
+}
+
+/* Stub KEM functions - ECDSA tests don't use KEM */
+int load_server_kem_keypair(__attribute__((unused)) const char * name,
+ __attribute__((unused)) bool raw_fmt,
+ __attribute__((unused)) void ** pkp)
+{
+ return -1;
+}
+
+int load_server_kem_pk(__attribute__((unused)) const char * name,
+ __attribute__((unused)) struct sec_config * cfg,
+ __attribute__((unused)) buffer_t * pk)
+{
+ return -1;
+}
+
+static void test_default_cfg(void)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: X25519, AES-256-GCM, SHA-256, with 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 = AUTH;
+
+ /* Client: same KEX/cipher/kdf/md, 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;
+}
+
+static int test_oap_auth_init_fini(void)
+{
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ TEST_START("(%s)", kex_str);
+
+ test_default_cfg();
+ test_cfg.srv.kex = kex;
+ test_cfg.cli.kex = kex;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip_auth_only(void)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth only, no encryption */
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: no auth, no encryption */
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_rekey(void)
+{
+ test_default_cfg();
+
+ return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_rekey_badcache(void)
+{
+ test_default_cfg();
+
+ return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_roundtrip_kex_only(void)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* 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;
+
+ /* 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 roundtrip_kex_only();
+}
+
+static int test_oap_piggyback_data(void)
+{
+ struct oap_test_ctx ctx;
+ const char * cli_data_str = "client_data";
+ const char * srv_data_str = "server_data";
+ buffer_t srv_data = BUF_INIT;
+
+ TEST_START();
+
+ test_default_cfg();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ /* Client prepares request with piggybacked data */
+ ctx.data.len = strlen(cli_data_str);
+ ctx.data.data = malloc(ctx.data.len);
+ if (ctx.data.data == NULL)
+ goto fail_cleanup;
+
+ memcpy(ctx.data.data, cli_data_str, ctx.data.len);
+
+ if (oap_cli_prepare_ctx(&ctx) < 0)
+ goto fail_cleanup;
+
+ /* Set server's response data (ctx.data will take cli data) */
+ srv_data.len = strlen(srv_data_str);
+ srv_data.data = (uint8_t *) srv_data_str;
+
+ freebuf(ctx.data);
+ ctx.data.data = srv_data.data;
+ ctx.data.len = srv_data.len;
+ srv_data.data = NULL;
+ srv_data.len = 0;
+
+ if (oap_srv_process_ctx(&ctx) < 0)
+ goto fail_cleanup;
+
+ /* Verify server received client's piggybacked data */
+ if (ctx.data.len != strlen(cli_data_str) ||
+ memcmp(ctx.data.data, cli_data_str, ctx.data.len) != 0) {
+ printf("Server did not receive correct client data.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(ctx.data);
+
+ if (oap_cli_complete_ctx(&ctx) < 0)
+ goto fail_cleanup;
+
+ /* Verify client received server's piggybacked data */
+ if (ctx.data.len != strlen(srv_data_str) ||
+ memcmp(ctx.data.data, srv_data_str, ctx.data.len) != 0) {
+ printf("Client did not receive correct server data.\n");
+ goto fail_cleanup;
+ }
+
+ /* Free the copied data */
+ free(ctx.data.data);
+ ctx.data.data = NULL;
+ ctx.data.len = 0;
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ freebuf(srv_data);
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_corrupted_request(void)
+{
+ test_default_cfg();
+ test_cfg.cli.auth = AUTH;
+
+ return corrupted_request(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_corrupted_response(void)
+{
+ test_default_cfg();
+
+ return corrupted_response(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_truncated_request(void)
+{
+ test_default_cfg();
+
+ return truncated_request(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+/* After ID (16), timestamp (8), cipher_nid (2), kdf_nid (2), md (2) */
+#define OAP_CERT_LEN_OFFSET 30
+static int test_oap_inflated_length_field(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t fake;
+
+ 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_CERT_LEN_OFFSET + 2) {
+ printf("Request too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Set cert length to claim more bytes than packet contains */
+ fake = hton16(60000);
+ memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject inflated length field.\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;
+}
+
+/* Attacker claims cert is smaller - causes misparse of subsequent fields */
+static int test_oap_deflated_length_field(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t fake;
+
+ 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_CERT_LEN_OFFSET + 2) {
+ printf("Request too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Set cert length to claim fewer bytes - will misparse rest */
+ fake = hton16(1);
+ memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject deflated length field.\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;
+}
+
+/* Header field offsets for byte manipulation */
+#define OAP_CIPHER_NID_OFFSET 24
+#define OAP_KEX_LEN_OFFSET 32
+
+/* Server rejects request when cipher NID set but no KEX data provided */
+static int test_oap_nid_without_kex(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t cipher_nid;
+ uint16_t zero = 0;
+
+ TEST_START();
+
+ /* 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;
+
+ 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;
+ }
+
+ /* Tamper: keep cipher_nid but set kex_len=0, truncate KEX data */
+ cipher_nid = hton16(NID_aes_256_gcm);
+ memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &cipher_nid,
+ sizeof(cipher_nid));
+ memcpy(ctx.req_hdr.data + OAP_KEX_LEN_OFFSET, &zero, sizeof(zero));
+ ctx.req_hdr.len = 36; /* Fixed header only, no KEX data */
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject cipher NID without KEX data.\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;
+}
+
+/* Server rejects OAP request with unsupported cipher NID */
+static int test_oap_unsupported_nid(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t bad_nid;
+
+ TEST_START();
+
+ /* 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;
+
+ 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;
+ }
+
+ /* 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));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject unsupported cipher NID.\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 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;
+
+ 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 key-confirm tag is the last field of an unsigned response */
+ ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a bad key-confirmation tag.\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;
+}
+
+static int test_oap_roundtrip_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]);
+
+ /* Skip KEM algorithms - tested in oap_test_ml_dsa */
+ if (IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_roundtrip(kex_supported_nids[i]);
+ }
+
+ return ret;
+}
+
+/* Cipher negotiation - strongest cipher and KDF are selected */
+static int test_oap_cipher_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: AES-128-GCM, SHA-256 */
+ test_cfg.srv.kex = NID_X25519;
+ test_cfg.srv.cipher = NID_aes_128_gcm;
+ test_cfg.srv.kdf = NID_sha256;
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: AES-256-GCM, SHA-512 */
+ test_cfg.cli.kex = NID_X25519;
+ test_cfg.cli.cipher = NID_aes_256_gcm;
+ test_cfg.cli.kdf = NID_sha512;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Verify: both should have the strongest cipher */
+ if (ctx.srv.nid != NID_aes_256_gcm) {
+ printf("Server cipher mismatch: expected %s, got %s\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.srv.nid));
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid != NID_aes_256_gcm) {
+ printf("Client cipher mismatch: expected %s, got %s\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.cli.nid));
+ goto fail_cleanup;
+ }
+
+ /* Parse response header to check negotiated KDF */
+ if (ctx.resp_hdr.len > 26) {
+ uint16_t resp_kdf_nid;
+ /* KDF NID at offset 26: ID(16) + ts(8) + cipher(2) */
+ resp_kdf_nid = ntoh16(*(uint16_t *)(ctx.resp_hdr.data + 26));
+
+ if (resp_kdf_nid != NID_sha512) {
+ printf("Response KDF mismatch: expected %s, got %s\n",
+ md_nid_to_str(NID_sha512),
+ md_nid_to_str(resp_kdf_nid));
+ 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;
+}
+
+/* Server encryption, client none: server rejects (no KEX data) */
+static int test_oap_srv_enc_cli_none(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: encryption configured */
+ 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 = AUTH;
+
+ /* Client: no encryption */
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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;
+ }
+
+ /* Server should reject: KEX required but client sent none */
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should have rejected.\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 encryption, server none: use client settings */
+static int test_oap_cli_enc_srv_none(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: no encryption configured */
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: encryption configured */
+ 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;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Key mismatch.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid != NID_aes_256_gcm) {
+ printf("Expected %s, got %s.\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.cli.nid));
+ goto fail_cleanup;
+ }
+
+ if (ctx.srv.nid != NID_aes_256_gcm) {
+ printf("Expected %s, got %s.\n",
+ crypt_nid_to_str(NID_aes_256_gcm),
+ crypt_nid_to_str(ctx.srv.nid));
+ 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;
+}
+
+/* Unauthenticated server: client floor-rejects a downgraded cipher */
+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_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;
+
+ 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;
+ }
+
+ /* Tamper: replace cipher NID with weaker one */
+ weak = hton16(NID_aes_128_gcm);
+ memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
+ &weak, sizeof(weak));
+
+ /* Client should reject the downgraded cipher */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted downgrade.\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;
+}
+
+/*
+ * Suite binding: a cipher swapped to a higher rank clears the client floor
+ * check, but the bound key commits to the negotiated suite, so the swap must
+ * still fail key confirmation.
+ */
+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_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;
+
+ 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;
+ }
+
+ /* Swap the response cipher to a higher-ranked one */
+ swap = hton16(NID_aes_256_gcm);
+ memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
+ &swap, sizeof(swap));
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a swapped cipher suite.\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;
+}
+
+/* Server rejects client with weaker KEX algorithm */
+static int test_oap_srv_rejects_weak_kex(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: secp521r1 (strong) */
+ test_cfg.srv.kex = NID_secp521r1;
+ test_cfg.srv.cipher = NID_aes_256_gcm;
+ test_cfg.srv.kdf = NID_sha256;
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: ffdhe2048 (weakest) */
+ test_cfg.cli.kex = NID_ffdhe2048;
+ 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;
+
+ 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;
+ }
+
+ /* Server should reject: client KEX too weak */
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject weak KEX.\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;
+}
+
+/* Test roundtrip with different signature digest algorithms */
+static int test_oap_roundtrip_md(int md)
+{
+ struct oap_test_ctx ctx;
+ const char * md_str = md_nid_to_str(md);
+
+ TEST_START("(%s)", md_str ? md_str : "default");
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth + KEX with specified md */
+ test_cfg.srv.kex = NID_X25519;
+ test_cfg.srv.cipher = NID_aes_256_gcm;
+ test_cfg.srv.kdf = NID_sha256;
+ test_cfg.srv.md = md;
+ test_cfg.srv.auth = AUTH;
+
+ /* Client: 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 = md;
+ test_cfg.cli.auth = NO_AUTH;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", md_str ? md_str : "default");
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", md_str ? md_str : "default");
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_roundtrip_md_all(void)
+{
+ int ret = 0;
+ int i;
+
+ /* Test with default */
+ ret |= test_oap_roundtrip_md(NID_undef);
+
+ /* Test with all supported digest NIDs */
+ for (i = 0; md_supported_nids[i] != NID_undef; i++)
+ ret |= test_oap_roundtrip_md(md_supported_nids[i]);
+
+ return ret;
+}
+
+/* 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)
+{
+ struct oap_test_ctx ctx;
+ struct timespec old_ts;
+ uint64_t old_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 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));
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject outdated packet.\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;
+}
+
+/* Test that packets from the future are rejected */
+static int test_oap_future_packet(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();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Test that replayed packets (same ID + timestamp) are rejected */
+static int test_oap_replay_packet(void)
+{
+ struct oap_test_ctx ctx;
+ buffer_t saved_req;
+
+ 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;
+ }
+
+ /* Save the request for replay */
+ saved_req.len = ctx.req_hdr.len;
+ saved_req.data = malloc(saved_req.len);
+ if (saved_req.data == NULL) {
+ printf("Failed to allocate saved request.\n");
+ goto fail_cleanup;
+ }
+ memcpy(saved_req.data, ctx.req_hdr.data, saved_req.len);
+
+ /* First request should succeed */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("First request should succeed.\n");
+ free(saved_req.data);
+ goto fail_cleanup;
+ }
+
+ /* Free response from first request before replay */
+ freebuf(ctx.resp_hdr);
+
+ /* Restore the saved request for replay */
+ freebuf(ctx.req_hdr);
+ ctx.req_hdr = saved_req;
+
+ /* Replay must return -EREPLAY so callers can drop silently. */
+ if (oap_srv_process_ctx(&ctx) != -EREPLAY) {
+ printf("Replayed packet rejection != -EREPLAY.\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;
+}
+
+/* Encode a distinct OAP session ID from an index */
+static void make_id(uint8_t * id,
+ size_t idx)
+{
+ memset(id, 0, OAP_ID_SIZE);
+ memcpy(id, &idx, sizeof(idx));
+}
+
+/*
+ * Replay cache fails closed at capacity: a flood is rejected and no genuine
+ * entry is evicted (so it cannot be replayed).
+ */
+static int test_oap_replay_cap(void)
+{
+ struct oap_hdr h;
+ struct timespec now;
+ uint8_t id[OAP_ID_SIZE];
+ uint64_t stamp;
+ size_t i;
+
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = TS_TO_UINT64(now);
+
+ memset(&h, 0, sizeof(h));
+ h.id.data = id;
+ h.id.len = OAP_ID_SIZE;
+ h.timestamp = stamp;
+
+ /* Fill one generation bucket to capacity with distinct IDs */
+ for (i = 0; i < OAP_REPLAY_MAX; i++) {
+ make_id(id, i);
+ if (oap_check_hdr(&h) != 0) {
+ printf("Distinct header %zu rejected.\n", i);
+ goto fail_fini;
+ }
+ }
+
+ /* One past capacity fails closed (rejected, not evict-oldest) */
+ make_id(id, OAP_REPLAY_MAX);
+ if (oap_check_hdr(&h) != -EAUTH) {
+ printf("Header past capacity not fail-closed.\n");
+ goto fail_fini;
+ }
+
+ /* No genuine entry was evicted: the oldest still reads as a replay */
+ make_id(id, 0);
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Genuine entry evicted under flood.\n");
+ goto fail_fini;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_fini:
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Distinct timestamp generations use separate buckets and are detected
+ * independently (covers the multi-generation / rotation path).
+ */
+static int test_oap_replay_generations(void)
+{
+ struct oap_hdr h;
+ struct timespec now;
+ uint8_t id[OAP_ID_SIZE];
+ uint64_t cur;
+ uint64_t gen_ns;
+ uint64_t stamp_a;
+ uint64_t stamp_b;
+
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ gen_ns = (uint64_t) OAP_REPLAY_TIMER * BILLION;
+
+ /* stamp_a in the current generation, stamp_b one generation older */
+ stamp_a = cur;
+ stamp_b = (cur / gen_ns) * gen_ns - 1;
+
+ memset(&h, 0, sizeof(h));
+ h.id.data = id;
+ h.id.len = OAP_ID_SIZE;
+ make_id(id, 1);
+
+ /* First sighting in each generation is accepted */
+ h.timestamp = stamp_a;
+ if (oap_check_hdr(&h) != 0) {
+ printf("Gen-A header rejected.\n");
+ goto fail_fini;
+ }
+
+ h.timestamp = stamp_b;
+ if (oap_check_hdr(&h) != 0) {
+ printf("Gen-B header rejected.\n");
+ goto fail_fini;
+ }
+
+ /* Each generation independently detects its own replay */
+ h.timestamp = stamp_a;
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Gen-A replay not detected.\n");
+ goto fail_fini;
+ }
+
+ h.timestamp = stamp_b;
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Gen-B replay not detected.\n");
+ goto fail_fini;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_fini:
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server rejects client certificate when root CA is missing from store */
+static int test_oap_missing_root_ca(void)
+{
+ struct oap_test_ctx ctx;
+ void * im_ca = NULL;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ memset(&ctx, 0, sizeof(ctx));
+
+ strcpy(ctx.srv.info.name, "test-1.unittest.o7s");
+ strcpy(ctx.cli.info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ /* Load intermediate CA but intentionally omit the root CA */
+ if (crypt_load_crt_str(im_ca_crt_ec, &im_ca) < 0) {
+ printf("Failed to load intermediate CA cert.\n");
+ goto fail_fini;
+ }
+
+ ctx.im_ca = im_ca;
+
+ if (oap_auth_add_ca_crt(im_ca) < 0) {
+ printf("Failed to add intermediate CA cert to store.\n");
+ goto fail_fini;
+ }
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_fini;
+ }
+
+ /* Server processes and signs response - succeeds without root CA */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_teardown;
+ }
+
+ /* Client verifies server certificate against trust store:
+ * should reject because root CA is not in the store */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject without root CA.\n");
+ goto fail_teardown;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_teardown:
+ oap_test_teardown(&ctx);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+ fail_fini:
+ crypt_free_crt(im_ca);
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Test that client rejects server with wrong certificate name */
+static int test_oap_server_name_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ /* Set client's expected name to something different from cert name */
+ strcpy(ctx.cli.info.name, "wrong.server.name");
+
+ 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;
+ }
+
+ /* Client should reject due to name mismatch */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject server with wrong cert name.\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 requiring auth rejects a response without certificate */
+static int test_oap_cli_requires_srv_auth(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.req_auth = true;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject unauthenticated server.\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;
+}
+
+/* Server requiring auth rejects a request without certificate */
+static int test_oap_srv_requires_cli_auth(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.req_auth = true;
+
+ 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 should reject unauthenticated client.\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;
+}
+
+/* Roundtrip succeeds when both sides require and provide auth */
+static int test_oap_mutual_req_auth(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.req_auth = true;
+ test_cfg.cli.auth = AUTH;
+ test_cfg.cli.req_auth = true;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\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 server signature with a different digest */
+static int test_oap_cli_rejects_md_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.md = NID_sha384;
+
+ 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;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject digest mismatch.\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;
+}
+
+/* Server rejects a client signature with a different digest */
+static int test_oap_srv_rejects_md_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.cli.auth = AUTH;
+ test_cfg.cli.md = NID_sha384;
+
+ 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 should reject digest mismatch.\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;
+}
+
+/* Naive substring search over raw bytes (memmem is not portable here). */
+static bool buf_contains(const uint8_t * hay,
+ size_t hlen,
+ const uint8_t * needle,
+ size_t nlen)
+{
+ size_t i;
+
+ if (nlen == 0 || nlen > hlen)
+ return false;
+
+ for (i = 0; i + nlen <= hlen; i++) {
+ if (memcmp(hay + i, needle, nlen) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+/* The server certificate must not appear in cleartext on the wire */
+static int test_oap_server_cert_hidden(void)
+{
+ struct oap_test_ctx ctx;
+ void * crt = NULL;
+ buffer_t der = BUF_INIT;
+
+ 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 (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
+ printf("Failed to load server crt.\n");
+ goto fail_cleanup;
+ }
+
+ if (crypt_crt_der(crt, &der) < 0) {
+ printf("Failed to DER-encode server crt.\n");
+ goto fail_crt;
+ }
+
+ if (der.len == 0 || der.len > ctx.resp_hdr.len) {
+ printf("Unexpected cert/response sizes.\n");
+ goto fail_der;
+ }
+
+ if (buf_contains(ctx.resp_hdr.data, ctx.resp_hdr.len,
+ der.data, der.len)) {
+ printf("Server certificate found in cleartext.\n");
+ goto fail_der;
+ }
+
+ /* The handshake must still complete and agree on a key */
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_der;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_der;
+ }
+
+ freebuf(der);
+ crypt_free_crt(crt);
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_der:
+ freebuf(der);
+ fail_crt:
+ crypt_free_crt(crt);
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Tampering the sealed identity block fails the handshake */
+static int test_oap_sealed_tamper(void)
+{
+ struct oap_test_ctx ctx;
+ size_t pos;
+
+ 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 (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.resp_hdr.len < 64) {
+ printf("Response too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Flip a byte inside the sealed ciphertext, before the AEAD tag */
+ pos = ctx.resp_hdr.len - 32;
+ ctx.resp_hdr.data[pos] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a tampered identity block.\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)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_oap_auth_init_fini();
+ ret |= test_oap_replay_cap();
+ ret |= test_oap_replay_generations();
+
+#ifdef HAVE_OPENSSL
+ 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_roundtrip_all();
+ ret |= test_oap_roundtrip_md_all();
+
+ ret |= test_oap_corrupted_request();
+ ret |= test_oap_corrupted_response();
+ ret |= test_oap_key_confirm_mismatch();
+ ret |= test_oap_truncated_request();
+ 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_cipher_mismatch();
+ ret |= test_oap_srv_enc_cli_none();
+ ret |= test_oap_cli_enc_srv_none();
+ ret |= test_oap_cli_rejects_downgrade();
+ 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_replay_packet();
+ ret |= test_oap_missing_root_ca();
+ ret |= test_oap_server_name_mismatch();
+
+ ret |= test_oap_cli_requires_srv_auth();
+ ret |= test_oap_srv_requires_cli_auth();
+ ret |= test_oap_mutual_req_auth();
+
+
+ ret |= test_oap_cli_rejects_md_mismatch();
+ ret |= test_oap_srv_rejects_md_mismatch();
+
+ ret |= test_oap_server_cert_hidden();
+ ret |= test_oap_sealed_tamper();
+#else
+ (void) test_oap_roundtrip_auth_only;
+ (void) test_oap_roundtrip_kex_only;
+ (void) test_oap_piggyback_data;
+ (void) test_oap_roundtrip;
+ (void) test_oap_roundtrip_all;
+ (void) test_oap_roundtrip_md;
+ (void) test_oap_roundtrip_md_all;
+ (void) test_oap_corrupted_request;
+ (void) test_oap_corrupted_response;
+ (void) test_oap_key_confirm_mismatch;
+ (void) test_oap_truncated_request;
+ (void) test_oap_inflated_length_field;
+ (void) test_oap_deflated_length_field;
+ (void) test_oap_nid_without_kex;
+ (void) test_oap_unsupported_nid;
+ (void) test_oap_cipher_mismatch;
+ (void) test_oap_srv_enc_cli_none;
+ (void) test_oap_cli_enc_srv_none;
+ (void) test_oap_cli_rejects_downgrade;
+ (void) test_oap_cli_rejects_suite_swap;
+ (void) test_oap_srv_rejects_weak_kex;
+ (void) test_oap_outdated_packet;
+ (void) test_oap_future_packet;
+ (void) test_oap_replay_packet;
+ (void) test_oap_replay_generations;
+ (void) test_oap_missing_root_ca;
+ (void) test_oap_server_name_mismatch;
+ (void) test_oap_cli_requires_srv_auth;
+ (void) test_oap_srv_requires_cli_auth;
+ (void) test_oap_mutual_req_auth;
+ (void) test_oap_cli_rejects_md_mismatch;
+ (void) test_oap_srv_rejects_md_mismatch;
+ (void) test_oap_server_cert_hidden;
+ (void) test_oap_sealed_tamper;
+ (void) test_oap_rekey;
+ (void) test_oap_rekey_badcache;
+
+ ret = TEST_RC_SKIP;
+#endif
+ crypt_cleanup();
+
+ return ret;
+}
diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c
new file mode 100644
index 00000000..8691aa00
--- /dev/null
+++ b/src/irmd/oap/tests/oap_test_ml_dsa.c
@@ -0,0 +1,491 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests of OAP ML-KEM/ML-DSA key exchange
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#if defined(__linux__) || defined(__CYGWIN__)
+#define _DEFAULT_SOURCE
+#else
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include "config.h"
+
+#include <ouroboros/crypt.h>
+#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
+#include <ouroboros/random.h>
+#include <test/test.h>
+
+#include <test/certs/ml_dsa.h>
+
+#include "oap.h"
+#include "common.h"
+
+#include <stdbool.h>
+#include <string.h>
+
+#ifdef HAVE_OPENSSL
+#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
+
+extern const uint16_t kex_supported_nids[];
+extern const uint16_t md_supported_nids[];
+
+static int get_random_kdf(void)
+{
+ static int idx = 0;
+ int count;
+
+ if (md_supported_nids[0] == NID_undef)
+ return NID_undef;
+
+ for (count = 0; md_supported_nids[count] != NID_undef; count++)
+ ;
+
+ return md_supported_nids[(idx++) % count];
+}
+
+struct test_cfg test_cfg;
+
+/* KEM keypair storage for tests (server-side keypair for KEM modes) */
+static void * test_kem_pkp = NULL; /* Private key pair */
+static uint8_t test_kem_pk[4096]; /* Public key buffer */
+static size_t test_kem_pk_len = 0;
+
+/* Mock load - called by load_*_credentials in common.c */
+int mock_load_credentials(void ** pkp,
+ void ** crt)
+{
+ *pkp = NULL;
+ *crt = NULL;
+
+ if (crypt_load_privkey_str(server_pkp_ml, pkp) < 0)
+ return -1;
+
+ if (crypt_load_crt_str(signed_server_crt_ml, crt) < 0) {
+ crypt_free_key(*pkp);
+ *pkp = NULL;
+ return -1;
+ }
+
+ return 0;
+}
+
+int load_server_kem_keypair(const char * name,
+ bool raw_fmt,
+ void ** pkp)
+{
+#ifdef HAVE_OPENSSL
+ struct sec_config local_cfg;
+ ssize_t pk_len;
+
+ (void) name;
+ (void) raw_fmt;
+
+ /*
+ * Uses reference counting. The caller will call
+ * EVP_PKEY_free which decrements the count.
+ */
+ if (test_kem_pkp != NULL) {
+ if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1)
+ return -1;
+
+ *pkp = test_kem_pkp;
+ return 0;
+ }
+
+ /*
+ * Generate a new KEM keypair from test_cfg.srv.kex.
+ */
+ memset(&local_cfg, 0, sizeof(local_cfg));
+ if (test_cfg.srv.kex == NID_undef)
+ goto fail;
+
+ SET_KEX_ALGO_NID(&local_cfg, test_cfg.srv.kex);
+
+ pk_len = kex_pkp_create(&local_cfg, &test_kem_pkp, test_kem_pk);
+ if (pk_len < 0)
+ goto fail;
+
+ test_kem_pk_len = (size_t) pk_len;
+
+ if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1)
+ goto fail_ref;
+
+ *pkp = test_kem_pkp;
+
+ return 0;
+ fail_ref:
+ kex_pkp_destroy(test_kem_pkp);
+ test_kem_pkp = NULL;
+ test_kem_pk_len = 0;
+ fail:
+ return -1;
+
+#else
+ (void) name;
+ (void) raw_fmt;
+ (void) pkp;
+ return -1;
+#endif
+}
+
+int load_server_kem_pk(const char * name,
+ struct sec_config * cfg,
+ buffer_t * pk)
+{
+ ssize_t len;
+
+ (void) name;
+
+ if (test_kem_pk_len > 0) {
+ pk->data = malloc(test_kem_pk_len);
+ if (pk->data == NULL)
+ return -1;
+ memcpy(pk->data, test_kem_pk, test_kem_pk_len);
+ pk->len = test_kem_pk_len;
+ return 0;
+ }
+
+ /* Generate keypair on demand if not already done */
+ len = kex_pkp_create(cfg, &test_kem_pkp, test_kem_pk);
+ if (len < 0)
+ return -1;
+
+ test_kem_pk_len = (size_t) len;
+ pk->data = malloc(test_kem_pk_len);
+ if (pk->data == NULL)
+ return -1;
+
+ memcpy(pk->data, test_kem_pk, test_kem_pk_len);
+ pk->len = test_kem_pk_len;
+
+ return 0;
+}
+
+static void reset_kem_state(void)
+{
+ if (test_kem_pkp != NULL) {
+ kex_pkp_destroy(test_kem_pkp);
+ test_kem_pkp = NULL;
+ }
+ test_kem_pk_len = 0;
+}
+
+static void test_cfg_init(int kex,
+ int cipher,
+ int kdf,
+ int kem_mode,
+ bool cli_auth)
+{
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server config */
+ test_cfg.srv.kex = kex;
+ test_cfg.srv.cipher = cipher;
+ test_cfg.srv.kdf = kdf;
+ test_cfg.srv.kem_mode = kem_mode;
+ test_cfg.srv.auth = true;
+
+ /* Client config */
+ test_cfg.cli.kex = kex;
+ test_cfg.cli.cipher = cipher;
+ test_cfg.cli.kdf = kdf;
+ test_cfg.cli.kem_mode = kem_mode;
+ test_cfg.cli.auth = cli_auth;
+}
+
+static int oap_test_setup_kem(struct oap_test_ctx * ctx,
+ const char * root_ca,
+ const char * im_ca)
+{
+ reset_kem_state();
+ return oap_test_setup(ctx, root_ca, im_ca);
+}
+
+static void oap_test_teardown_kem(struct oap_test_ctx * ctx)
+{
+ oap_test_teardown(ctx);
+}
+
+static int test_oap_roundtrip_auth_only(void)
+{
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, false);
+
+ return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+/* 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.cli.md = NID_sha256;
+
+ return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_srv_md_pin_exempts_pqc(void)
+{
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, CLI_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)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, NO_CLI_AUTH);
+
+ return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_rekey_badcache(void)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, NO_CLI_AUTH);
+
+ return roundtrip_rekey_badcache(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_corrupted_request(void)
+{
+ test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
+ SRV_ENCAP, CLI_AUTH);
+
+ return corrupted_request(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_corrupted_response(void)
+{
+ test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
+ SRV_ENCAP, NO_CLI_AUTH);
+
+ return corrupted_response(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_truncated_request(void)
+{
+ test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
+ SRV_ENCAP, NO_CLI_AUTH);
+
+ return truncated_request(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_roundtrip_kem(int kex,
+ int kem_mode)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(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);
+
+ 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("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid == NID_undef ||
+ ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\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_roundtrip_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_roundtrip_kem(kex_supported_nids[i], SRV_ENCAP);
+ ret |= test_oap_roundtrip_kem(kex_supported_nids[i], CLI_ENCAP);
+ }
+
+ return ret;
+}
+
+static int test_oap_kem_srv_uncfg(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Server: auth only, no KEX configured */
+ test_cfg.srv.auth = true;
+
+ /* Client: requests KEM with server-side encapsulation */
+ test_cfg.cli.kex = kex;
+ test_cfg.cli.cipher = NID_aes_256_gcm;
+ test_cfg.cli.kdf = get_random_kdf();
+ test_cfg.cli.kem_mode = SRV_ENCAP;
+ test_cfg.cli.auth = false;
+
+ 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 process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid == NID_undef ||
+ ctx.srv.nid == NID_undef) {
+ printf("Cipher not set in flow.\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_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ const char * algo;
+
+ algo = kex_nid_to_str(kex_supported_nids[i]);
+
+ if (!IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_kem_srv_uncfg(kex_supported_nids[i]);
+ }
+
+ return ret;
+}
+
+int oap_test_ml_dsa(int argc,
+ char **argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+#ifdef HAVE_OPENSSL_ML_KEM
+ ret |= test_oap_roundtrip_auth_only();
+ ret |= test_oap_cli_md_pin_exempts_pqc();
+ ret |= test_oap_srv_md_pin_exempts_pqc();
+
+ ret |= test_oap_roundtrip_kem_all();
+
+ ret |= test_oap_kem_srv_uncfg_all();
+
+ ret |= test_oap_corrupted_request();
+ ret |= test_oap_corrupted_response();
+ ret |= test_oap_truncated_request();
+
+ ret |= test_oap_rekey();
+ ret |= test_oap_rekey_badcache();
+#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_badcache;
+ (void) test_oap_roundtrip_kem;
+ (void) test_oap_roundtrip_kem_all;
+ (void) test_oap_kem_srv_uncfg;
+ (void) test_oap_kem_srv_uncfg_all;
+ (void) test_oap_corrupted_request;
+ (void) test_oap_corrupted_response;
+ (void) test_oap_truncated_request;
+
+ ret = TEST_RC_SKIP;
+#endif
+ crypt_cleanup();
+
+ return ret;
+}
diff --git a/src/irmd/reg/CMakeLists.txt b/src/irmd/reg/CMakeLists.txt
deleted file mode 100644
index ff9d2e99..00000000
--- a/src/irmd/reg/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
-include_directories(${CMAKE_CURRENT_BINARY_DIR})
-
-include_directories(${CMAKE_SOURCE_DIR}/include)
-include_directories(${CMAKE_BINARY_DIR}/include)
-
-add_subdirectory(tests)
diff --git a/src/irmd/reg/flow.c b/src/irmd/reg/flow.c
index 4d091b23..8be2dfc7 100644
--- a/src/irmd/reg/flow.c
+++ b/src/irmd/reg/flow.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows
*
@@ -24,6 +24,7 @@
#define OUROBOROS_PREFIX "reg/flow"
+#include <ouroboros/crypt.h>
#include <ouroboros/logs.h>
#include "flow.h"
@@ -32,6 +33,7 @@
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
+#include <string.h>
struct reg_flow * reg_flow_create(const struct flow_info * info)
{
@@ -42,6 +44,7 @@ struct reg_flow * reg_flow_create(const struct flow_info * info)
assert(info->n_pid != 0);
assert(info->n_1_pid == 0);
assert(info->mpl == 0);
+ assert(info->mtu == 0);
assert(info->state == FLOW_INIT);
flow = malloc(sizeof(*flow));
@@ -66,11 +69,13 @@ struct reg_flow * reg_flow_create(const struct flow_info * info)
static void destroy_rbuffs(struct reg_flow * flow)
{
if (flow->n_rb != NULL)
- shm_rbuff_destroy(flow->n_rb);
+ ssm_rbuff_destroy(flow->n_rb);
+
flow->n_rb = NULL;
if (flow->n_1_rb != NULL)
- shm_rbuff_destroy(flow->n_1_rb);
+ ssm_rbuff_destroy(flow->n_1_rb);
+
flow->n_1_rb = NULL;
}
@@ -78,9 +83,14 @@ void reg_flow_destroy(struct reg_flow * flow)
{
assert(flow != NULL);
+ if (flow->rk.pending_seed != NULL)
+ crypt_secure_free(flow->rk.pending_seed, SYMMKEYSZ);
+
+ freebuf(flow->rk.peer_crt);
+
switch(flow->info.state) {
case FLOW_ACCEPT_PENDING:
- clrbuf(flow->data);
+ clrbuf(flow->req_data);
/* FALLTHRU */
default:
destroy_rbuffs(flow);
@@ -89,8 +99,10 @@ void reg_flow_destroy(struct reg_flow * flow)
assert(flow->n_rb == NULL);
assert(flow->n_1_rb == NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
assert(list_is_empty(&flow->next));
@@ -103,22 +115,28 @@ static int create_rbuffs(struct reg_flow * flow,
assert(flow != NULL);
assert(info != NULL);
- flow->n_rb = shm_rbuff_create(info->n_pid, info->id);
+ flow->n_rb = ssm_rbuff_create(info->n_pid, info->id);
if (flow->n_rb == NULL)
goto fail_n_rb;
+ if (ssm_rbuff_mlock(flow->n_rb) < 0)
+ log_warn("Failed to mlock n_rb for flow %d.", info->id);
+
assert(flow->info.n_1_pid == 0);
assert(flow->n_1_rb == NULL);
flow->info.n_1_pid = info->n_1_pid;
- flow->n_1_rb = shm_rbuff_create(info->n_1_pid, info->id);
+ flow->n_1_rb = ssm_rbuff_create(info->n_1_pid, info->id);
if (flow->n_1_rb == NULL)
goto fail_n_1_rb;
+ if (ssm_rbuff_mlock(flow->n_1_rb) < 0)
+ log_warn("Failed to mlock n_1_rb for flow %d.", info->id);
+
return 0;
fail_n_1_rb:
- shm_rbuff_destroy(flow->n_rb);
+ ssm_rbuff_destroy(flow->n_rb);
fail_n_rb:
return -ENOMEM;
}
@@ -152,6 +170,7 @@ int reg_flow_update(struct reg_flow * flow,
assert(info->mpl != 0);
flow->info.mpl = info->mpl;
+ flow->info.mtu = info->mtu;
if (flow->info.state == FLOW_ALLOC_PENDING)
break;
@@ -172,6 +191,7 @@ int reg_flow_update(struct reg_flow * flow,
}
flow->info.state = info->state;
+ flow->info.uid = info->uid;
*info = flow->info;
@@ -179,30 +199,3 @@ int reg_flow_update(struct reg_flow * flow,
fail:
return -ENOMEM;
}
-
-void reg_flow_set_data(struct reg_flow * flow,
- const buffer_t * buf)
-{
- assert(flow != NULL);
- assert(buf != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
-
- flow->data = *buf;
-}
-
-void reg_flow_get_data(struct reg_flow * flow,
- buffer_t * buf)
-{
- assert(flow != NULL);
- assert(buf != NULL);
-
- *buf = flow->data;
-
- clrbuf(flow->data);
-}
-
-void reg_flow_free_data(struct reg_flow * flow)
-{
- freebuf(flow->data);
-}
diff --git a/src/irmd/reg/flow.h b/src/irmd/reg/flow.h
index 75ada971..166bed61 100644
--- a/src/irmd/reg/flow.h
+++ b/src/irmd/reg/flow.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows
*
@@ -25,24 +25,48 @@
#include <ouroboros/list.h>
#include <ouroboros/flow.h>
+#include <ouroboros/name.h>
#include <ouroboros/pthread.h>
#include <ouroboros/qos.h>
-#include <ouroboros/shm_rbuff.h>
+#include <ouroboros/ssm_rbuff.h>
#include <ouroboros/utils.h>
+#include <stdbool.h>
#include <sys/types.h>
#include <time.h>
struct reg_flow {
- struct list_head next;
+ struct list_head next;
- struct flow_info info;
+ struct flow_info info;
+ int response;
- buffer_t data;
- struct timespec t0;
+ buffer_t req_data;
+ buffer_t rsp_data;
+ struct timespec t0;
- struct shm_rbuff * n_rb;
- struct shm_rbuff * n_1_rb;
+ char name[NAME_SIZE + 1];
+
+ bool direct;
+
+ /* Tier-2 re-key state (encrypted flows only) */
+ struct {
+ bool encrypted; /* flow carries a cipher */
+ uint8_t epoch; /* last epoch installed by app */
+ bool initiator; /* OAP initiator (role 0) */
+ bool in_flight; /* a re-key is in progress */
+ bool req_queued; /* a peer REQ is in the inbox */
+ bool resp_queued; /* a peer RESP is in the inbox */
+ uint8_t * pending_seed; /* secure heap; NULL until set */
+ uint8_t pending_epoch;
+ bool pending_initiator; /* pending seed: oap_cli side */
+ bool has_pending; /* new seed awaits app pull */
+ uint8_t pulled; /* direct: per-app pull mask */
+ buffer_t peer_crt; /* peer cert DER, cached at HS */
+ } rk;
+
+ struct ssm_rbuff * n_rb;
+ struct ssm_rbuff * n_1_rb;
};
struct reg_flow * reg_flow_create(const struct flow_info * info);
@@ -52,12 +76,4 @@ void reg_flow_destroy(struct reg_flow * flow);
int reg_flow_update(struct reg_flow * flow,
struct flow_info * info);
-void reg_flow_set_data(struct reg_flow * flow,
- const buffer_t * buf);
-
-void reg_flow_get_data(struct reg_flow * flow,
- buffer_t * buf);
-
-void reg_flow_free_data(struct reg_flow * flow);
-
#endif /* OUROBOROS_IRMD_REG_FLOW_H */
diff --git a/src/irmd/reg/ipcp.c b/src/irmd/reg/ipcp.c
index 6580cb5b..b193e28f 100644
--- a/src/irmd/reg/ipcp.c
+++ b/src/irmd/reg/ipcp.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs
*
@@ -40,7 +40,7 @@ struct reg_ipcp * reg_ipcp_create(const struct ipcp_info * info)
struct reg_ipcp * ipcp;
assert(info != NULL);
- assert(info->state == IPCP_BOOT);
+ assert(info->state == IPCP_INIT);
ipcp = malloc(sizeof(*ipcp));
if (ipcp == NULL) {
@@ -54,7 +54,7 @@ struct reg_ipcp * reg_ipcp_create(const struct ipcp_info * info)
list_head_init(&ipcp->next);
ipcp->info = *info;
- ipcp->info.state = IPCP_BOOT;
+ ipcp->info.state = IPCP_INIT;
strcpy(ipcp->layer.name, "Not enrolled.");
@@ -77,7 +77,6 @@ void reg_ipcp_update(struct reg_ipcp * ipcp,
const struct ipcp_info * info)
{
assert(ipcp != NULL);
- assert(info->state != IPCP_INIT);
ipcp->info = *info;
}
@@ -86,7 +85,7 @@ void reg_ipcp_set_layer(struct reg_ipcp * ipcp,
const struct layer_info * info)
{
assert(ipcp != NULL);
- assert(ipcp->info.state == IPCP_OPERATIONAL);
+ assert(ipcp->info.state == IPCP_BOOT);
ipcp->layer = *info;
}
diff --git a/src/irmd/reg/ipcp.h b/src/irmd/reg/ipcp.h
index 375973a7..fe9d3bf1 100644
--- a/src/irmd/reg/ipcp.h
+++ b/src/irmd/reg/ipcp.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs
*
diff --git a/src/irmd/reg/name.c b/src/irmd/reg/name.c
index 1ac939a5..61a328ec 100644
--- a/src/irmd/reg/name.c
+++ b/src/irmd/reg/name.c
@@ -1,6 +1,6 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names
*
@@ -66,15 +66,14 @@ struct reg_name * reg_name_create(const struct name_info * info)
goto fail_malloc;
}
+ memset(name, 0, sizeof(*name));
+
list_head_init(&name->next);
- list_head_init(&name->progs);
- list_head_init(&name->procs);
- list_head_init(&name->active);
+ llist_init(&name->progs);
+ llist_init(&name->procs);
+ llist_init(&name->active);
- name->info = *info;
- name->n_progs = 0;
- name->n_procs = 0;
- name->n_active = 0;
+ name->info = *info;
return name;
@@ -88,13 +87,9 @@ void reg_name_destroy(struct reg_name * name)
assert(list_is_empty(&name->next));
- assert(name->n_progs == 0);
- assert(name->n_procs == 0);
- assert(name->n_active == 0);
-
- assert(list_is_empty(&name->progs));
- assert(list_is_empty(&name->procs));
- assert(list_is_empty(&name->active));
+ assert(llist_is_empty(&name->progs));
+ assert(llist_is_empty(&name->procs));
+ assert(llist_is_empty(&name->active));
free(name);
}
@@ -107,7 +102,7 @@ static struct proc_entry * __reg_name_get_active(const struct reg_name * name,
assert(name != NULL);
assert(pid > 0);
- list_for_each(p, &name->active) {
+ llist_for_each(p, &name->active) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid)
@@ -123,13 +118,12 @@ static void __reg_name_del_all_active(struct reg_name * name,
struct list_head * p;
struct list_head * h;
- list_for_each_safe(p, h, &name->active) {
+ llist_for_each_safe(p, h, &name->active) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid) {
- list_del(&entry->next);
+ llist_del(&entry->next, &name->active);
free(entry);
- name->n_active--;
}
}
}
@@ -142,7 +136,7 @@ static struct proc_entry * __reg_name_get_proc(const struct reg_name * name,
assert(name != NULL);
assert(pid > 0);
- list_for_each(p, &name->procs) {
+ llist_for_each(p, &name->procs) {
struct proc_entry * entry;
entry = list_entry(p, struct proc_entry, next);
if (entry->pid == pid)
@@ -160,7 +154,7 @@ static struct prog_entry * __reg_name_get_prog(const struct reg_name * name,
assert(name != NULL);
assert(prog != NULL);
- list_for_each(p, &name->progs) {
+ llist_for_each(p, &name->progs) {
struct prog_entry * entry;
entry = list_entry(p, struct prog_entry, next);
if (strcmp(entry->exec[0], prog) == 0)
@@ -195,17 +189,15 @@ int reg_name_add_active(struct reg_name * name,
switch (name->info.pol_lb) {
case LB_RR: /* Round robin policy. */
- list_add_tail(&entry->next, &name->active);
+ llist_add_tail(&entry->next, &name->active);
break;
case LB_SPILL: /* Keep accepting flows on the current process */
- list_add(&entry->next, &name->active);
+ llist_add(&entry->next, &name->active);
break;
default:
goto fail_unreachable;
}
- name->n_active++;
-
return 0;
fail_unreachable:
@@ -224,21 +216,23 @@ void reg_name_del_active(struct reg_name * name,
if (entry == NULL)
return;
- list_del(&entry->next);
-
- name->n_active--;
+ llist_del(&entry->next, &name->active);
free(entry);
}
pid_t reg_name_get_active(struct reg_name * name)
{
+ struct proc_entry * e;
+
assert(name != NULL);
- if (list_is_empty(&name->active))
+ if (llist_is_empty(&name->active))
return -1;
- return list_first_entry(&name->active, struct proc_entry, next)->pid;
+ e = llist_first_entry(&name->active, struct proc_entry, next);
+
+ return e->pid;
}
int reg_name_add_proc(struct reg_name * name,
@@ -259,9 +253,7 @@ int reg_name_add_proc(struct reg_name * name,
entry->pid = pid;
- list_add(&entry->next, &name->procs);
-
- name->n_procs++;
+ llist_add(&entry->next, &name->procs);
return 0;
@@ -283,12 +275,10 @@ void reg_name_del_proc(struct reg_name * name,
__reg_name_del_all_active(name, pid);
- list_del(&entry->next);
+ llist_del(&entry->next, &name->procs);
free(entry);
- name->n_procs--;
-
assert(__reg_name_get_proc(name, pid) == NULL);
}
@@ -296,8 +286,7 @@ bool reg_name_has_proc(const struct reg_name * name,
pid_t pid)
{
return __reg_name_get_proc(name, pid) != NULL;
-} char ** exec;
-
+}
int reg_name_add_prog(struct reg_name * name,
char ** exec)
@@ -322,12 +311,10 @@ int reg_name_add_prog(struct reg_name * name,
goto fail_exec;
}
- list_add(&entry->next, &name->progs);
+ llist_add(&entry->next, &name->progs);
log_dbg("Add prog %s to name %s.", exec[0], name->info.name);
- name->n_progs++;
-
return 0;
fail_exec:
@@ -348,12 +335,10 @@ void reg_name_del_prog(struct reg_name * name,
if (entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &name->progs);
__free_prog_entry(entry);
- name->n_progs--;
-
assert(__reg_name_get_prog(name, prog) == NULL);
}
@@ -368,8 +353,12 @@ bool reg_name_has_prog(const struct reg_name * name,
char ** reg_name_get_exec(const struct reg_name * name)
{
- if (list_is_empty(&name->progs))
+ struct prog_entry * e;
+
+ if (llist_is_empty(&name->progs))
return NULL;
- return list_first_entry(&name->progs, struct prog_entry, next)->exec;
+ e = llist_first_entry(&name->progs, struct prog_entry, next);
+
+ return e->exec;
}
diff --git a/src/irmd/reg/name.h b/src/irmd/reg/name.h
index 97ca7f04..59d6d9bd 100644
--- a/src/irmd/reg/name.h
+++ b/src/irmd/reg/name.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names
*
@@ -33,14 +33,16 @@ struct reg_name {
struct name_info info;
- struct list_head progs; /* autostart programs for this name */
- size_t n_progs; /* number of programs */
+ struct {
+ void * key;
+ void * crt;
+ } cache;
- struct list_head procs; /* processes bound to this name */
- size_t n_procs; /* number of processes */
+ struct llist progs; /* autostart programs for this name */
- struct list_head active; /* processes actively calling accept */
- size_t n_active; /* number of processes accepting */
+ struct llist procs; /* processes bound to this name */
+
+ struct llist active; /* processes actively calling accept */
};
struct reg_name * reg_name_create(const struct name_info * info);
@@ -74,5 +76,4 @@ pid_t reg_name_get_active(struct reg_name * name);
void reg_name_del_active(struct reg_name * name,
pid_t proc);
-
#endif /* OUROBOROS_IRMD_REG_NAME_H */
diff --git a/src/irmd/reg/pool.c b/src/irmd/reg/pool.c
new file mode 100644
index 00000000..4b1486bb
--- /dev/null
+++ b/src/irmd/reg/pool.c
@@ -0,0 +1,97 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * The IPC Resource Manager - Registry - Per-User Pools
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#define OUROBOROS_PREFIX "reg/pool"
+
+#include <ouroboros/logs.h>
+#include <ouroboros/ssm_pool.h>
+
+#include "pool.h"
+
+#include <assert.h>
+#include <stdlib.h>
+
+struct reg_pool * reg_pool_create(uid_t uid,
+ gid_t gid)
+{
+ struct reg_pool * pool;
+
+ pool = malloc(sizeof(*pool));
+ if (pool == NULL) {
+ log_err("Failed to malloc pool.");
+ goto fail_malloc;
+ }
+
+ pool->ssm = ssm_pool_create(uid, gid);
+ if (pool->ssm == NULL) {
+ log_err("Failed to create PUP for uid %d.", uid);
+ goto fail_ssm;
+ }
+
+ list_head_init(&pool->next);
+ pool->uid = uid;
+ pool->gid = gid;
+ pool->refcount = 1;
+
+ log_dbg("Created PUP for uid %d gid %d.", uid, gid);
+
+ return pool;
+
+ fail_ssm:
+ free(pool);
+ fail_malloc:
+ return NULL;
+}
+
+void reg_pool_destroy(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount == 0);
+
+ log_dbg("Destroying PUP for uid %d.", pool->uid);
+
+ ssm_pool_destroy(pool->ssm);
+
+ assert(list_is_empty(&pool->next));
+
+ free(pool);
+}
+
+void reg_pool_ref(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount > 0);
+
+ pool->refcount++;
+}
+
+int reg_pool_unref(struct reg_pool * pool)
+{
+ assert(pool != NULL);
+ assert(pool->refcount > 0);
+
+ pool->refcount--;
+
+ return pool->refcount == 0 ? 0 : 1;
+}
diff --git a/src/irmd/reg/pool.h b/src/irmd/reg/pool.h
new file mode 100644
index 00000000..576f491c
--- /dev/null
+++ b/src/irmd/reg/pool.h
@@ -0,0 +1,48 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * The IPC Resource Manager - Registry - Per-User Pools
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IRMD_REG_POOL_H
+#define OUROBOROS_IRMD_REG_POOL_H
+
+#include <ouroboros/list.h>
+#include <ouroboros/ssm_pool.h>
+
+#include <sys/types.h>
+
+struct reg_pool {
+ struct list_head next;
+ uid_t uid;
+ gid_t gid;
+ size_t refcount;
+ struct ssm_pool * ssm;
+};
+
+struct reg_pool * reg_pool_create(uid_t uid,
+ gid_t gid);
+
+void reg_pool_destroy(struct reg_pool * pool);
+
+void reg_pool_ref(struct reg_pool * pool);
+
+int reg_pool_unref(struct reg_pool * pool);
+
+#endif /* OUROBOROS_IRMD_REG_POOL_H */
diff --git a/src/irmd/reg/proc.c b/src/irmd/reg/proc.c
index 9bbdf0eb..8a7e24c9 100644
--- a/src/irmd/reg/proc.c
+++ b/src/irmd/reg/proc.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes
*
@@ -25,6 +25,7 @@
#define OUROBOROS_PREFIX "reg/proc"
#include <ouroboros/logs.h>
+#include <ouroboros/utils.h>
#include "proc.h"
@@ -54,12 +55,11 @@ static void __reg_proc_clear_names(struct reg_proc * proc)
assert(proc != NULL);
- list_for_each_safe(p, h, &proc->names) {
+ llist_for_each_safe(p, h, &proc->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &proc->names);
__free_name_entry(entry);
- proc->n_names--;
}
}
@@ -75,17 +75,18 @@ struct reg_proc * reg_proc_create(const struct proc_info * info)
goto fail_malloc;
}
- proc->set = shm_flow_set_create(info->pid);
+ memset(proc, 0, sizeof(*proc));
+
+ proc->set = ssm_flow_set_create(info->pid);
if (proc->set == NULL) {
log_err("Failed to create flow set for %d.", info->pid);
goto fail_set;
}
list_head_init(&proc->next);
- list_head_init(&proc->names);
+ llist_init(&proc->names);
proc->info = *info;
- proc->n_names = 0;
return proc;
@@ -99,15 +100,13 @@ void reg_proc_destroy(struct reg_proc * proc)
{
assert(proc != NULL);
- shm_flow_set_destroy(proc->set);
+ ssm_flow_set_destroy(proc->set);
__reg_proc_clear_names(proc);
assert(list_is_empty(&proc->next));
- assert(proc->n_names == 0);
-
- assert(list_is_empty(&proc->names));
+ assert(llist_is_empty(&proc->names));
free(proc);
}
@@ -117,7 +116,7 @@ static struct name_entry * __reg_proc_get_name(const struct reg_proc * proc,
{
struct list_head * p;
- list_for_each(p, &proc->names) {
+ llist_for_each(p, &proc->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
if (strcmp(entry->name, name) == 0)
@@ -146,9 +145,7 @@ int reg_proc_add_name(struct reg_proc * proc,
goto fail_name;
}
- list_add(&entry->next, &proc->names);
-
- proc->n_names++;
+ llist_add(&entry->next, &proc->names);
return 0;
@@ -167,12 +164,10 @@ void reg_proc_del_name(struct reg_proc * proc,
if(entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &proc->names);
__free_name_entry(entry);
- proc->n_names--;
-
assert(__reg_proc_get_name(proc, name) == NULL);
}
@@ -181,3 +176,10 @@ bool reg_proc_has_name(const struct reg_proc * proc,
{
return __reg_proc_get_name(proc, name) != NULL;
}
+
+bool reg_proc_is_privileged(const struct reg_proc * proc)
+{
+ assert(proc != NULL);
+
+ return is_ouroboros_member_uid(proc->info.uid);
+}
diff --git a/src/irmd/reg/proc.h b/src/irmd/reg/proc.h
index 99f74fef..18cc2803 100644
--- a/src/irmd/reg/proc.h
+++ b/src/irmd/reg/proc.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes
*
@@ -25,17 +25,16 @@
#include <ouroboros/list.h>
#include <ouroboros/proc.h>
-#include <ouroboros/shm_flow_set.h>
+#include <ouroboros/ssm_flow_set.h>
struct reg_proc {
struct list_head next;
struct proc_info info;
- struct list_head names; /* names for which process accepts flows */
- size_t n_names; /* number of names */
+ struct llist names; /* process accepts flows for names */
- struct shm_flow_set * set;
+ struct ssm_flow_set * set;
};
struct reg_proc * reg_proc_create(const struct proc_info * info);
@@ -53,4 +52,6 @@ void reg_proc_del_name(struct reg_proc * proc,
bool reg_proc_has_name(const struct reg_proc * proc,
const char * name);
+bool reg_proc_is_privileged(const struct reg_proc * proc);
+
#endif /* OUROBOROS_IRMD_REG_PROC_H */
diff --git a/src/irmd/reg/prog.c b/src/irmd/reg/prog.c
index 9b9e7510..2d7f9f8d 100644
--- a/src/irmd/reg/prog.c
+++ b/src/irmd/reg/prog.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs
*
@@ -55,12 +55,11 @@ static void __reg_prog_clear_names(struct reg_prog * prog)
assert(prog != NULL);
- list_for_each_safe(p, h, &prog->names) {
+ llist_for_each_safe(p, h, &prog->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &prog->names);
__free_name_entry(entry);
- prog->n_names--;
}
}
@@ -77,10 +76,9 @@ struct reg_prog * reg_prog_create(const struct prog_info * info)
}
list_head_init(&p->next);
- list_head_init(&p->names);
+ llist_init(&p->names);
p->info = *info;
- p->n_names = 0;
return p;
@@ -96,9 +94,7 @@ void reg_prog_destroy(struct reg_prog * prog)
assert(list_is_empty(&prog->next));
- assert(prog->n_names == 0);
-
- assert(list_is_empty(&prog->names));
+ assert(llist_is_empty(&prog->names));
free(prog);
}
@@ -108,7 +104,7 @@ static struct name_entry * __reg_prog_get_name(const struct reg_prog * prog,
{
struct list_head * p;
- list_for_each(p, &prog->names) {
+ llist_for_each(p, &prog->names) {
struct name_entry * entry;
entry = list_entry(p, struct name_entry, next);
if (strcmp(entry->name, name) == 0)
@@ -137,9 +133,7 @@ int reg_prog_add_name(struct reg_prog * prog,
goto fail_name;
}
- list_add(&entry->next, &prog->names);
-
- prog->n_names++;
+ llist_add(&entry->next, &prog->names);
return 0;
@@ -158,12 +152,10 @@ void reg_prog_del_name(struct reg_prog * prog,
if (entry == NULL)
return;
- list_del(&entry->next);
+ llist_del(&entry->next, &prog->names);
__free_name_entry(entry);
- prog->n_names--;
-
assert(__reg_prog_get_name(prog, name) == NULL);
}
diff --git a/src/irmd/reg/prog.h b/src/irmd/reg/prog.h
index a98fc6a1..e52b8e15 100644
--- a/src/irmd/reg/prog.h
+++ b/src/irmd/reg/prog.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs
*
@@ -33,8 +33,7 @@ struct reg_prog {
struct prog_info info;
- struct list_head names; /* names to listen for */
- size_t n_names; /* number of names in list */
+ struct llist names; /* names to listen for */
};
struct reg_prog * reg_prog_create(const struct prog_info * info);
diff --git a/src/irmd/reg/reg.c b/src/irmd/reg/reg.c
index d95a4722..ebf3959d 100644
--- a/src/irmd/reg/reg.c
+++ b/src/irmd/reg/reg.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
The IPC Resource Manager - Registry
*
@@ -25,15 +25,18 @@ The IPC Resource Manager - Registry
#define OUROBOROS_PREFIX "reg"
#include <ouroboros/bitmap.h>
+#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
#include <ouroboros/list.h>
#include <ouroboros/logs.h>
+#include <ouroboros/protobuf.h>
#include <ouroboros/pthread.h>
#include "reg.h"
#include "flow.h"
#include "ipcp.h"
#include "name.h"
+#include "pool.h"
#include "proc.h"
#include "prog.h"
@@ -45,27 +48,18 @@ The IPC Resource Manager - Registry
#define ID_OFFT 1 /* reserve some flow_ids */
struct {
- struct bmp * flow_ids; /* flow_ids for flows */
- struct list_head flows; /* flow information */
- size_t n_flows; /* number of flows */
-
- struct list_head ipcps; /* list of ipcps in system */
- size_t n_ipcps; /* number of ipcps */
-
- struct list_head names; /* registered names known */
- size_t n_names; /* number of names */
-
- struct list_head procs; /* processes */
- size_t n_procs; /* number of processes */
-
- struct list_head progs; /* programs known */
- size_t n_progs; /* number of programs */
-
- struct list_head spawned; /* child processes */
- size_t n_spawned; /* number of child processes */
-
- pthread_mutex_t mtx; /* registry lock */
- pthread_cond_t cond; /* condvar for reg changes */
+ struct bmp * ids; /* flow bitmap */
+
+ struct llist flows; /* list of flows */
+ struct llist ipcps; /* list of ipcps in system */
+ struct llist names; /* registered names known */
+ struct llist pools; /* per-user pools */
+ struct llist procs; /* processes known */
+ struct llist progs; /* programs known */
+ struct llist spawned; /* child processes */
+
+ pthread_mutex_t mtx; /* registry lock */
+ pthread_cond_t cond; /* condvar for reg changes */
} reg;
struct pid_entry {
@@ -79,7 +73,7 @@ static struct reg_flow * __reg_get_flow(int flow_id)
assert(flow_id >= ID_OFFT);
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.id == flow_id)
@@ -93,7 +87,7 @@ static struct reg_flow * __reg_get_accept_flow(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.state != FLOW_ACCEPT_PENDING)
@@ -111,7 +105,7 @@ static struct list_head * __reg_after_flow(int flow_id)
assert(flow_id >= ID_OFFT);
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.id > flow_id)
@@ -127,7 +121,7 @@ static struct reg_ipcp * __reg_get_ipcp(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (entry->info.pid == pid)
@@ -141,7 +135,7 @@ static struct reg_ipcp * __reg_get_ipcp_by_layer(const char * layer)
{
struct list_head * p;
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
if (strcmp(entry->layer.name, layer) == 0)
@@ -151,16 +145,23 @@ static struct reg_ipcp * __reg_get_ipcp_by_layer(const char * layer)
return NULL;
}
-static struct list_head * __reg_after_ipcp(pid_t pid)
+
+static struct list_head * __reg_after_ipcp(const struct ipcp_info * info)
{
struct list_head * p;
- assert(pid > 0);
+ assert(info != NULL);
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
- if (entry->info.pid > pid)
+ if (entry->info.type < info->type)
+ continue;
+
+ if (entry->info.type > info->type)
+ break;
+
+ if (entry->info.pid > info->pid)
break;
}
@@ -173,7 +174,7 @@ static struct reg_name * __reg_get_name(const char * name)
assert(name != NULL);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
if (strcmp(entry->info.name, name) == 0)
@@ -183,41 +184,17 @@ static struct reg_name * __reg_get_name(const char * name)
return NULL;
}
-static struct reg_name * __reg_get_name_by_hash(enum hash_algo algo,
- const uint8_t * hash)
-{
- struct list_head * p;
- uint8_t * thash;
- size_t len;
-
- len = hash_len(algo);
-
- thash = malloc(len);
- if (thash == NULL)
- return NULL;
-
- list_for_each(p, &reg.names) {
- struct reg_name * n = list_entry(p, struct reg_name, next);
- str_hash(algo, thash, n->info.name);
- if (memcmp(thash, hash, len) == 0) {
- free(thash);
- return n;
- }
- }
-
- free(thash);
-
- return NULL;
-}
-
-static int __reg_get_pending_flow_id_for_hash(enum hash_algo algo,
- const uint8_t * hash)
+static int __reg_get_pending_flow_id(const char * name)
{
struct reg_name * entry;
struct reg_flow * flow;
pid_t pid;
- entry =__reg_get_name_by_hash(algo, hash);
+ assert(name != NULL);
+ assert(strlen(name) > 0);
+ assert(strlen(name) < NAME_SIZE + 1);
+
+ entry =__reg_get_name(name);
if (entry == NULL)
return -ENAME;
@@ -226,7 +203,10 @@ static int __reg_get_pending_flow_id_for_hash(enum hash_algo algo,
return -EAGAIN;
flow = __reg_get_accept_flow(pid);
- assert(flow != NULL);
+ if (flow == NULL) /* compiler barks, this can't be NULL */
+ return -EAGAIN;
+
+ strcpy(flow->name, name);
return flow->info.id;
}
@@ -237,7 +217,7 @@ static struct list_head * __reg_after_name(const char * name)
assert(name != NULL);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
if (strcmp(entry->info.name, name) > 0)
@@ -247,11 +227,25 @@ static struct list_head * __reg_after_name(const char * name)
return p;
}
+static struct reg_pool * __reg_get_pool(uid_t uid)
+{
+ struct list_head * p;
+
+ llist_for_each(p, &reg.pools) {
+ struct reg_pool * entry;
+ entry = list_entry(p, struct reg_pool, next);
+ if (entry->uid == uid)
+ return entry;
+ }
+
+ return NULL;
+}
+
static struct reg_proc * __reg_get_proc(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (entry->info.pid == pid)
@@ -265,7 +259,7 @@ static struct list_head * __reg_after_proc(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (entry->info.pid > pid)
@@ -279,7 +273,7 @@ static void __reg_kill_all_proc(int signal)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
kill(entry->info.pid, signal);
@@ -290,7 +284,7 @@ static pid_t __reg_get_dead_proc(void)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
if (kill(entry->info.pid, 0) < 0)
@@ -305,7 +299,7 @@ static void __reg_cancel_flows_for_proc(pid_t pid)
struct list_head * p;
bool changed = false;
- list_for_each(p, &reg.flows) {
+ llist_for_each(p, &reg.flows) {
struct reg_flow * entry;
entry = list_entry(p, struct reg_flow, next);
if (entry->info.n_pid != pid)
@@ -331,7 +325,7 @@ static struct pid_entry * __reg_get_spawned(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
if (entry->pid == pid)
@@ -345,7 +339,7 @@ static struct list_head * __reg_after_spawned(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
if (entry->pid > pid)
@@ -359,7 +353,7 @@ static void __reg_kill_all_spawned(int signal)
{
struct list_head * p;
- list_for_each(p, &reg.spawned) {
+ llist_for_each(p, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
kill(entry->pid, signal);
@@ -368,17 +362,17 @@ static void __reg_kill_all_spawned(int signal)
static pid_t __reg_first_spawned(void)
{
- if (list_is_empty(&reg.spawned))
+ if (llist_is_empty(&reg.spawned))
return -1;
- return list_first_entry(&reg.spawned, struct pid_entry, next)->pid;
+ return llist_first_entry(&reg.spawned, struct pid_entry, next)->pid;
}
static struct reg_prog * __reg_get_prog(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
if (strcmp(entry->info.name, name) == 0)
@@ -388,30 +382,17 @@ static struct reg_prog * __reg_get_prog(const char * name)
return NULL;
}
-static char ** __reg_get_exec(enum hash_algo algo,
- const uint8_t * hash)
+static char ** __reg_get_exec(const char * name)
{
struct list_head * p;
- uint8_t * buf;
-
- buf = malloc(hash_len(algo));
- if (buf == NULL) {
- log_err("Failed to malloc hash buffer.");
- return NULL;
- }
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
- str_hash(algo, buf, entry->info.name);
- if (memcmp(buf, hash, hash_len(algo)) == 0) {
- free(buf);
+ if (strcmp(entry->info.name, name) == 0)
return reg_name_get_exec(entry);
- }
}
- free(buf);
-
return NULL;
}
@@ -419,7 +400,7 @@ static struct list_head * __reg_after_prog(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
if (strcmp(entry->info.name, name) > 0)
@@ -433,7 +414,7 @@ static void __reg_del_name_from_procs(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.procs) {
+ llist_for_each(p, &reg.procs) {
struct reg_proc * proc;
proc = list_entry(p, struct reg_proc, next);
reg_proc_del_name(proc, name);
@@ -444,7 +425,7 @@ static void __reg_del_name_from_progs(const char * name)
{
struct list_head * p;
- list_for_each(p, &reg.progs) {
+ llist_for_each(p, &reg.progs) {
struct reg_prog * prog;
prog = list_entry(p, struct reg_prog, next);
reg_prog_del_name(prog, name);
@@ -456,13 +437,13 @@ static void __reg_proc_update_names(struct reg_proc * proc)
struct list_head * p;
struct reg_prog * prog;
- assert(list_is_empty(&proc->names));
+ assert(llist_is_empty(&proc->names));
prog = __reg_get_prog(proc->info.prog);
if (prog == NULL)
return;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
assert(!reg_name_has_proc(name, proc->info.pid));
@@ -477,7 +458,7 @@ static void __reg_del_proc_from_names(pid_t pid)
{
struct list_head * p;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_proc(name, pid);
@@ -488,7 +469,7 @@ static void __reg_del_prog_from_names(const char * prog)
{
struct list_head * p;
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_prog(name, prog);
@@ -503,7 +484,7 @@ static int __reg_add_active_proc(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
if (reg_name_has_proc(name, pid)) {
@@ -525,7 +506,7 @@ static void __reg_del_active_proc(pid_t pid)
assert(pid > 0);
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * name;
name = list_entry(p, struct reg_name, next);
reg_name_del_active(name, pid);
@@ -554,20 +535,21 @@ int reg_init(void)
goto fail_cond;
}
- reg.flow_ids = bmp_create(SYS_MAX_FLOWS -ID_OFFT, ID_OFFT);
- if (reg.flow_ids == NULL) {
+ reg.ids = bmp_create(SYS_MAX_FLOWS - ID_OFFT, ID_OFFT);
+ if (reg.ids == NULL) {
log_err("Failed to create flow_ids bitmap.");
goto fail_flow_ids;
}
pthread_condattr_destroy(&cattr);
- list_head_init(&reg.flows);
- list_head_init(&reg.ipcps);
- list_head_init(&reg.names);
- list_head_init(&reg.procs);
- list_head_init(&reg.progs);
- list_head_init(&reg.spawned);
+ llist_init(&reg.flows);
+ llist_init(&reg.ipcps);
+ llist_init(&reg.names);
+ llist_init(&reg.pools);
+ llist_init(&reg.procs);
+ llist_init(&reg.progs);
+ llist_init(&reg.spawned);
return 0;
@@ -588,54 +570,56 @@ void reg_clear(void)
pthread_mutex_lock(&reg.mtx);
- list_for_each_safe(p, h, &reg.spawned) {
+ llist_for_each_safe(p, h, &reg.spawned) {
struct pid_entry * entry;
entry = list_entry(p, struct pid_entry, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.spawned);
free(entry);
- reg.n_spawned--;
}
- list_for_each_safe(p, h, &reg.progs) {
+ llist_for_each_safe(p, h, &reg.progs) {
struct reg_prog * entry;
entry = list_entry(p, struct reg_prog, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.progs);
__reg_del_prog_from_names(entry->info.path);
reg_prog_destroy(entry);
- reg.n_progs--;
}
- list_for_each_safe(p, h, &reg.procs) {
+ llist_for_each_safe(p, h, &reg.procs) {
struct reg_proc * entry;
entry = list_entry(p, struct reg_proc, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.procs);
__reg_del_proc_from_names(entry->info.pid);
reg_proc_destroy(entry);
- reg.n_procs--;
}
- list_for_each_safe(p, h, &reg.names) {
+ llist_for_each_safe(p, h, &reg.pools) {
+ struct reg_pool * entry;
+ entry = list_entry(p, struct reg_pool, next);
+ llist_del(&entry->next, &reg.pools);
+ entry->refcount = 0; /* Force destroy during cleanup */
+ reg_pool_destroy(entry);
+ }
+
+ llist_for_each_safe(p, h, &reg.flows) {
+ struct reg_flow * entry;
+ entry = list_entry(p, struct reg_flow, next);
+ llist_del(&entry->next, &reg.flows);
+ reg_flow_destroy(entry);
+ }
+
+ llist_for_each_safe(p, h, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.names);
reg_name_destroy(entry);
- reg.n_names--;
}
- list_for_each_safe(p, h, &reg.ipcps) {
+ llist_for_each_safe(p, h, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
- list_del(&entry->next);
+ llist_del(&entry->next, &reg.ipcps);
reg_ipcp_destroy(entry);
- reg.n_ipcps--;
- }
-
- list_for_each_safe(p, h, &reg.flows) {
- struct reg_flow * entry;
- entry = list_entry(p, struct reg_flow, next);
- list_del(&entry->next);
- reg_flow_destroy(entry);
- reg.n_flows--;
}
pthread_mutex_unlock(&reg.mtx);
@@ -643,21 +627,15 @@ void reg_clear(void)
void reg_fini(void)
{
- assert(list_is_empty(&reg.spawned));
- assert(list_is_empty(&reg.progs));
- assert(list_is_empty(&reg.procs));
- assert(list_is_empty(&reg.names));
- assert(list_is_empty(&reg.ipcps));
- assert(list_is_empty(&reg.flows));
+ assert(llist_is_empty(&reg.spawned));
+ assert(llist_is_empty(&reg.progs));
+ assert(llist_is_empty(&reg.procs));
+ assert(llist_is_empty(&reg.pools));
+ assert(llist_is_empty(&reg.names));
+ assert(llist_is_empty(&reg.ipcps));
+ assert(llist_is_empty(&reg.flows));
- assert(reg.n_spawned == 0);
- assert(reg.n_progs == 0);
- assert(reg.n_procs == 0);
- assert(reg.n_names == 0);
- assert(reg.n_ipcps == 0);
- assert(reg.n_flows == 0);
-
- bmp_destroy(reg.flow_ids);
+ bmp_destroy(reg.ids);
if (pthread_cond_destroy(&reg.cond) != 0)
log_warn("Failed to destroy condvar.");
@@ -677,8 +655,8 @@ int reg_create_flow(struct flow_info * info)
pthread_mutex_lock(&reg.mtx);
- info->id = bmp_allocate(reg.flow_ids);
- if (!bmp_is_id_valid(reg.flow_ids, info->id)) {
+ info->id = bmp_allocate(reg.ids);
+ if (!bmp_is_id_valid(reg.ids, info->id)) {
log_err("Failed to allocate flow id.");
goto fail_id;
}
@@ -689,16 +667,14 @@ int reg_create_flow(struct flow_info * info)
goto fail_flow;
}
- list_add(&f->next, __reg_after_flow(info->id));
-
- reg.n_flows++;
+ llist_add_at(&f->next, __reg_after_flow(info->id), &reg.flows);
pthread_mutex_unlock(&reg.mtx);
return 0;
fail_flow:
- bmp_release(reg.flow_ids, info->id);
+ bmp_release(reg.ids, info->id);
info->id = 0;
fail_id:
pthread_mutex_unlock(&reg.mtx);
@@ -717,11 +693,9 @@ int reg_destroy_flow(int flow_id)
goto no_flow;
}
- list_del(&f->next);
-
- reg.n_flows--;
+ llist_del(&f->next, &reg.flows);
- bmp_release(reg.flow_ids, flow_id);
+ bmp_release(reg.ids, flow_id);
pthread_mutex_unlock(&reg.mtx);
@@ -757,7 +731,7 @@ int reg_create_ipcp(const struct ipcp_info * info)
assert(info != NULL);
assert(info->pid != 0);
- assert(info->state == IPCP_BOOT);
+ assert(info->state == IPCP_INIT);
pthread_mutex_lock(&reg.mtx);
@@ -780,11 +754,10 @@ int reg_create_ipcp(const struct ipcp_info * info)
entry->pid = info->pid;
- list_add(&ipcp->next, __reg_after_ipcp(info->pid));
- list_add(&entry->next, __reg_after_spawned(info->pid));
-
- reg.n_ipcps++;
- reg.n_spawned++;
+ llist_add_tail_at(&ipcp->next, __reg_after_ipcp(info), &reg.ipcps);
+ llist_add_at(&entry->next,
+ __reg_after_spawned(info->pid),
+ &reg.spawned);
pthread_mutex_unlock(&reg.mtx);
@@ -848,11 +821,11 @@ static int __get_ipcp_info(ipcp_list_msg_t ** msg,
(*msg)->name = strdup(ipcp->info.name);
if ((*msg)->name == NULL)
- goto fail_name;
+ goto fail_msg;
(*msg)->layer = strdup(ipcp->layer.name);
if ((*msg)->layer == NULL)
- goto fail_layer;
+ goto fail_msg;
(*msg)->pid = ipcp->info.pid;
(*msg)->type = ipcp->info.type;
@@ -860,10 +833,8 @@ static int __get_ipcp_info(ipcp_list_msg_t ** msg,
return 0;
- fail_layer:
- free((*msg)->name);
- fail_name:
- free(*msg);
+ fail_msg:
+ ipcp_list_msg__free_unpacked(*msg, NULL);
*msg = NULL;
fail:
return -1;
@@ -876,37 +847,31 @@ int reg_list_ipcps(ipcp_list_msg_t *** ipcps)
pthread_mutex_lock(&reg.mtx);
- if (reg.n_ipcps == 0) {
- *ipcps = NULL;
+ if (llist_is_empty(&reg.ipcps))
goto finish;
- }
- *ipcps = malloc(reg.n_ipcps * sizeof(**ipcps));
+ *ipcps = malloc(reg.ipcps.len * sizeof(**ipcps));
if (*ipcps == NULL) {
log_err("Failed to malloc ipcps.");
goto fail_malloc;
}
- list_for_each(p, &reg.ipcps) {
+ llist_for_each(p, &reg.ipcps) {
struct reg_ipcp * entry;
entry = list_entry(p, struct reg_ipcp, next);
- if (__get_ipcp_info(&((*ipcps)[i]), entry) < 0) {
- log_err("Failed to create ipcp list info.");
+ if (__get_ipcp_info(&(*ipcps)[i], entry) < 0)
goto fail;
- }
- ++i;
+ i++;
}
-
- assert(i == (int) reg.n_ipcps);
finish:
pthread_mutex_unlock(&reg.mtx);
return i;
fail:
- while (i > 0)
- ipcp_list_msg__free_unpacked((*ipcps)[--i], NULL);
+ while (i-- > 0)
+ ipcp_list_msg__free_unpacked((*ipcps)[i], NULL);
free(*ipcps);
fail_malloc:
@@ -934,9 +899,7 @@ int reg_create_name(const struct name_info * info)
goto fail_name;
}
- list_add(&n->next, __reg_after_name(info->name));
-
- reg.n_names++;
+ llist_add_at(&n->next, __reg_after_name(info->name), &reg.names);
pthread_mutex_unlock(&reg.mtx);
return 0;
@@ -965,9 +928,7 @@ int reg_destroy_name(const char * name)
__reg_del_name_from_procs(name);
__reg_del_name_from_progs(name);
- list_del(&n->next);
-
- reg.n_names--;
+ llist_del(&n->next, &reg.names);
pthread_mutex_unlock(&reg.mtx);
@@ -993,28 +954,98 @@ bool reg_has_name(const char * name)
return ret;
}
-static int __get_name_info(name_info_msg_t ** msg,
- struct reg_name * n)
+int reg_get_name_info(const char * name,
+ struct name_info * info)
{
- *msg = malloc(sizeof(**msg));
- if (*msg == NULL)
- goto fail;
+ struct reg_name * n;
- name_info_msg__init(*msg);
+ assert(name != NULL);
+ assert(info != NULL);
- (*msg)->name = strdup(n->info.name);
- if ((*msg)->name == NULL)
- goto fail_name;
+ pthread_mutex_lock(&reg.mtx);
- (*msg)->pol_lb = n->info.pol_lb;
+ n = __reg_get_name(name);
+ if (n == NULL) {
+ log_err("Name %s does not exist.", name);
+ goto no_name;
+ }
+
+ *info = n->info;
+
+ pthread_mutex_unlock(&reg.mtx);
return 0;
- fail_name:
- free(*msg);
- *msg = NULL;
- fail:
- return -1;
+ no_name:
+ pthread_mutex_unlock(&reg.mtx);
+ return -ENOENT;
+
+}
+
+int reg_get_name_for_hash(char * buf,
+ enum hash_algo algo,
+ const uint8_t * hash)
+{
+ struct list_head * p;
+ uint8_t * thash;
+ size_t len;
+ char * name = NULL;
+
+ len = hash_len(algo);
+
+ thash = malloc(len);
+ if (thash == NULL)
+ return -ENOMEM;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ llist_for_each(p, &reg.names) {
+ struct reg_name * n = list_entry(p, struct reg_name, next);
+ str_hash(algo, thash, n->info.name);
+ if (memcmp(thash, hash, len) == 0) {
+ name = n->info.name;
+ break;
+ }
+ }
+
+ if (name != NULL)
+ strcpy(buf, name);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ free(thash);
+
+ return name == NULL ? -ENOENT : 0;
+}
+
+int reg_get_name_for_flow_id(char * buf,
+ int flow_id)
+{
+ struct reg_flow * f;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ f = __reg_get_flow(flow_id);
+ if (f != NULL)
+ strcpy(buf, f->name);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return f == NULL ? -ENOENT : 0;
+}
+
+void reg_set_name_for_flow_id(const char * name,
+ int flow_id)
+{
+ struct reg_flow * f;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ f = __reg_get_flow(flow_id);
+ if (f != NULL)
+ strcpy(f->name, name);
+
+ pthread_mutex_unlock(&reg.mtx);
}
int reg_list_names(name_info_msg_t *** names)
@@ -1024,35 +1055,43 @@ int reg_list_names(name_info_msg_t *** names)
pthread_mutex_lock(&reg.mtx);
- if (reg.n_names == 0)
+ if (llist_is_empty(&reg.names))
goto finish;
- *names = malloc(reg.n_names * sizeof(**names));
+ *names = malloc(reg.names.len * sizeof(**names));
if (*names == NULL) {
log_err("Failed to malloc names.");
goto fail_malloc;
}
- list_for_each(p, &reg.names) {
+ llist_for_each(p, &reg.names) {
struct reg_name * entry;
entry = list_entry(p, struct reg_name, next);
- if (__get_name_info(&((*names)[i]), entry) < 0) {
+ (*names)[i] = name_info_s_to_msg(&entry->info);
+ if ((*names)[i] == NULL) {
log_err("Failed to create name list info.");
goto fail;
}
-
- ++i;
+ /* wipe security info to avoid huge messages */
+ free((*names)[i]->scrt);
+ (*names)[i]->scrt = NULL;
+ free((*names)[i]->skey);
+ (*names)[i]->skey = NULL;
+ free((*names)[i]->ccrt);
+ (*names)[i]->ccrt = NULL;
+ free((*names)[i]->ckey);
+ (*names)[i]->ckey = NULL;
+
+ i++;
}
-
- assert(i == (int) reg.n_names);
finish:
pthread_mutex_unlock(&reg.mtx);
return i;
fail:
- while (i > 0)
- name_info_msg__free_unpacked((*names)[--i], NULL);
+ while (i-- > 0)
+ name_info_msg__free_unpacked((*names)[i], NULL);
free(*names);
fail_malloc:
@@ -1061,6 +1100,34 @@ int reg_list_names(name_info_msg_t *** names)
return -ENOMEM;
}
+int reg_prepare_pool(uid_t uid,
+ gid_t gid)
+{
+ struct reg_pool * pool;
+
+ if (is_ouroboros_member_uid(uid))
+ return 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ pool = __reg_get_pool(uid);
+ if (pool == NULL) {
+ pool = reg_pool_create(uid, gid);
+ if (pool == NULL) {
+ log_err("Failed to create pool for uid %d.", uid);
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+ }
+ llist_add(&pool->next, &reg.pools);
+ }
+
+ reg_pool_ref(pool);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+}
+
int reg_create_proc(const struct proc_info * info)
{
struct reg_proc * proc;
@@ -1071,20 +1138,18 @@ int reg_create_proc(const struct proc_info * info)
if (__reg_get_proc(info->pid) != NULL) {
log_err("Process %d already exists.", info->pid);
- goto fail_proc;
+ goto fail;
}
proc = reg_proc_create(info);
if (proc == NULL) {
log_err("Failed to create process %d.", info->pid);
- goto fail_proc;
+ goto fail;
}
__reg_proc_update_names(proc);
- list_add(&proc->next, __reg_after_proc(info->pid));
-
- reg.n_procs++;
+ llist_add_at(&proc->next, __reg_after_proc(info->pid), &reg.procs);
pthread_cond_broadcast(&reg.cond);
@@ -1092,7 +1157,7 @@ int reg_create_proc(const struct proc_info * info)
return 0;
- fail_proc:
+ fail:
pthread_mutex_unlock(&reg.mtx);
return -1;
}
@@ -1100,6 +1165,7 @@ int reg_create_proc(const struct proc_info * info)
int reg_destroy_proc(pid_t pid)
{
struct reg_proc * proc;
+ struct reg_pool * pool = NULL;
struct pid_entry * spawn;
struct reg_ipcp * ipcp;
@@ -1107,24 +1173,27 @@ int reg_destroy_proc(pid_t pid)
proc = __reg_get_proc(pid);
if (proc != NULL) {
- list_del(&proc->next);
- reg.n_procs--;
+ if (!is_ouroboros_member_uid(proc->info.uid))
+ pool = __reg_get_pool(proc->info.uid);
+ llist_del(&proc->next, &reg.procs);
reg_proc_destroy(proc);
__reg_del_proc_from_names(pid);
__reg_cancel_flows_for_proc(pid);
+ if (pool != NULL && reg_pool_unref(pool) == 0) {
+ llist_del(&pool->next, &reg.pools);
+ reg_pool_destroy(pool);
+ }
}
spawn = __reg_get_spawned(pid);
if (spawn != NULL) {
- list_del(&spawn->next);
- reg.n_spawned--;
+ llist_del(&spawn->next, &reg.spawned);
free(spawn);
}
ipcp = __reg_get_ipcp(pid);
if (ipcp != NULL) {
- list_del(&ipcp->next);
- reg.n_ipcps--;
+ llist_del(&ipcp->next, &reg.ipcps);
reg_ipcp_destroy(ipcp);
}
@@ -1146,6 +1215,38 @@ bool reg_has_proc(pid_t pid)
return ret;
}
+bool reg_is_proc_privileged(pid_t pid)
+{
+ struct reg_proc * proc;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL)
+ ret = reg_proc_is_privileged(proc);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+uid_t reg_get_proc_uid(pid_t pid)
+{
+ struct reg_proc * proc;
+ uid_t ret = 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL && !is_ouroboros_member_uid(proc->info.uid))
+ ret = proc->info.uid;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
void reg_kill_all_proc(int signal)
{
pthread_mutex_lock(&reg.mtx);
@@ -1187,9 +1288,7 @@ int reg_create_spawned(pid_t pid)
entry->pid = pid;
- list_add(&entry->next, __reg_after_spawned(pid));
-
- reg.n_spawned++;
+ llist_add_at(&entry->next, __reg_after_spawned(pid), &reg.spawned);
pthread_mutex_unlock(&reg.mtx);
@@ -1359,9 +1458,7 @@ int reg_create_prog(const struct prog_info * info)
goto fail_prog;
}
- list_add(&prog->next, __reg_after_prog(info->name));
-
- reg.n_progs++;
+ llist_add_at(&prog->next, __reg_after_prog(info->name), &reg.progs);
exists:
pthread_mutex_unlock(&reg.mtx);
@@ -1389,9 +1486,7 @@ int reg_destroy_prog(const char * name)
__reg_del_prog_from_names(prog->info.path);
- list_del(&prog->next);
-
- reg.n_progs--;
+ llist_del(&prog->next, &reg.progs);
pthread_mutex_unlock(&reg.mtx);
@@ -1419,19 +1514,18 @@ bool reg_has_prog(const char * name)
return ret;
}
-int reg_get_exec(enum hash_algo algo,
- const uint8_t * hash,
- char *** prog)
+int reg_get_exec(const char * name,
+ char *** prog)
{
char ** exec;
int ret = 0;
- assert(hash != NULL);
+ assert(name != NULL);
assert(prog != NULL);
pthread_mutex_lock(&reg.mtx);
- exec = __reg_get_exec(algo, hash);
+ exec = __reg_get_exec(name);
if (exec == NULL) {
ret = -EPERM;
goto finish;
@@ -1444,12 +1538,9 @@ int reg_get_exec(enum hash_algo algo,
goto finish;
}
- pthread_mutex_unlock(&reg.mtx);
-
- return 0;
-
finish:
pthread_mutex_unlock(&reg.mtx);
+
return ret;
}
@@ -1557,8 +1648,7 @@ int reg_set_layer_for_ipcp(struct ipcp_info * info,
struct reg_ipcp * ipcp;
assert(info != NULL);
- assert(info->state > IPCP_BOOT);
- assert(info->state < IPCP_SHUTDOWN);
+ assert(info->state == IPCP_BOOT);
pthread_mutex_lock(&reg.mtx);
@@ -1690,7 +1780,7 @@ int reg_wait_flow_allocated(struct flow_info * info,
stop = true;
break;
case FLOW_DEALLOCATED:
- ret = -1;
+ ret = flow->response;
stop = true;
break;
default:
@@ -1712,7 +1802,8 @@ int reg_wait_flow_allocated(struct flow_info * info,
}
if (flow != NULL) {
- reg_flow_get_data(flow, pbuf);
+ *pbuf = flow->rsp_data;
+ clrbuf(flow->rsp_data);
*info = flow->info;
}
@@ -1722,7 +1813,8 @@ int reg_wait_flow_allocated(struct flow_info * info,
}
int reg_respond_alloc(struct flow_info * info,
- buffer_t * pbuf)
+ buffer_t * pbuf,
+ int response)
{
struct reg_flow * flow;
@@ -1745,9 +1837,13 @@ int reg_respond_alloc(struct flow_info * info,
goto fail_flow;
}
- assert(flow->info.state == FLOW_ALLOC_PENDING);
- assert(flow->data.len == 0);
- assert(flow->data.data == NULL);
+ if (flow->info.state != FLOW_ALLOC_PENDING) {
+ log_warn("Flow %d already responded.", info->id);
+ goto fail_flow;
+ }
+
+ assert(flow->rsp_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
info->n_pid = flow->info.n_pid;
info->n_1_pid = flow->info.n_pid;
@@ -1755,10 +1851,14 @@ int reg_respond_alloc(struct flow_info * info,
if (reg_flow_update(flow, info) < 0) {
log_err("Failed to create flow structs.");
goto fail_flow;
- };
+ }
+
+ flow->response = response;
- if (info->state == FLOW_ALLOCATED)
- reg_flow_set_data(flow, pbuf);
+ if (info->state == FLOW_ALLOCATED) {
+ flow->rsp_data = *pbuf;
+ clrbuf(*pbuf);
+ }
pthread_cond_broadcast(&reg.cond);
@@ -1771,8 +1871,7 @@ int reg_respond_alloc(struct flow_info * info,
return -1;
}
-int reg_prepare_flow_accept(struct flow_info * info,
- buffer_t * pbuf)
+int reg_prepare_flow_accept(struct flow_info * info)
{
struct reg_flow * flow;
int ret;
@@ -1790,7 +1889,7 @@ int reg_prepare_flow_accept(struct flow_info * info,
ret = reg_flow_update(flow, info);
- reg_flow_set_data(flow, pbuf);
+ pthread_cond_broadcast(&reg.cond);
pthread_mutex_unlock(&reg.mtx);
@@ -1824,8 +1923,6 @@ int reg_wait_flow_accepted(struct flow_info * info,
assert(flow != NULL);
assert(info->id == flow->info.id);
assert(info->n_pid == flow->info.n_pid);
- assert(info->state == flow->info.state);
- assert(flow->info.state == FLOW_ACCEPT_PENDING);
if (__reg_add_active_proc(info->n_pid) < 0) {
log_err("Failed to mark pid %d active.", info->n_pid);
@@ -1871,7 +1968,8 @@ int reg_wait_flow_accepted(struct flow_info * info,
pthread_cleanup_pop(true); /* __cleanup_wait_accept */
if (flow != NULL) {
- reg_flow_get_data(flow, pbuf);
+ *pbuf = flow->req_data;
+ clrbuf(flow->req_data);
*info = flow->info;
}
@@ -1883,13 +1981,12 @@ int reg_wait_flow_accepted(struct flow_info * info,
return -1;
}
-int reg_wait_flow_accepting(enum hash_algo algo,
- const uint8_t * hash,
+int reg_wait_flow_accepting(const char * name,
const struct timespec * abstime)
{
int ret;
- assert(hash != NULL);
+ assert(name != NULL);
assert(abstime != NULL);
pthread_mutex_lock(&reg.mtx);
@@ -1897,7 +1994,7 @@ int reg_wait_flow_accepting(enum hash_algo algo,
pthread_cleanup_push(__cleanup_mutex_unlock, &reg.mtx);
while (true) {
- ret = __reg_get_pending_flow_id_for_hash(algo, hash);
+ ret = __reg_get_pending_flow_id(name);
if (ret != -EAGAIN)
break;
@@ -1915,7 +2012,6 @@ int reg_respond_accept(struct flow_info * info,
buffer_t * pbuf)
{
struct reg_flow * flow;
- buffer_t temp;
assert(info != NULL);
assert(info->state == FLOW_ALLOCATED);
@@ -1933,12 +2029,64 @@ int reg_respond_accept(struct flow_info * info,
info->n_pid = flow->info.n_pid;
- if (info->qs.cypher_s > 0) {
- reg_flow_get_data(flow, &temp);
- reg_flow_set_data(flow, pbuf);
- *pbuf = temp;
+ flow->req_data = *pbuf;
+ clrbuf(*pbuf);
+
+ if (reg_flow_update(flow, info) < 0) {
+ log_err("Failed to create flow structs.");
+ goto fail_flow;
+ }
+
+ pthread_cond_broadcast(&reg.cond);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+
+ fail_flow:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+int reg_prepare_flow_direct(struct flow_info * info,
+ buffer_t * pbuf,
+ uid_t alloc_uid)
+{
+ struct reg_flow * flow;
+ struct reg_proc * proc;
+ uid_t accept_uid = 0;
+
+ assert(info != NULL);
+ assert(info->state == FLOW_ALLOCATED);
+ assert(info->n_1_pid != 0);
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(info->id);
+ if (flow == NULL) {
+ log_err("Flow not found: %d.", info->id);
+ goto fail_flow;
+ }
+
+ assert(flow->info.state == FLOW_ACCEPT_PENDING);
+
+ info->n_pid = flow->info.n_pid;
+
+ proc = __reg_get_proc(info->n_pid);
+ if (proc != NULL && !is_ouroboros_member_uid(proc->info.uid))
+ accept_uid = proc->info.uid;
+
+ if (alloc_uid != accept_uid) {
+ pthread_mutex_unlock(&reg.mtx);
+ return -EPERM;
}
+ flow->direct = true;
+
+ flow->req_data = *pbuf;
+ clrbuf(*pbuf);
+
if (reg_flow_update(flow, info) < 0) {
log_err("Failed to create flow structs.");
goto fail_flow;
@@ -1955,6 +2103,614 @@ int reg_respond_accept(struct flow_info * info,
return -1;
}
+bool reg_flow_is_direct(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+
+ ret = flow != NULL && flow->direct;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+void reg_flow_set_rekey(int flow_id,
+ bool initiator,
+ buffer_t peer_crt)
+{
+ struct reg_flow * flow;
+ uint8_t * crt = NULL;
+
+ /* Copy the cert outside the lock; publish it with rk.encrypted. */
+ if (peer_crt.len > 0) {
+ crt = malloc(peer_crt.len);
+ if (crt != NULL)
+ memcpy(crt, peer_crt.data, peer_crt.len);
+ else
+ log_warn("Failed to cache peer cert for re-key.");
+ }
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ flow->rk.encrypted = true;
+ flow->rk.initiator = initiator;
+ flow->rk.epoch = 0;
+ if (crt != NULL) {
+ freebuf(flow->rk.peer_crt);
+ flow->rk.peer_crt.data = crt;
+ flow->rk.peer_crt.len = peer_crt.len;
+ crt = NULL;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ free(crt);
+}
+
+int reg_flow_get_peer_crt(int flow_id,
+ buffer_t * crt)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ assert(crt != NULL);
+
+ clrbuf(*crt);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.peer_crt.len > 0) {
+ crt->data = malloc(flow->rk.peer_crt.len);
+ if (crt->data == NULL) {
+ ret = -ENOMEM;
+ } else {
+ memcpy(crt->data, flow->rk.peer_crt.data,
+ flow->rk.peer_crt.len);
+ crt->len = flow->rk.peer_crt.len;
+ ret = 0;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_get_epoch(int flow_id)
+{
+ struct reg_flow * flow;
+ int epoch = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted)
+ epoch = flow->rk.epoch;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return epoch;
+}
+
+bool reg_flow_rekey_pending(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.has_pending;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+pid_t reg_flow_get_n_1_pid(int flow_id)
+{
+ struct reg_flow * flow;
+ pid_t pid = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ pid = flow->info.n_1_pid;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return pid;
+}
+
+int reg_flow_snapshot_rekey_due(struct rekey_info * snap,
+ int max)
+{
+ struct list_head * p;
+ int n = 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ llist_for_each(p, &reg.flows) {
+ struct reg_flow * f;
+
+ if (n == max)
+ break;
+
+ f = list_entry(p, struct reg_flow, next);
+
+ if (f->info.state != FLOW_ALLOCATED)
+ continue;
+
+ if (!f->rk.encrypted)
+ continue;
+
+ /* Direct flows have no IPCP initiator; either side drives. */
+ if (!f->direct && !f->rk.initiator)
+ continue;
+
+ if (f->rk.in_flight || f->rk.has_pending)
+ continue;
+
+ f->rk.in_flight = true;
+
+ snap[n].flow_id = f->info.id;
+ snap[n].n_pid = f->info.n_pid;
+ snap[n].n_1_pid = f->info.n_1_pid;
+ snap[n].epoch = f->rk.epoch;
+ snap[n].direct = f->direct;
+ strcpy(snap[n].name, f->name);
+ ++n;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return n;
+}
+
+void reg_flow_clear_in_flight(int flow_id)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ flow->rk.in_flight = false;
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+/* Test-and-set the in-flight latch; refuse if a re-key is already active. */
+bool reg_flow_rekey_begin(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted) {
+ if (!flow->rk.in_flight && !flow->rk.has_pending) {
+ flow->rk.in_flight = true;
+ ret = true;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Initiator yields the responder role while driving its own exchange. */
+bool reg_flow_rekey_should_yield(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.initiator && flow->rk.in_flight;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_store_pending(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch,
+ bool initiator)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ /* Exchange done: release the latch regardless of parking. */
+ flow->rk.in_flight = false;
+
+ if (flow->rk.pending_seed == NULL)
+ flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ);
+
+ if (flow->rk.pending_seed != NULL) {
+ memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ);
+ flow->rk.pending_epoch = epoch;
+ flow->rk.pending_initiator = initiator;
+ flow->rk.has_pending = true;
+ /* Doorbell raised only after the seed is parked. */
+ if (flow->n_rb != NULL)
+ ssm_rbuff_set_bits(flow->n_rb, RB_REKEY);
+ ret = 0;
+ } else {
+ ret = -ENOMEM;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Direct re-key: which of the two local apps has pulled the seed. */
+#define RK_N_PID 0x1 /* acceptor (n_pid) pulled the seed */
+#define RK_N_1_PID 0x2 /* allocator (n_1_pid) pulled the seed */
+#define RK_PID_MASK (RK_N_PID | RK_N_1_PID)
+
+/*
+ * Park a single re-key seed for a direct flow and ring BOTH apps'
+ * doorbells. The seed is the one shared secret; each app pulls it once
+ * (reg_flow_take_pending), so it is held until both have taken it.
+ */
+int reg_flow_store_pending_direct(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL)
+ goto out;
+
+ /* Exchange done: release the latch regardless of parking. */
+ flow->rk.in_flight = false;
+
+ if (flow->rk.pending_seed == NULL)
+ flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ);
+
+ if (flow->rk.pending_seed == NULL) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ);
+ flow->rk.pending_epoch = epoch;
+ flow->rk.has_pending = true;
+ flow->rk.pulled = 0;
+
+ /* A departed peer never pulls; treat its side as already done. */
+ if (flow->info.n_pid <= 0)
+ flow->rk.pulled |= RK_N_PID;
+
+ if (flow->info.n_1_pid <= 0)
+ flow->rk.pulled |= RK_N_1_PID;
+
+ if (flow->n_rb != NULL && !(flow->rk.pulled & RK_N_PID))
+ ssm_rbuff_set_bits(flow->n_rb, RB_REKEY);
+
+ if (flow->n_1_rb != NULL && !(flow->rk.pulled & RK_N_1_PID))
+ ssm_rbuff_set_bits(flow->n_1_rb, RB_REKEY);
+
+ ret = 0;
+ out:
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* A caller may act on a flow if it is privileged or owns the flow. */
+static bool uid_may_access(uid_t caller,
+ uid_t owner)
+{
+ return is_ouroboros_member_uid(caller) || caller == owner;
+}
+
+/*
+ * Caller holds reg.mtx. The direct seed is shared by both apps, so the
+ * per-app initiator role is resolved from the verified caller pid (the
+ * allocator is n_1_pid), and the seed is held until both have pulled.
+ */
+static void __take_pending_direct(struct reg_flow * flow,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator)
+{
+ bool allocator;
+
+ allocator = cpid == flow->info.n_1_pid;
+
+ memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ);
+ *epoch = flow->rk.pending_epoch;
+ *initiator = allocator;
+ flow->rk.epoch = flow->rk.pending_epoch;
+
+ if (allocator) {
+ flow->rk.pulled |= RK_N_1_PID;
+ if (flow->n_1_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_1_rb, RB_REKEY);
+ } else {
+ flow->rk.pulled |= RK_N_PID;
+ if (flow->n_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_rb, RB_REKEY);
+ }
+
+ if ((flow->rk.pulled & RK_PID_MASK) != RK_PID_MASK)
+ return;
+
+ flow->rk.has_pending = false;
+ flow->rk.pulled = 0;
+ crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ);
+}
+
+int reg_flow_take_pending(int flow_id,
+ uid_t uid,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL || !flow->rk.has_pending)
+ goto out;
+
+ if (!uid_may_access(uid, flow->info.uid)) {
+ ret = -EPERM;
+ goto out;
+ }
+
+ if (flow->direct) {
+ __take_pending_direct(flow, cpid, seed, epoch, initiator);
+ ret = 0;
+ goto out;
+ }
+
+ memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ);
+ *epoch = flow->rk.pending_epoch;
+ *initiator = flow->rk.pending_initiator;
+ flow->rk.epoch = flow->rk.pending_epoch;
+ flow->rk.has_pending = false;
+ crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ);
+ if (flow->n_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_rb, RB_REKEY);
+
+ ret = 0;
+ out:
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/*
+ * Admit a peer-driven re-key arrival before a worker event is allocated:
+ * the flow must exist, carry a cipher, and the update must come from its
+ * own lower IPCP. Coalesces to one queued REQ and one queued RESP per flow
+ * so a flooding peer cannot grow the inbox without bound.
+ */
+bool reg_flow_rekey_arr_admit(int flow_id,
+ pid_t n_1_pid,
+ bool is_req)
+{
+ struct reg_flow * flow;
+ bool admit = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted
+ && flow->info.n_1_pid == n_1_pid) {
+ if (is_req && !flow->rk.req_queued) {
+ flow->rk.req_queued = true;
+ admit = true;
+ } else if (!is_req && flow->rk.in_flight
+ && !flow->rk.resp_queued) {
+ flow->rk.resp_queued = true;
+ admit = true;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return admit;
+}
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ if (is_req)
+ flow->rk.req_queued = false;
+ else
+ flow->rk.resp_queued = false;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = uid_may_access(uid, flow->info.uid);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Caller holds reg.mtx. */
+static void __notify_proc(pid_t pid,
+ int flow_id,
+ int event)
+{
+ struct reg_proc * proc;
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL)
+ ssm_flow_set_notify(proc->set, flow_id, event);
+}
+
+void reg_notify_flow(int flow_id,
+ int event)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ __notify_proc(flow->info.n_pid, flow_id, event);
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+/* Wake both endpoints of a direct flow (acceptor and allocator). */
+void reg_notify_flow_peers(int flow_id,
+ int event)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ __notify_proc(flow->info.n_pid, flow_id, event);
+ __notify_proc(flow->info.n_1_pid, flow_id, event);
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+int reg_respond_flow_direct(int flow_id,
+ buffer_t * pbuf)
+{
+ struct reg_flow * flow;
+
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL) {
+ log_err("Flow %d not found.", flow_id);
+ goto fail;
+ }
+
+ assert(flow->direct);
+ assert(flow->rsp_data.data == NULL);
+
+ flow->rsp_data = *pbuf;
+ clrbuf(*pbuf);
+
+ pthread_cond_broadcast(&reg.cond);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return 0;
+ fail:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+int reg_wait_flow_direct(int flow_id,
+ buffer_t * pbuf,
+ const struct timespec * abstime)
+{
+ struct reg_flow * flow;
+ int ret = -1;
+
+ assert(pbuf != NULL);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL)
+ goto fail;
+
+ assert(flow->direct);
+
+ pthread_cleanup_push(__cleanup_mutex_unlock, &reg.mtx);
+
+ while (flow != NULL && flow->rsp_data.data == NULL) {
+ ret = -__timedwait(&reg.cond, &reg.mtx, abstime);
+ if (ret == -ETIMEDOUT)
+ break;
+ flow = __reg_get_flow(flow_id);
+ }
+
+ if (flow != NULL && flow->rsp_data.data != NULL) {
+ *pbuf = flow->rsp_data;
+ clrbuf(flow->rsp_data);
+ ret = 0;
+ }
+
+ pthread_cleanup_pop(true);
+
+ return ret;
+ fail:
+ pthread_mutex_unlock(&reg.mtx);
+ return -1;
+}
+
+static int direct_flow_dealloc(struct reg_flow * flow,
+ pid_t pid)
+{
+ if (!flow->direct)
+ return -1;
+
+ if (pid == flow->info.n_pid && flow->info.n_pid != -1)
+ flow->info.n_pid = -1;
+ else if (pid == flow->info.n_1_pid && flow->info.n_1_pid != -1)
+ flow->info.n_1_pid = -1;
+ else
+ return -1;
+
+ return 0;
+}
+
void reg_dealloc_flow(struct flow_info * info)
{
struct reg_flow * flow;
@@ -1968,13 +2724,34 @@ void reg_dealloc_flow(struct flow_info * info)
flow = __reg_get_flow(info->id);
assert(flow != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
+
+ info->n_1_pid = flow->info.n_1_pid;
+
+ if (flow->info.state == FLOW_DEALLOC_PENDING) {
+ if (direct_flow_dealloc(flow, info->n_pid) < 0) {
+ info->state = FLOW_DEALLOC_PENDING;
+ pthread_mutex_unlock(&reg.mtx);
+ return;
+ }
+ flow->info.state = FLOW_DEALLOCATED;
+ info->state = FLOW_DEALLOCATED;
+ reg_flow_update(flow, info);
+ pthread_mutex_unlock(&reg.mtx);
+ return;
+ }
assert(flow->info.state == FLOW_ALLOCATED);
+
flow->info.state = FLOW_DEALLOC_PENDING;
info->state = FLOW_DEALLOC_PENDING;
- info->n_1_pid = flow->info.n_1_pid;
+
+ direct_flow_dealloc(flow, info->n_pid);
+
+ memset(flow->name, 0, sizeof(flow->name));
reg_flow_update(flow, info);
@@ -1994,8 +2771,10 @@ void reg_dealloc_flow_resp(struct flow_info * info)
flow = __reg_get_flow(info->id);
assert(flow != NULL);
- assert(flow->data.data == NULL);
- assert(flow->data.len == 0);
+ assert(flow->req_data.data == NULL);
+ assert(flow->req_data.len == 0);
+ assert(flow->rsp_data.data == NULL);
+ assert(flow->rsp_data.len == 0);
assert(flow->info.state == FLOW_DEALLOC_PENDING);
flow->info.state = FLOW_DEALLOCATED;
@@ -2043,7 +2822,7 @@ int reg_wait_ipcp_boot(struct ipcp_info * info,
int ret;
bool stop = false;
- assert(info->state == IPCP_BOOT);
+ assert(info->state == IPCP_INIT);
pthread_mutex_lock(&reg.mtx);
@@ -2063,16 +2842,18 @@ int reg_wait_ipcp_boot(struct ipcp_info * info,
ret = -1;
stop = true;
break;
+ case IPCP_BOOT:
+ /* FALLTHRU*/
case IPCP_OPERATIONAL:
ret = 0;
stop = true;
break;
- case IPCP_BOOT:
+ case IPCP_INIT:
ret = -__timedwait(&reg.cond, &reg.mtx, abstime);
break;
default:
assert(false);
- continue; /* Shut up static analyzer. */
+ break; /* Shut up static analyzer. */
}
ipcp = __reg_get_ipcp(info->pid);
diff --git a/src/irmd/reg/reg.h b/src/irmd/reg/reg.h
index 17dfcc32..8a313d46 100644
--- a/src/irmd/reg/reg.h
+++ b/src/irmd/reg/reg.h
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry
*
@@ -31,6 +31,8 @@
#include <ouroboros/time.h>
#include <ouroboros/utils.h>
+#include "pool.h"
+
int reg_init(void);
void reg_clear(void);
@@ -50,6 +52,13 @@ int reg_destroy_proc(pid_t pid);
bool reg_has_proc(pid_t pid);
+bool reg_is_proc_privileged(pid_t pid);
+
+int reg_prepare_pool(uid_t uid,
+ gid_t gid);
+
+uid_t reg_get_proc_uid(pid_t pid);
+
void reg_kill_all_proc(int signal);
pid_t reg_get_dead_proc(void);
@@ -90,6 +99,19 @@ int reg_destroy_name(const char * name);
bool reg_has_name(const char * name);
+int reg_get_name_info(const char * name,
+ struct name_info * info);
+
+int reg_get_name_for_hash(char * buf,
+ enum hash_algo algo,
+ const uint8_t * hash);
+
+int reg_get_name_for_flow_id(char * buf,
+ int flow_id);
+
+void reg_set_name_for_flow_id(const char * name,
+ int flow_id);
+
/* TODO don't rely on protobuf here */
int reg_list_names(name_info_msg_t *** names);
@@ -99,9 +121,8 @@ int reg_destroy_prog(const char * name);
bool reg_has_prog(const char * name);
-int reg_get_exec(enum hash_algo algo,
- const uint8_t * hash,
- char *** exec);
+int reg_get_exec(const char * name,
+ char *** exec);
int reg_bind_prog(const char * name,
char ** exec,
@@ -117,22 +138,98 @@ int reg_wait_flow_allocated(struct flow_info * info,
const struct timespec * abstime);
int reg_respond_alloc(struct flow_info * info,
- buffer_t * pbuf);
+ buffer_t * pbuf,
+ int response);
-int reg_prepare_flow_accept(struct flow_info * info,
- buffer_t * pbuf);
+int reg_prepare_flow_accept(struct flow_info * info);
int reg_wait_flow_accepted(struct flow_info * info,
buffer_t * pbuf,
const struct timespec * abstime);
-int reg_wait_flow_accepting(enum hash_algo algo,
- const uint8_t * hash,
+int reg_wait_flow_accepting(const char * name,
const struct timespec * abstime);
int reg_respond_accept(struct flow_info * info,
buffer_t * pbuf);
+int reg_prepare_flow_direct(struct flow_info * info,
+ buffer_t * pbuf,
+ uid_t alloc_uid);
+
+int reg_respond_flow_direct(int flow_id,
+ buffer_t * pbuf);
+
+int reg_wait_flow_direct(int flow_id,
+ buffer_t * pbuf,
+ const struct timespec * abstime);
+
+bool reg_flow_is_direct(int flow_id);
+
+/* Per-flow snapshot for the re-key timer */
+struct rekey_info {
+ int flow_id;
+ pid_t n_pid;
+ pid_t n_1_pid;
+ char name[NAME_SIZE + 1];
+ uint8_t epoch;
+ bool direct;
+};
+
+void reg_flow_set_rekey(int flow_id,
+ bool initiator,
+ buffer_t peer_crt);
+
+int reg_flow_get_peer_crt(int flow_id,
+ buffer_t * crt);
+
+int reg_flow_get_epoch(int flow_id);
+
+bool reg_flow_rekey_pending(int flow_id);
+
+pid_t reg_flow_get_n_1_pid(int flow_id);
+
+int reg_flow_snapshot_rekey_due(struct rekey_info * snap,
+ int max);
+
+void reg_flow_clear_in_flight(int flow_id);
+
+bool reg_flow_rekey_begin(int flow_id);
+
+bool reg_flow_rekey_should_yield(int flow_id);
+
+int reg_flow_store_pending(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch,
+ bool initiator);
+
+int reg_flow_store_pending_direct(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch);
+
+int reg_flow_take_pending(int flow_id,
+ uid_t uid,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator);
+
+bool reg_flow_rekey_arr_admit(int flow_id,
+ pid_t n_1_pid,
+ bool is_req);
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req);
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid);
+
+void reg_notify_flow(int flow_id,
+ int event);
+
+void reg_notify_flow_peers(int flow_id,
+ int event);
+
void reg_dealloc_flow(struct flow_info * info);
void reg_dealloc_flow_resp(struct flow_info * info);
diff --git a/src/irmd/reg/tests/CMakeLists.txt b/src/irmd/reg/tests/CMakeLists.txt
index bc1354ed..e8521545 100644
--- a/src/irmd/reg/tests/CMakeLists.txt
+++ b/src/irmd/reg/tests/CMakeLists.txt
@@ -1,7 +1,9 @@
-get_filename_component(tmp ".." ABSOLUTE)
-get_filename_component(src_folder "${tmp}" NAME)
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
-create_test_sourcelist(${src_folder}_tests test_suite.c
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
# Add new tests here
flow_test.c
ipcp_test.c
@@ -11,19 +13,21 @@ create_test_sourcelist(${src_folder}_tests test_suite.c
reg_test.c
)
-add_executable(${src_folder}_test EXCLUDE_FROM_ALL ${${src_folder}_tests})
-target_link_libraries(${src_folder}_test ouroboros-common)
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
-if (CMAKE_BUILD_TYPE MATCHES "Debug*")
- add_compile_flags(${src_folder}_test -DCONFIG_OUROBOROS_DEBUG)
-endif ()
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+ ${CMAKE_SOURCE_DIR}/src/irmd
+ ${CMAKE_BINARY_DIR}/src/irmd
+)
-add_dependencies(check ${src_folder}_test)
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common)
+ouroboros_target_debug_definitions(${PARENT_DIR}_test)
-set(tests_to_run ${${src_folder}_tests})
-remove(tests_to_run test_suite.c)
+add_dependencies(build_tests ${PARENT_DIR}_test)
-foreach(test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(irmd/reg/${test_name} ${C_TEST_PATH}/${src_folder}_test ${test_name})
-endforeach(test)
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/irmd/reg/tests/flow_test.c b/src/irmd/reg/tests/flow_test.c
index f9d23fd1..18214078 100644
--- a/src/irmd/reg/tests/flow_test.c
+++ b/src/irmd/reg/tests/flow_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Flows - Unit Tests
*
@@ -22,13 +22,9 @@
#include "../flow.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
-#include <string.h>
-
-#define TEST_DATA "testpiggybackdata"
-
-static int test_reg_flow_create(void)
+static int test_reg_flow_create_destroy(void)
{
struct reg_flow * f;
@@ -51,10 +47,10 @@ static int test_reg_flow_create(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_flow_create_no_id(void) {
@@ -67,7 +63,7 @@ static int test_reg_flow_create_no_id(void) {
reg_flow_create(&info); /* assert fail */
- return 0;
+ return TEST_RC_SUCCESS;
}
static int test_reg_flow_create_no_pid(void) {
@@ -80,7 +76,7 @@ static int test_reg_flow_create_no_pid(void) {
reg_flow_create(&info); /* assert fail */
- return 0;
+ return TEST_RC_SUCCESS;
}
static int test_reg_flow_create_has_n_1_pid(void) {
@@ -94,7 +90,7 @@ static int test_reg_flow_create_has_n_1_pid(void) {
reg_flow_create(&info); /* assert fail */
- return 0;
+ return TEST_RC_SUCCESS;
}
static int test_reg_flow_create_wrong_state(void) {
@@ -108,7 +104,7 @@ static int test_reg_flow_create_wrong_state(void) {
reg_flow_create(&info); /* assert fail */
- return 0;
+ return TEST_RC_SUCCESS;
}
static int test_reg_flow_create_has_mpl(void) {
@@ -123,7 +119,22 @@ static int test_reg_flow_create_has_mpl(void) {
reg_flow_create(&info); /* assert fail */
- return 0;
+ return TEST_RC_SUCCESS;
+}
+
+static int test_reg_flow_create_has_mtu(void) {
+ struct flow_info info = {
+ .id = 1,
+ .n_pid = 1,
+ .n_1_pid = 0,
+ .mtu = 1400,
+ .qs = qos_raw,
+ .state = FLOW_ALLOC_PENDING
+ };
+
+ reg_flow_create(&info); /* assert fail */
+
+ return TEST_RC_SUCCESS;
}
static int test_reg_flow_update(void)
@@ -140,7 +151,7 @@ static int test_reg_flow_update(void)
struct flow_info upd = {
.id = 1,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -163,10 +174,10 @@ static int test_reg_flow_update(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_flow_update_wrong_id(void)
@@ -183,7 +194,7 @@ static int test_reg_flow_update_wrong_id(void)
struct flow_info upd = {
.id = 2,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -199,10 +210,10 @@ static int test_reg_flow_update_wrong_id(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_flow_assert_fails(void)
@@ -210,70 +221,16 @@ static int test_reg_flow_assert_fails(void)
int ret = 0;
ret |= test_assert_fail(test_reg_flow_create_no_id);
-
ret |= test_assert_fail(test_reg_flow_create_no_pid);
-
ret |= test_assert_fail(test_reg_flow_create_has_n_1_pid);
-
ret |= test_assert_fail(test_reg_flow_create_wrong_state);
-
ret |= test_assert_fail(test_reg_flow_create_has_mpl);
-
+ ret |= test_assert_fail(test_reg_flow_create_has_mtu);
ret |= test_assert_fail(test_reg_flow_update_wrong_id);
return ret;
}
-static int test_flow_data(void)
-{
- struct reg_flow * f;
-
- struct flow_info info = {
- .id = 1,
- .n_pid = 1,
- .qs = qos_raw,
- .state = FLOW_INIT
- };
-
- char * data;
- buffer_t buf;
- buffer_t rcv = {NULL, 0};
-
- TEST_START();
-
- data = strdup(TEST_DATA);
- if (data == NULL) {
- printf("Failed to strdup data.\n");
- goto fail;
- }
-
- buf.data = (uint8_t *) data;
- buf.len = strlen(data);
-
- f = reg_flow_create(&info);
- if (f == NULL) {
- printf("Failed to create flow.\n");
- goto fail;
- }
-
- reg_flow_set_data(f, &buf);
-
- reg_flow_get_data(f, &rcv);
-
- freebuf(buf);
- clrbuf(rcv);
-
- reg_flow_destroy(f);
-
- TEST_SUCCESS();
-
- return 0;
- fail:
- free(data);
- TEST_FAIL();
- return -1;
-}
-
int flow_test(int argc,
char ** argv)
{
@@ -282,13 +239,9 @@ int flow_test(int argc,
(void) argc;
(void) argv;
- ret |= test_reg_flow_create();
-
+ ret |= test_reg_flow_create_destroy();
ret |= test_reg_flow_update();
-
ret |= test_reg_flow_assert_fails();
- ret |= test_flow_data();
-
return ret;
}
diff --git a/src/irmd/reg/tests/ipcp_test.c b/src/irmd/reg/tests/ipcp_test.c
index fb8ba71b..5279283e 100644
--- a/src/irmd/reg/tests/ipcp_test.c
+++ b/src/irmd/reg/tests/ipcp_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - IPCPs - Unit Tests
*
@@ -20,7 +20,7 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
-#include <ouroboros/test.h>
+#include <test/test.h>
#include "../ipcp.c"
@@ -31,7 +31,7 @@ static int test_reg_ipcp_create(void)
struct reg_ipcp * ipcp;
struct ipcp_info info = {
.pid = TEST_PID,
- .state = IPCP_BOOT
+ .state = IPCP_INIT
};
struct layer_info layer = {
.name = "testlayer",
@@ -51,7 +51,7 @@ static int test_reg_ipcp_create(void)
goto fail;
}
- ipcp->info.state = IPCP_OPERATIONAL;
+ ipcp->info.state = IPCP_BOOT;
reg_ipcp_set_layer(ipcp, &layer);
@@ -60,11 +60,6 @@ static int test_reg_ipcp_create(void)
goto fail;
}
- if (ipcp->info.state != IPCP_OPERATIONAL) {
- printf("IPCP state was not set.\n");
- goto fail;
- }
-
reg_ipcp_destroy(ipcp);
TEST_SUCCESS();
diff --git a/src/irmd/reg/tests/name_test.c b/src/irmd/reg/tests/name_test.c
index 48f132e9..403c8a6c 100644
--- a/src/irmd/reg/tests/name_test.c
+++ b/src/irmd/reg/tests/name_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Names - Unit Tests
*
@@ -20,8 +20,11 @@
* Foundation, Inc., http://www.fsf.org/about/contact/.
*/
+
#include "../name.c"
+#include <test/test.h>
+
#define TEST_PID 65534
#define TEST_PROG "/usr/bin/testprog"
#define TEST_NAME "testservicename"
@@ -34,6 +37,8 @@ static int test_reg_name_create(void)
.pol_lb = LB_RR,
};
+ TEST_START();
+
n = reg_name_create(&info);
if (n == NULL) {
printf("Failed to create name %s.\n", info.name);
@@ -42,9 +47,12 @@ static int test_reg_name_create(void)
reg_name_destroy(n);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_name_add_proc(void)
@@ -55,6 +63,8 @@ static int test_reg_name_add_proc(void)
.pol_lb = LB_RR,
};
+ TEST_START();
+
n = reg_name_create(&info);
if (n == NULL) {
printf("Failed to create name %s.\n", info.name);
@@ -66,8 +76,8 @@ static int test_reg_name_add_proc(void)
goto fail;
}
- if (n->n_procs != 1) {
- printf("n_procs not updated.\n");
+ if (n->procs.len != 1) {
+ printf("Proc not added to list.\n");
goto fail;
}
@@ -78,16 +88,19 @@ static int test_reg_name_add_proc(void)
reg_name_del_proc(n, TEST_PID);
- if (n->n_procs != 0) {
- printf("n_procs not updated.\n");
+ if (!llist_is_empty(&n->procs)) {
+ printf("Proc not removed from list.\n");
goto fail;
}
reg_name_destroy(n);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_name_add_prog(void)
@@ -100,6 +113,8 @@ static int test_reg_name_add_prog(void)
char * exec[] = { TEST_PROG, "--argswitch", "argvalue", NULL};
+ TEST_START();
+
n = reg_name_create(&info);
if (n == NULL) {
printf("Failed to create name %s.\n", info.name);
@@ -111,8 +126,8 @@ static int test_reg_name_add_prog(void)
goto fail;
}
- if (n->n_progs != 1) {
- printf("n_progs not updated.\n");
+ if (n->progs.len != 1) {
+ printf("Prog not added to list.\n");
goto fail;
}
@@ -123,16 +138,19 @@ static int test_reg_name_add_prog(void)
reg_name_del_prog(n, TEST_PROG);
- if (n->n_progs != 0) {
- printf("n_progs not updated.\n");
+ if (!llist_is_empty(&n->progs)) {
+ printf("Prog not removed from list.\n");
goto fail;
}
reg_name_destroy(n);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_name_add_active(enum pol_balance lb)
@@ -144,6 +162,8 @@ static int test_reg_name_add_active(enum pol_balance lb)
.pol_lb = lb,
};
+ TEST_START();
+
n = reg_name_create(&info);
if (n == NULL) {
printf("Failed to create name %s.\n", info.name);
@@ -175,8 +195,8 @@ static int test_reg_name_add_active(enum pol_balance lb)
goto fail;
}
- if (n->n_active != 1) {
- printf("n_active not updated.\n");
+ if (n->active.len != 1) {
+ printf("Active list not updated.\n");
goto fail;
}
@@ -206,13 +226,13 @@ static int test_reg_name_add_active(enum pol_balance lb)
goto fail;
}
- if (n->n_procs != 3) {
- printf("n_procs not updated.\n");
+ if (n->procs.len != 3) {
+ printf("Procs list not updated.\n");
goto fail;
}
- if (n->n_active != 4) {
- printf("n_active not updated.\n");
+ if (n->active.len != 4) {
+ printf("Active list not updated.\n");
goto fail;
}
@@ -243,41 +263,39 @@ static int test_reg_name_add_active(enum pol_balance lb)
reg_name_del_proc(n, TEST_PID);
- if (n->n_procs != 0) {
- printf("n_procs not updated.\n");
+ if (!llist_is_empty(&n->procs)) {
+ printf("Procs list not cleared.\n");
goto fail;
}
- if (n->n_active != 0) {
- printf("n_active not updated.\n");
+ if (!llist_is_empty(&n->active)) {
+ printf("Active list not cleared.\n");
goto fail;
}
reg_name_destroy(n);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
-
int name_test(int argc,
char ** argv)
{
- int res = 0;
+ int rc = 0;
(void) argc;
(void) argv;
- res |= test_reg_name_create();
-
- res |= test_reg_name_add_proc();
-
- res |= test_reg_name_add_prog();
-
- res |= test_reg_name_add_active(LB_RR);
-
- res |= test_reg_name_add_active(LB_SPILL);
+ rc |= test_reg_name_create();
+ rc |= test_reg_name_add_proc();
+ rc |= test_reg_name_add_prog();
+ rc |= test_reg_name_add_active(LB_RR);
+ rc |= test_reg_name_add_active(LB_SPILL);
- return res;
+ return rc;
}
diff --git a/src/irmd/reg/tests/proc_test.c b/src/irmd/reg/tests/proc_test.c
index 5c9dd865..a85f4039 100644
--- a/src/irmd/reg/tests/proc_test.c
+++ b/src/irmd/reg/tests/proc_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Processes - Unit Tests
*
@@ -22,16 +22,24 @@
#include "../proc.c"
+#include <test/test.h>
+
#define TEST_PID 65534
#define TEST_PROG "usr/bin/testprog"
-static int test_reg_proc_create(void)
+#define TEST_PROC { \
+ .pid = TEST_PID, \
+ .prog = TEST_PROG, \
+ .uid = getuid(), \
+ .gid = getgid() \
+}
+
+static int test_reg_proc_create_destroy(void)
{
struct reg_proc * proc;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC;
+
+ TEST_START();
proc = reg_proc_create(&info);
if (proc == NULL) {
@@ -41,21 +49,23 @@ static int test_reg_proc_create(void)
reg_proc_destroy(proc);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_proc_add_name(void)
{
struct reg_proc * proc;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC;
char * name = "testname";
+ TEST_START();
+
proc = reg_proc_create(&info);
if (proc == NULL) {
printf("Failed to create proc.\n");
@@ -67,7 +77,7 @@ static int test_reg_proc_add_name(void)
goto fail;
}
- if (proc->n_names != 1) {
+ if (proc->names.len != 1) {
printf("n_names not updated.\n");
goto fail;
}
@@ -79,16 +89,19 @@ static int test_reg_proc_add_name(void)
reg_proc_del_name(proc, name);
- if (proc->n_names != 0) {
+ if (!llist_is_empty(&proc->names)) {
printf("n_names not updated.\n");
goto fail;
}
reg_proc_destroy(proc);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
int proc_test(int argc,
@@ -99,8 +112,7 @@ int proc_test(int argc,
(void) argc;
(void) argv;
- res |= test_reg_proc_create();
-
+ res |= test_reg_proc_create_destroy();
res |= test_reg_proc_add_name();
return res;
diff --git a/src/irmd/reg/tests/prog_test.c b/src/irmd/reg/tests/prog_test.c
index 5e6931d8..91264aba 100644
--- a/src/irmd/reg/tests/prog_test.c
+++ b/src/irmd/reg/tests/prog_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Programs - Unit Tests
*
@@ -22,8 +22,9 @@
#include "../prog.c"
-#define TEST_PROG "usr/bin/testprog"
+#include <test/test.h>
+#define TEST_PROG "usr/bin/testprog"
static int test_reg_prog_create(void)
{
@@ -32,6 +33,8 @@ static int test_reg_prog_create(void)
.name = TEST_PROG
};
+ TEST_START();
+
prog = reg_prog_create(&info);
if (prog == NULL) {
printf("Failed to create prog.\n");
@@ -40,9 +43,12 @@ static int test_reg_prog_create(void)
reg_prog_destroy(prog);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_prog_add_name(void)
@@ -54,6 +60,8 @@ static int test_reg_prog_add_name(void)
char * name = "testname";
+ TEST_START();
+
prog = reg_prog_create(&info);
if (prog == NULL) {
printf("Failed to create prog.\n");
@@ -65,7 +73,7 @@ static int test_reg_prog_add_name(void)
goto fail;
}
- if (prog->n_names != 1) {
+ if (prog->names.len != 1) {
printf("n_names not updated.\n");
goto fail;
}
@@ -77,16 +85,19 @@ static int test_reg_prog_add_name(void)
reg_prog_del_name(prog, name);
- if (prog->n_names != 0) {
+ if (!llist_is_empty(&prog->names)) {
printf("n_names not updated.\n");
goto fail;
}
reg_prog_destroy(prog);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
fail:
- return -1;
+ TEST_FAIL();
+ return TEST_RC_FAIL;
}
int prog_test(int argc,
@@ -98,7 +109,6 @@ int prog_test(int argc,
(void) argv;
ret |= test_reg_prog_create();
-
ret |= test_reg_prog_add_name();
return ret;
diff --git a/src/irmd/reg/tests/reg_test.c b/src/irmd/reg/tests/reg_test.c
index c341c297..a8c1b1fa 100644
--- a/src/irmd/reg/tests/reg_test.c
+++ b/src/irmd/reg/tests/reg_test.c
@@ -1,5 +1,5 @@
/*
- * Ouroboros - Copyright (C) 2016 - 2024
+ * Ouroboros - Copyright (C) 2016 - 2026
*
* The IPC Resource Manager - Registry - Unit Tests
*
@@ -21,22 +21,31 @@
*/
+#include "../pool.c"
+#undef OUROBOROS_PREFIX
#include "../reg.c"
-#include <ouroboros/test.h>
+#include <test/test.h>
#define TEST_PID 3666
#define TEST_N_1_PID 3999
#define TEST_FAKE_ID 9128349
#define TEST_MPL 5
+#define TEST_MTU 1400
#define TEST_PROG "reg_test" /* own binary for binary check */
#define TEST_IPCP "testipcp"
#define TEST_NAME "testname"
#define TEST_DATA "testpbufdata"
#define TEST_DATA2 "testpbufdata2"
#define TEST_LAYER "testlayer"
+#define TEST_PROC_INFO { \
+ .pid = TEST_PID, \
+ .prog = TEST_PROG, \
+ .uid = 0, \
+ .gid = 0 \
+}
#define REG_TEST_FAIL() \
- do { TEST_FAIL(); memset(&reg, 0, sizeof(reg)); } while(0)
+ do { TEST_FAIL(); reg_clear(); return TEST_RC_FAIL;} while(0)
static int test_reg_init(void)
{
@@ -51,10 +60,10 @@ static int test_reg_init(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_create_flow(void)
@@ -81,7 +90,7 @@ static int test_reg_create_flow(void)
goto fail;
}
- if (reg.n_flows != 1) {
+ if (reg.flows.len != 1) {
printf("n_flows was not updated.\n");
goto fail;
}
@@ -96,8 +105,8 @@ static int test_reg_create_flow(void)
goto fail;
}
- if (reg.n_flows != 0) {
- printf("n_flows was not updated.\n");
+ if (!llist_is_empty(&reg.flows)) {
+ printf("flows.len was not updated.\n");
goto fail;
}
@@ -105,18 +114,17 @@ static int test_reg_create_flow(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_allocate_flow_timeout(void)
{
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_MS(1);
- buffer_t pbuf;
- buffer_t rbuf = {NULL, 0};
+ buffer_t rbuf = BUF_INIT;
struct flow_info info = {
.n_pid = TEST_PID,
@@ -125,14 +133,6 @@ static int test_reg_allocate_flow_timeout(void)
TEST_START();
- pbuf.data = (uint8_t *) strdup(TEST_DATA);;
- if (pbuf.data == NULL) {
- printf("Failed to strdup data.\n");
- goto fail;
- }
-
- pbuf.len = strlen((char *) pbuf.data) + 1;
-
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
@@ -147,7 +147,7 @@ static int test_reg_allocate_flow_timeout(void)
goto fail;
}
- if (reg_prepare_flow_accept(&info, &pbuf) < 0) {
+ if (reg_prepare_flow_accept(&info) < 0) {
printf("Failed to prepare flow for accept.\n");
goto fail;
}
@@ -162,15 +162,9 @@ static int test_reg_allocate_flow_timeout(void)
goto fail;
}
- if (pbuf.data == NULL) {
- printf("Flow data was updated on timeout.");
- goto fail;
- }
-
- freebuf(pbuf);
reg_destroy_flow(info.id);
- if (reg.n_flows != 0) {
+ if (!llist_is_empty(&reg.flows)) {
printf("Flow did not destroy.\n");
goto fail;
}
@@ -179,16 +173,19 @@ static int test_reg_allocate_flow_timeout(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static void * test_flow_respond_alloc(void * o)
{
struct flow_info * info = (struct flow_info *) o;
- buffer_t pbuf = {NULL, 0};
+ buffer_t pbuf = BUF_INIT;
+ int response;
+
+ response = (info->state == FLOW_ALLOCATED) ? 0 : -1;
if (info->state == FLOW_ALLOCATED) {
pbuf.data = (uint8_t *) strdup(TEST_DATA2);
@@ -199,7 +196,9 @@ static void * test_flow_respond_alloc(void * o)
pbuf.len = strlen((char *) pbuf.data) + 1;
}
- reg_respond_alloc(info, &pbuf);
+ reg_respond_alloc(info, &pbuf, response);
+
+ freebuf(pbuf);
return (void *) 0;
fail:
@@ -220,12 +219,7 @@ static void * test_flow_respond_accept(void * o)
reg_respond_accept(info, &pbuf);
- if (info->qs.cypher_s == 0) {
- freebuf(pbuf);
- } else if (strcmp((char *) pbuf.data, TEST_DATA) != 0) {
- printf("Data was not passed correctly.\n");
- goto fail;
- }
+ freebuf(pbuf);
return (void *) 0;
fail:
@@ -237,8 +231,7 @@ static int test_reg_accept_flow_success(void)
pthread_t thr;
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_S(1);
- buffer_t pbuf = {(uint8_t *) TEST_DATA, strlen(TEST_DATA)};
- buffer_t rbuf = {NULL, 0};
+ buffer_t rbuf = BUF_INIT;
struct flow_info info = {
.n_pid = TEST_PID,
@@ -247,7 +240,7 @@ static int test_reg_accept_flow_success(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data_crypt,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -267,18 +260,22 @@ static int test_reg_accept_flow_success(void)
goto fail;
}
- if (reg_prepare_flow_accept(&info, &pbuf) < 0) {
+ if (reg_prepare_flow_accept(&info) < 0) {
printf("Failed to prepare flow for accept.\n");
goto fail;
}
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
- if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0 ) {
+ if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) {
printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -289,6 +286,11 @@ static int test_reg_accept_flow_success(void)
goto fail;
}
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
if (rbuf.data == NULL) {
printf("rbuf data not returned.\n");
goto fail;
@@ -321,10 +323,10 @@ static int test_reg_accept_flow_success(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_accept_flow_success_no_crypt(void)
@@ -332,8 +334,7 @@ static int test_reg_accept_flow_success_no_crypt(void)
pthread_t thr;
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_S(1);
- buffer_t pbuf = {(uint8_t *) TEST_DATA, strlen(TEST_DATA)};
- buffer_t rbuf = {NULL, 0};
+ buffer_t rbuf = BUF_INIT;
struct flow_info info = {
.n_pid = TEST_PID,
@@ -342,7 +343,7 @@ static int test_reg_accept_flow_success_no_crypt(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -362,18 +363,22 @@ static int test_reg_accept_flow_success_no_crypt(void)
goto fail;
}
- if (reg_prepare_flow_accept(&info, &pbuf) < 0) {
+ if (reg_prepare_flow_accept(&info) < 0) {
printf("Failed to prepare flow for accept.\n");
goto fail;
}
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0 ) {
printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -384,16 +389,18 @@ static int test_reg_accept_flow_success_no_crypt(void)
goto fail;
}
- if (rbuf.data == NULL) {
- printf("rbuf data was not returned.\n");
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
goto fail;
}
- if (strcmp((char *) rbuf.data, TEST_DATA) != 0) {
- printf("Data was updated.\n");
+ if (rbuf.data == NULL) {
+ printf("rbuf data was not returned.\n");
goto fail;
}
+ freebuf(rbuf);
+
n_1_info.state = FLOW_DEALLOCATED;
reg_dealloc_flow(&info);
@@ -416,16 +423,16 @@ static int test_reg_accept_flow_success_no_crypt(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_allocate_flow_fail(void)
{
- buffer_t buf = {NULL, 0};
+ buffer_t buf = BUF_INIT;
pthread_t thr;
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_S(1);
@@ -437,7 +444,7 @@ static int test_reg_allocate_flow_fail(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED /* RESPONSE FAIL */
};
@@ -470,6 +477,9 @@ static int test_reg_allocate_flow_fail(void)
if (reg_wait_flow_allocated(&info, &buf, &abstime) == 0 ) {
printf("Flow allocation succeeded.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
goto fail;
}
@@ -486,26 +496,455 @@ static int test_reg_allocate_flow_fail(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
-static int test_reg_flow(void) {
- int ret = 0;
+static int test_reg_respond_alloc_duplicate(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t empty = BUF_INIT;
+ struct flow_info dup_info;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ struct flow_info n_1_info = {
+ .n_1_pid = TEST_N_1_PID,
+ .qs = qos_msg,
+ .state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
+ };
+
+ TEST_START();
+
+ clock_gettime(PTHREAD_COND_CLOCK, &abstime);
+ ts_add(&abstime, &timeo, &abstime);
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ if (reg_create_flow(&info) < 0) {
+ printf("Failed to add flow.\n");
+ goto fail;
+ }
+
+ info.n_1_pid = TEST_N_1_PID;
+
+ if (reg_prepare_flow_alloc(&info) < 0) {
+ printf("Failed to prepare flow for alloc.\n");
+ goto fail;
+ }
+
+ n_1_info.id = info.id;
+ n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
+
+ pthread_create(&thr, NULL, test_flow_respond_alloc, &n_1_info);
+
+ if (reg_wait_flow_allocated(&info, &rbuf, &abstime) < 0) {
+ printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
+ goto fail;
+ }
+
+ pthread_join(thr, NULL);
+ freebuf(rbuf);
+
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
+ /* Duplicate reply on an already-ALLOCATED flow must not assert. */
+ dup_info = n_1_info;
+ dup_info.state = FLOW_DEALLOCATED;
+
+ if (reg_respond_alloc(&dup_info, &empty, -EREPLAY) != -1) {
+ printf("Duplicate respond_alloc should return -1.\n");
+ goto fail;
+ }
+
+ reg_dealloc_flow(&info);
+ reg_dealloc_flow_resp(&info);
+ reg_destroy_flow(n_1_info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+struct direct_alloc_info {
+ struct flow_info info;
+ buffer_t rsp;
+ struct timespec abstime;
+};
+
+static void * test_flow_alloc_direct(void * o)
+{
+ struct direct_alloc_info * dai;
+ buffer_t req;
+
+ dai = (struct direct_alloc_info *) o;
+
+ req.data = (uint8_t *) strdup(TEST_DATA);
+ if (req.data == NULL) {
+ printf("Failed to strdup req data.\n");
+ goto fail;
+ }
+ req.len = strlen(TEST_DATA) + 1;
+
+ if (reg_prepare_flow_direct(&dai->info, &req, 0) < 0) {
+ printf("Failed to prepare direct flow.\n");
+ freebuf(req);
+ goto fail;
+ }
+
+ if (reg_wait_flow_direct(dai->info.id, &dai->rsp, &dai->abstime) < 0) {
+ printf("Failed to wait direct flow.\n");
+ goto fail;
+ }
- ret |= test_reg_create_flow();
+ return (void *) 0;
+ fail:
+ return (void *) -1;
+}
+
+static int test_reg_direct_flow_success(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t rsp;
+ struct direct_alloc_info dai;
+ void * thr_ret;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ TEST_START();
+
+ clock_gettime(PTHREAD_COND_CLOCK, &abstime);
+
+ ts_add(&abstime, &timeo, &abstime);
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ if (reg_create_flow(&info) < 0) {
+ printf("Failed to add flow.\n");
+ goto fail;
+ }
+
+ if (reg_prepare_flow_accept(&info) < 0) {
+ printf("Failed to prepare for accept.\n");
+ goto fail;
+ }
+
+ dai.info.id = info.id;
+ dai.info.n_1_pid = TEST_N_1_PID;
+ dai.info.mpl = TEST_MPL;
+ dai.info.qs = qos_msg;
+ dai.info.state = FLOW_ALLOCATED;
+ dai.rsp.len = 0;
+ dai.rsp.data = NULL;
+ dai.abstime = abstime;
+
+ pthread_create(&thr, NULL, test_flow_alloc_direct, &dai);
+
+ if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) {
+ printf("Flow accept failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
+ goto fail;
+ }
+
+ if (info.state != FLOW_ALLOCATED) {
+ printf("Flow not in allocated state.\n");
+ goto fail;
+ }
+
+ if (rbuf.data == NULL) {
+ printf("req_data not received.\n");
+ goto fail;
+ }
+
+ if (strcmp((char *) rbuf.data, TEST_DATA) != 0) {
+ printf("req_data content mismatch.\n");
+ goto fail;
+ }
+
+ freebuf(rbuf);
+
+ if (!reg_flow_is_direct(info.id)) {
+ printf("Flow not marked direct.\n");
+ goto fail;
+ }
+
+ rsp.data = (uint8_t *) strdup(TEST_DATA2);
+ if (rsp.data == NULL) {
+ printf("Failed to strdup rsp data.\n");
+ goto fail;
+ }
+ rsp.len = strlen(TEST_DATA2) + 1;
+
+ if (reg_respond_flow_direct(info.id, &rsp) < 0) {
+ printf("Failed to respond direct.\n");
+ freebuf(rsp);
+ goto fail;
+ }
+
+ pthread_join(thr, &thr_ret);
+
+ if (thr_ret != (void *) 0) {
+ printf("Allocator thread failed.\n");
+ goto fail;
+ }
+
+ if (dai.rsp.data == NULL) {
+ printf("rsp_data not received.\n");
+ goto fail;
+ }
+
+ if (strcmp((char *) dai.rsp.data, TEST_DATA2) != 0) {
+ printf("rsp_data content mismatch.\n");
+ goto fail;
+ }
+
+ freebuf(dai.rsp);
+
+ reg_dealloc_flow(&info);
+
+ if (info.state != FLOW_DEALLOC_PENDING) {
+ printf("Flow not in dealloc pending.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_PID;
+
+ reg_dealloc_flow(&info);
+
+ if (info.state != FLOW_DEALLOC_PENDING) {
+ printf("Same endpoint dealloc changed state.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_N_1_PID;
+
+ reg_dealloc_flow(&info);
- ret |= test_reg_allocate_flow_timeout();
+ if (info.state != FLOW_DEALLOCATED) {
+ printf("Flow not deallocated.\n");
+ goto fail;
+ }
- ret |= test_reg_accept_flow_success();
+ reg_destroy_flow(info.id);
- ret |= test_reg_accept_flow_success_no_crypt();
+ reg_fini();
- ret |= test_reg_allocate_flow_fail();
+ TEST_SUCCESS();
- return ret;
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Direct-flow re-key: one shared seed is parked for both local apps. The
+ * per-app initiator role is resolved from the verified caller pid (the
+ * allocator is n_1_pid), and the seed is held until both have pulled it.
+ */
+static int test_reg_direct_flow_rekey(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t rsp;
+ buffer_t no_crt = BUF_INIT;
+ struct direct_alloc_info dai;
+ uint8_t seed[SYMMKEYSZ];
+ uint8_t out[SYMMKEYSZ];
+ uint8_t epoch;
+ bool initiator;
+ size_t i;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ TEST_START();
+
+ for (i = 0; i < SYMMKEYSZ; ++i)
+ seed[i] = (uint8_t) i;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &abstime);
+
+ ts_add(&abstime, &timeo, &abstime);
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ if (reg_create_flow(&info) < 0) {
+ printf("Failed to add flow.\n");
+ goto fail;
+ }
+
+ if (reg_prepare_flow_accept(&info) < 0) {
+ printf("Failed to prepare for accept.\n");
+ goto fail;
+ }
+
+ dai.info.id = info.id;
+ dai.info.n_1_pid = TEST_N_1_PID;
+ dai.info.mpl = TEST_MPL;
+ dai.info.qs = qos_msg;
+ dai.info.state = FLOW_ALLOCATED;
+ dai.rsp.len = 0;
+ dai.rsp.data = NULL;
+ dai.abstime = abstime;
+
+ pthread_create(&thr, NULL, test_flow_alloc_direct, &dai);
+
+ if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) {
+ printf("Flow accept failed.\n");
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+
+ freebuf(rbuf);
+
+ rsp.data = (uint8_t *) strdup(TEST_DATA2);
+ if (rsp.data == NULL) {
+ printf("Failed to strdup rsp data.\n");
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+ rsp.len = strlen(TEST_DATA2) + 1;
+
+ if (reg_respond_flow_direct(info.id, &rsp) < 0) {
+ printf("Failed to respond direct.\n");
+ freebuf(rsp);
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+
+ pthread_join(thr, NULL);
+
+ freebuf(dai.rsp);
+
+ if (!reg_flow_is_direct(info.id)) {
+ printf("Flow not marked direct.\n");
+ goto fail;
+ }
+
+ reg_flow_set_rekey(info.id, false, no_crt);
+
+ if (reg_flow_store_pending_direct(info.id, seed, 5) < 0) {
+ printf("Failed to store pending direct seed.\n");
+ goto fail;
+ }
+
+ if (!reg_flow_rekey_pending(info.id)) {
+ printf("Seed not pending after store.\n");
+ goto fail;
+ }
+
+ /* Allocator (n_1_pid) pulls: initiator role, seed still held. */
+ if (reg_flow_take_pending(info.id, 0, TEST_N_1_PID, out,
+ &epoch, &initiator) != 0) {
+ printf("Allocator failed to take pending seed.\n");
+ goto fail;
+ }
+
+ if (!initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) {
+ printf("Allocator got wrong seed/role/epoch.\n");
+ goto fail;
+ }
+
+ if (!reg_flow_rekey_pending(info.id)) {
+ printf("Seed cleared before both apps pulled.\n");
+ goto fail;
+ }
+
+ /* Acceptor (n_pid) pulls: responder role, seed now released. */
+ if (reg_flow_take_pending(info.id, 0, TEST_PID, out,
+ &epoch, &initiator) != 0) {
+ printf("Acceptor failed to take pending seed.\n");
+ goto fail;
+ }
+
+ if (initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) {
+ printf("Acceptor got wrong seed/role/epoch.\n");
+ goto fail;
+ }
+
+ if (reg_flow_rekey_pending(info.id)) {
+ printf("Seed still pending after both pulled.\n");
+ goto fail;
+ }
+
+ if (reg_flow_get_epoch(info.id) != 5) {
+ printf("Flow epoch not advanced.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_PID;
+ reg_dealloc_flow(&info);
+
+ info.n_pid = TEST_N_1_PID;
+ reg_dealloc_flow(&info);
+
+ reg_destroy_flow(info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_reg_flow(void) {
+ int rc = 0;
+
+ rc |= test_reg_create_flow();
+ rc |= test_reg_allocate_flow_timeout();
+ rc |= test_reg_accept_flow_success();
+ rc |= test_reg_accept_flow_success_no_crypt();
+ rc |= test_reg_allocate_flow_fail();
+ rc |= test_reg_respond_alloc_duplicate();
+ rc |= test_reg_direct_flow_success();
+ rc |= test_reg_direct_flow_rekey();
+
+ return rc;
}
static int test_reg_create_ipcp(void)
@@ -513,7 +952,7 @@ static int test_reg_create_ipcp(void)
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_BOOT /* set by spawn_ipcp */
+ .state = IPCP_INIT /* set by spawn_ipcp */
};
TEST_START();
@@ -528,7 +967,7 @@ static int test_reg_create_ipcp(void)
goto fail;
}
- if (reg.n_ipcps != 1) {
+ if (reg.ipcps.len != 1) {
printf("n_ipcps was not updated.\n");
goto fail;
}
@@ -543,19 +982,141 @@ static int test_reg_create_ipcp(void)
goto fail;
}
- if (reg.n_ipcps != 0) {
- printf("n_ipcps was not updated.\n");
+ if (reg.ipcps.len != 0) {
+ printf("ipcps.len was not updated.\n");
+ goto fail;
+ }
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_reg_list_ipcps(void)
+{
+ ipcp_list_msg_t ** ipcps;
+ int i;
+ ssize_t len;
+
+ TEST_START();
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < 10; i++) {
+ struct ipcp_info info = {
+ .pid = TEST_PID + i,
+ .state = IPCP_INIT /* set by spawn_ipcp */
+ };
+
+ sprintf(info.name, "%s%d", TEST_IPCP, i);
+
+ if (reg_create_ipcp(&info) < 0) {
+ printf("Failed to create ipcp %d.\n", i);
+ goto fail;
+ }
+ }
+
+ len = reg_list_ipcps(&ipcps);
+ if (len < 0) {
+ printf("Failed to list ipcps.\n");
goto fail;
}
+ if (len != 10) {
+ printf("Failed to list all ipcps.\n");
+ goto fail;
+ }
+
+ while (len-- > 0)
+ ipcp_list_msg__free_unpacked(ipcps[len], NULL);
+
+ free(ipcps);
+
+ for (i = 0; i < 10; i++)
+ reg_destroy_proc(TEST_PID + i);
+
reg_fini();
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
+
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
+}
+
+static int test_insert_ipcps(void)
+{
+ ipcp_list_msg_t ** ipcps;
+ struct ipcp_info info;
+ size_t i;
+ size_t len;
+
+ TEST_START();
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < 100; i++) {
+ sprintf(info.name, "%s-%zd", TEST_IPCP, i);
+ info.pid = TEST_PID + rand() % 10000;
+ info.type = rand() % IPCP_INVALID;
+ info.state = IPCP_INIT; /* set by spawn_ipcp */
+
+ if (reg_create_ipcp(&info) < 0) {
+ printf("Failed to create ipcp %s.\n", info.name);
+ goto fail;
+ }
+ }
+
+ len = reg_list_ipcps(&ipcps);
+ if (len != 100) {
+ printf("Failed to list all ipcps.\n");
+ goto fail;
+ }
+
+ for (i = 1; i < len; i++) {
+ if (ipcps[i]->type < ipcps[i - 1]->type) {
+ printf("IPCPS not sorted by type.\n");
+ goto fail;
+ }
+
+ if (ipcps[i]->type != ipcps[i - 1]->type)
+ continue;
+
+ /* allow occasional duplicate PID in test */
+ if (ipcps[i]->pid < ipcps[i - 1]->pid) {
+ printf("IPCPS not sorted by pid.\n");
+ goto fail;
+ }
+ }
+
+ while (len-- > 0)
+ ipcp_list_msg__free_unpacked(ipcps[len], NULL);
+
+ free(ipcps);
+
+ reg_clear();
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_set_layer(void)
@@ -564,7 +1125,7 @@ static int test_set_layer(void)
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_BOOT /* set by spawn_ipcp */
+ .state = IPCP_INIT /* set by spawn_ipcp */
};
struct layer_info layer = {
.name = TEST_LAYER,
@@ -588,8 +1149,9 @@ static int test_set_layer(void)
}
ipcp = __reg_get_ipcp(info.pid);
- ipcp->info.state = IPCP_OPERATIONAL;
- info.state = IPCP_ENROLLED;
+
+ ipcp->info.state = IPCP_BOOT;
+ info.state = IPCP_BOOT;
reg_set_layer_for_ipcp(&info, &layer);
@@ -614,21 +1176,22 @@ static int test_set_layer(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_ipcp(void)
{
- int ret = 0;
-
- ret |= test_reg_create_ipcp();
+ int rc = 0;
- ret |= test_set_layer();
+ rc |= test_reg_create_ipcp();
+ rc |= test_reg_list_ipcps();
+ rc |= test_insert_ipcps();
+ rc |= test_set_layer();
- return ret;
+ return rc;
}
static int test_reg_create_name(void)
@@ -650,7 +1213,7 @@ static int test_reg_create_name(void)
goto fail;
}
- if (reg.n_names != 1) {
+ if (reg.names.len != 1) {
printf("n_names was not updated.\n");
goto fail;
}
@@ -665,7 +1228,7 @@ static int test_reg_create_name(void)
goto fail;
}
- if (reg.n_names != 0) {
+ if (!llist_is_empty(&reg.names)) {
printf("n_names was not updated.\n");
goto fail;
}
@@ -674,27 +1237,83 @@ static int test_reg_create_name(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
+}
+
+static int test_reg_list_names(void)
+{
+ name_info_msg_t ** names;
+ int i;
+ ssize_t len;
+
+ TEST_START();
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < 10; i++) {
+ struct name_info info = {
+ .pol_lb = LB_RR
+ };
+
+ sprintf(info.name, "%s%d", TEST_NAME, i);
+
+ if (reg_create_name(&info) < 0) {
+ printf("Failed to create name %d.\n", i);
+ goto fail;
+ }
+ }
+
+ len = reg_list_names(&names);
+ if (len < 0) {
+ printf("Failed to list names.\n");
+ goto fail;
+ }
+
+ if (len != 10) {
+ printf("Failed to list all names.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < len; i++)
+ name_info_msg__free_unpacked(names[i], NULL);
+
+ free(names);
+
+ for (i = 0; i < 10; i++) {
+ char name[NAME_MAX];
+ sprintf(name, "%s%d", TEST_NAME, i);
+ reg_destroy_name(name);
+ }
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
}
static int test_reg_name(void)
{
- int ret = 0;
+ int rc = 0;
- ret |= test_reg_create_name();
+ rc |= test_reg_create_name();
+ rc |= test_reg_list_names();
- return ret;
+ return rc;
}
static int test_reg_create_proc(void)
{
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC_INFO;
TEST_START();
@@ -708,7 +1327,7 @@ static int test_reg_create_proc(void)
goto fail;
}
- if (reg.n_procs != 1) {
+ if (reg.procs.len != 1) {
printf("n_procs was not updated.\n");
goto fail;
}
@@ -723,7 +1342,7 @@ static int test_reg_create_proc(void)
goto fail;
}
- if (reg.n_procs != 0) {
+ if (!llist_is_empty(&reg.procs)) {
printf("n_procs was not updated.\n");
goto fail;
}
@@ -732,19 +1351,19 @@ static int test_reg_create_proc(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_proc(void)
{
- int ret = 0;
+ int rc = 0;
- ret |= test_reg_create_proc();
+ rc |= test_reg_create_proc();
- return ret;
+ return rc;
}
static int test_reg_spawned(void)
@@ -761,7 +1380,7 @@ static int test_reg_spawned(void)
goto fail;
}
- if (reg.n_spawned != 1) {
+ if (reg.spawned.len != 1) {
printf("n_spawned was not updated.\n");
goto fail;
}
@@ -776,7 +1395,7 @@ static int test_reg_spawned(void)
goto fail;
}
- if (reg.n_spawned != 0) {
+ if (!llist_is_empty(&reg.spawned)) {
printf("n_spawned was not updated.\n");
goto fail;
}
@@ -785,10 +1404,10 @@ static int test_reg_spawned(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_create_prog(void)
@@ -809,7 +1428,7 @@ static int test_reg_create_prog(void)
goto fail;
}
- if (reg.n_progs != 1) {
+ if (reg.progs.len != 1) {
printf("n_progs was not updated.\n");
goto fail;
}
@@ -824,7 +1443,7 @@ static int test_reg_create_prog(void)
goto fail;
}
- if (reg.n_progs != 0) {
+ if (!llist_is_empty(&reg.progs)) {
printf("n_progs was not updated.\n");
goto fail;
}
@@ -833,27 +1452,24 @@ static int test_reg_create_prog(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_reg_prog(void)
{
- int ret = 0;
+ int rc = 0;
- ret |= test_reg_create_prog();
+ rc |= test_reg_create_prog();
- return ret;
+ return rc;
}
static int test_bind_proc(void)
{
- struct proc_info pinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info pinfo = TEST_PROC_INFO;
struct name_info ninfo = {
.name = TEST_NAME,
@@ -900,10 +1516,10 @@ static int test_bind_proc(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_bind_prog(void)
@@ -989,10 +1605,10 @@ static int test_bind_prog(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_inherit_prog(void)
@@ -1006,10 +1622,7 @@ static int test_inherit_prog(void)
.name = TEST_PROG
};
- struct proc_info procinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info procinfo = TEST_PROC_INFO;
char * exec[] = { TEST_PROG, NULL};
@@ -1060,10 +1673,10 @@ static int test_inherit_prog(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_accepting_timeout(void)
@@ -1071,7 +1684,6 @@ static int test_wait_accepting_timeout(void)
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_MS(1);
int flow_id;
- uint8_t hash[64];
struct name_info ninfo = {
.name = TEST_NAME,
.pol_lb = LB_RR
@@ -1089,12 +1701,10 @@ static int test_wait_accepting_timeout(void)
goto fail;
}
- str_hash(HASH_SHA3_256, hash, ninfo.name);
-
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- flow_id = reg_wait_flow_accepting(HASH_SHA3_256, hash, &abstime);
+ flow_id = reg_wait_flow_accepting(ninfo.name, &abstime);
if (flow_id != -ETIMEDOUT) {
printf("Wait accept did not time out: %d.\n", flow_id);
goto fail;
@@ -1106,10 +1716,10 @@ static int test_wait_accepting_timeout(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_accepting_fail_name(void)
@@ -1117,7 +1727,6 @@ static int test_wait_accepting_fail_name(void)
struct timespec abstime;
struct timespec timeo = TIMESPEC_INIT_S(1);
int flow_id;
- uint8_t hash[64];
TEST_START();
@@ -1128,11 +1737,10 @@ static int test_wait_accepting_fail_name(void)
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- str_hash(HASH_SHA3_256, hash, "C0FF33");
- flow_id = reg_wait_flow_accepting(HASH_SHA3_256, hash, &abstime);
+ flow_id = reg_wait_flow_accepting(TEST_NAME, &abstime);
if (flow_id != -ENAME) {
- printf("Wait accept did not fail on name: %d.\n", flow_id);
+ printf("Wait accept did not fail: %d.\n", flow_id);
goto fail;
}
@@ -1140,22 +1748,19 @@ static int test_wait_accepting_fail_name(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static void * test_call_flow_accept(void * o)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_MS(1);
- buffer_t pbuf = {NULL, 0};
+ struct timespec timeo = TIMESPEC_INIT_MS(30);
+ buffer_t pbuf = BUF_INIT;
- struct proc_info pinfo = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info pinfo = TEST_PROC_INFO;
struct flow_info info = {
.n_pid = pinfo.pid,
@@ -1179,16 +1784,21 @@ static void * test_call_flow_accept(void * o)
info.state = FLOW_ACCEPT_PENDING;
+ reg_prepare_flow_accept(&info);
+
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- reg_prepare_flow_accept(&info, &pbuf);
-
if (reg_wait_flow_accepted(&info, &pbuf, &abstime) != -ETIMEDOUT) {
printf("Wait allocated did not timeout.\n");
goto fail;
}
+ if (reg_unbind_proc((char *) o, pinfo.pid) < 0) {
+ printf("Failed to unbind proc.\n");
+ goto fail;
+ }
+
reg_destroy_flow(info.id);
reg_destroy_proc(pinfo.pid);
@@ -1200,15 +1810,15 @@ static void * test_call_flow_accept(void * o)
static int test_wait_accepting_success(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
- int flow_id;
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
- uint8_t hash[64];
+ int flow_id;
struct name_info ninfo = {
.name = TEST_NAME,
.pol_lb = LB_RR
};
+
TEST_START();
if (reg_init()) {
@@ -1226,11 +1836,12 @@ static int test_wait_accepting_success(void)
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- str_hash(HASH_SHA3_256, hash, ninfo.name);
-
- flow_id = reg_wait_flow_accepting(HASH_SHA3_256, hash, &abstime);
+ flow_id = reg_wait_flow_accepting(ninfo.name, &abstime);
if (flow_id < 0) {
- printf("Wait accept did not return a flow id: %d.", flow_id);
+ printf("Wait accept did not return a flow id: %d.\n", flow_id);
+ pthread_join(thr, NULL);
+ reg_destroy_name(TEST_NAME);
+ reg_fini();
goto fail;
}
@@ -1242,23 +1853,21 @@ static int test_wait_accepting_success(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_accepting(void)
{
- int ret = 0;
-
- ret |= test_wait_accepting_timeout();
+ int rc = 0;
- ret |= test_wait_accepting_fail_name();
+ rc |= test_wait_accepting_timeout();
+ rc |= test_wait_accepting_fail_name();
+ rc |= test_wait_accepting_success();
- ret |= test_wait_accepting_success();
-
- return ret;
+ return rc;
}
static int test_wait_ipcp_boot_timeout(void)
@@ -1268,7 +1877,7 @@ static int test_wait_ipcp_boot_timeout(void)
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_BOOT /* set by spawn_ipcp */
+ .state = IPCP_INIT /* set by spawn_ipcp */
};
TEST_START();
@@ -1300,10 +1909,10 @@ static int test_wait_ipcp_boot_timeout(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static void * test_ipcp_respond(void * o)
@@ -1318,12 +1927,12 @@ static void * test_ipcp_respond(void * o)
static int test_wait_ipcp_boot_fail(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_BOOT /* set by spawn_ipcp */
+ .state = IPCP_INIT /* set by spawn_ipcp */
};
struct ipcp_info resp_info = {
.name = TEST_IPCP,
@@ -1348,10 +1957,13 @@ static int test_wait_ipcp_boot_fail(void)
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- info.state = IPCP_BOOT;
+ info.state = IPCP_INIT;
if (reg_wait_ipcp_boot(&info, &abstime) == 0) {
printf("IPCP boot reported success.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1362,8 +1974,8 @@ static int test_wait_ipcp_boot_fail(void)
goto fail;
}
- if (reg.n_ipcps != 0) {
- printf("n_ipcps was not updated.\n");
+ if (!llist_is_empty(&reg.ipcps)) {
+ printf("ipcps.len was not updated.\n");
goto fail;
}
@@ -1371,21 +1983,21 @@ static int test_wait_ipcp_boot_fail(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_ipcp_boot_success(void)
{
pthread_t thr;
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
struct ipcp_info info = {
.name = TEST_IPCP,
.pid = TEST_PID,
- .state = IPCP_BOOT /* set by spawn_ipcp */
+ .state = IPCP_INIT /* set by spawn_ipcp */
};
struct ipcp_info resp_info = {
.name = TEST_IPCP,
@@ -1410,10 +2022,13 @@ static int test_wait_ipcp_boot_success(void)
clock_gettime(PTHREAD_COND_CLOCK, &abstime);
ts_add(&abstime, &timeo, &abstime);
- info.state = IPCP_BOOT;
+ info.state = IPCP_INIT;
if (reg_wait_ipcp_boot(&info, &abstime) < 0) {
printf("IPCP boot failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1421,6 +2036,8 @@ static int test_wait_ipcp_boot_success(void)
if (info.state != IPCP_OPERATIONAL) {
printf("IPCP boot succeeded in non-operational state.\n");
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1433,23 +2050,21 @@ static int test_wait_ipcp_boot_success(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_ipcp_boot(void)
{
- int ret = 0;
-
- ret |= test_wait_ipcp_boot_timeout();
+ int rc = 0;
- ret |= test_wait_ipcp_boot_fail();
+ rc |= test_wait_ipcp_boot_timeout();
+ rc |= test_wait_ipcp_boot_fail();
+ rc |= test_wait_ipcp_boot_success();
- ret |= test_wait_ipcp_boot_success();
-
- return ret;
+ return rc;
}
static int test_wait_proc_timeout(void)
@@ -1477,10 +2092,10 @@ static int test_wait_proc_timeout(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static void * test_proc(void * o)
@@ -1495,12 +2110,9 @@ static void * test_proc(void * o)
static int test_wait_proc_success(void)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_S(1);
+ struct timespec timeo = TIMESPEC_INIT_S(10);
pthread_t thr;
- struct proc_info info = {
- .pid = TEST_PID,
- .prog = TEST_PROG
- };
+ struct proc_info info = TEST_PROC_INFO;
TEST_START();
@@ -1516,6 +2128,9 @@ static int test_wait_proc_success(void)
if (reg_wait_proc(info.pid, &abstime) < 0) {
printf("Waiting for proc failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_proc(info.pid);
+ reg_fini();
goto fail;
}
@@ -1527,57 +2142,43 @@ static int test_wait_proc_success(void)
TEST_SUCCESS();
- return 0;
+ return TEST_RC_SUCCESS;
fail:
REG_TEST_FAIL();
- return -1;
+ return TEST_RC_FAIL;
}
static int test_wait_proc(void)
{
- int ret = 0;
-
- ret |= test_wait_proc_timeout();
+ int rc = 0;
- ret |= test_wait_proc_success();
+ rc |= test_wait_proc_timeout();
+ rc |= test_wait_proc_success();
- return ret;
+ return rc;
}
-
int reg_test(int argc,
char ** argv)
{
- int ret = 0;
+ int rc = 0;
(void) argc;
(void) argv;
- ret |= test_reg_init();
-
- ret |= test_reg_flow();
-
- ret |= test_reg_ipcp();
-
- ret |= test_reg_name();
-
- ret |= test_reg_proc();
-
- ret |= test_reg_prog();
-
- ret |= test_reg_spawned();
-
- ret |= test_bind_proc();
-
- ret |= test_bind_prog();
-
- ret |= test_inherit_prog();
-
- ret |= test_wait_accepting();
-
- ret |= test_wait_ipcp_boot();
-
- ret |= test_wait_proc();
-
- return ret;
+ rc |= test_reg_init();
+ rc |= test_reg_flow();
+ rc |= test_reg_ipcp();
+ rc |= test_reg_name();
+ rc |= test_reg_proc();
+ rc |= test_reg_prog();
+ rc |= test_reg_spawned();
+ rc |= test_bind_proc();
+ rc |= test_bind_prog();
+ rc |= test_inherit_prog();
+ rc |= test_wait_accepting();
+ rc |= test_wait_ipcp_boot();
+ rc |= test_wait_proc();
+
+ return rc;
}
diff --git a/src/irmd/tests/CMakeLists.txt b/src/irmd/tests/CMakeLists.txt
deleted file mode 100644
index e005d194..00000000
--- a/src/irmd/tests/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-get_filename_component(tmp ".." ABSOLUTE)
-get_filename_component(src_folder "${tmp}" NAME)
-
-create_test_sourcelist(${src_folder}_tests test_suite.c
- # Add new tests here
-)
-
-add_executable(${src_folder}_test EXCLUDE_FROM_ALL ${${src_folder}_tests})
-target_link_libraries(${src_folder}_test ouroboros-common)
-
-add_dependencies(check ${src_folder}_test)
-
-set(tests_to_run ${${src_folder}_tests})
-remove(tests_to_run test_suite.c)
-
-foreach(test ${tests_to_run})
- get_filename_component(test_name ${test} NAME_WE)
- add_test(irmd/${test_name} ${C_TEST_PATH}/${src_folder}_test ${test_name})
-endforeach(test)