diff options
Diffstat (limited to 'src/irmd')
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 "[38;5;4m" +#define _G "[38;5;8m" +#define RST "[0m" -#define O7S_ASCII_ART \ -"[0m\n" \ -" [38;5;4m▄▄█████▄▄▄[38;5;7m\n" \ -" [38;5;4m▄█▀▀[38;5;7m [38;5;4m▀▀███▄[38;5;7m " \ -"[38;5;8m█[38;5;7m\n" \ -" [38;5;4m██[38;5;7m [38;5;4m▄▄▄[38;5;7m [38;5;4m▄███▄[38;5;7m " \ -"[38;5;8m▄[38;5;7m [38;5;8m▄[38;5;7m [38;5;8m▄[38;5;7m [38;5;8m▄▄" \ -"[38;5;7m [38;5;8m▄▄[38;5;7m [38;5;8m█[38;5;7m [38;5;8m▄▄[38;5;7m " \ -" [38;5;8m▄▄[38;5;7m [38;5;8m▄[38;5;7m [38;5;8m▄▄[38;5;7m [38;5;8m" \ -"▄▄[38;5;7m [38;5;8m▄▄[38;5;7m\n" \ -" [38;5;4m██[38;5;7m [38;5;4m█[38;5;7m [38;5;4m█[38;5;7m " \ -"[38;5;4m█████[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█[38;5;7m " \ -"[38;5;8m█▀[38;5;7m [38;5;8m▀[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█" \ -"[38;5;7m [38;5;8m█▀[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█[38;5;7m " \ -"[38;5;8m█[38;5;7m [38;5;8m█▀[38;5;7m [38;5;8m▀[38;5;7m [38;5;8m█" \ -"[38;5;7m [38;5;8m█[38;5;7m [38;5;8m▀▄[38;5;7m [38;5;8m▀[38;5;7m\n" \ -" [38;5;4m██[38;5;7m [38;5;4m▀▄▄▄▀[38;5;7m [38;5;4m▀█▀[38;5;7m " \ -"[38;5;8m█[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█[38;5;7m [38;5;8m" \ -"█[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█▄[38;5;7m [38;5;8m█[38;5;7m " \ -"[38;5;8m█[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█[38;5;7m [38;5;8m█" \ -"[38;5;7m [38;5;8m█[38;5;7m [38;5;8m▄[38;5;7m [38;5;8m▀▄[38;5;7m\n" \ -" [38;5;4m█▄[38;5;7m [38;5;4m█[38;5;7m [38;5;8m▀▀▀" \ -"[38;5;7m [38;5;8m▀[38;5;7m [38;5;8m▀[38;5;7m [38;5;8m▀▀[38;5;7m" \ -" [38;5;8m▀[38;5;7m [38;5;8m▀▀[38;5;7m [38;5;8m▀▀[38;5;7m " \ -"[38;5;8m▀[38;5;7m [38;5;8m▀▀[38;5;7m [38;5;8m▀▀[38;5;7m\n" \ -" [38;5;4m▀█▄▄▄▄▄▄▄▄▀[38;5;7m\n" \ -" [38;5;4m▀▀▀▀▀▀[38;5;7m\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, ®.flows) { + llist_for_each(p, ®.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, ®.flows) { + llist_for_each(p, ®.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, ®.flows) { + llist_for_each(p, ®.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, ®.ipcps) { + llist_for_each(p, ®.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, ®.ipcps) { + llist_for_each(p, ®.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, ®.ipcps) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.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, ®.procs) { + llist_for_each(p, ®.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, ®.procs) { + llist_for_each(p, ®.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, ®.procs) { + llist_for_each(p, ®.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, ®.procs) { + llist_for_each(p, ®.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, ®.flows) { + llist_for_each(p, ®.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, ®.spawned) { + llist_for_each(p, ®.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, ®.spawned) { + llist_for_each(p, ®.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, ®.spawned) { + llist_for_each(p, ®.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(®.spawned)) + if (llist_is_empty(®.spawned)) return -1; - return list_first_entry(®.spawned, struct pid_entry, next)->pid; + return llist_first_entry(®.spawned, struct pid_entry, next)->pid; } static struct reg_prog * __reg_get_prog(const char * name) { struct list_head * p; - list_for_each(p, ®.progs) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.progs) { + llist_for_each(p, ®.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, ®.procs) { + llist_for_each(p, ®.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, ®.progs) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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, ®.names) { + llist_for_each(p, ®.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(®.flows); - list_head_init(®.ipcps); - list_head_init(®.names); - list_head_init(®.procs); - list_head_init(®.progs); - list_head_init(®.spawned); + llist_init(®.flows); + llist_init(®.ipcps); + llist_init(®.names); + llist_init(®.pools); + llist_init(®.procs); + llist_init(®.progs); + llist_init(®.spawned); return 0; @@ -588,54 +570,56 @@ void reg_clear(void) pthread_mutex_lock(®.mtx); - list_for_each_safe(p, h, ®.spawned) { + llist_for_each_safe(p, h, ®.spawned) { struct pid_entry * entry; entry = list_entry(p, struct pid_entry, next); - list_del(&entry->next); + llist_del(&entry->next, ®.spawned); free(entry); - reg.n_spawned--; } - list_for_each_safe(p, h, ®.progs) { + llist_for_each_safe(p, h, ®.progs) { struct reg_prog * entry; entry = list_entry(p, struct reg_prog, next); - list_del(&entry->next); + llist_del(&entry->next, ®.progs); __reg_del_prog_from_names(entry->info.path); reg_prog_destroy(entry); - reg.n_progs--; } - list_for_each_safe(p, h, ®.procs) { + llist_for_each_safe(p, h, ®.procs) { struct reg_proc * entry; entry = list_entry(p, struct reg_proc, next); - list_del(&entry->next); + llist_del(&entry->next, ®.procs); __reg_del_proc_from_names(entry->info.pid); reg_proc_destroy(entry); - reg.n_procs--; } - list_for_each_safe(p, h, ®.names) { + llist_for_each_safe(p, h, ®.pools) { + struct reg_pool * entry; + entry = list_entry(p, struct reg_pool, next); + llist_del(&entry->next, ®.pools); + entry->refcount = 0; /* Force destroy during cleanup */ + reg_pool_destroy(entry); + } + + llist_for_each_safe(p, h, ®.flows) { + struct reg_flow * entry; + entry = list_entry(p, struct reg_flow, next); + llist_del(&entry->next, ®.flows); + reg_flow_destroy(entry); + } + + llist_for_each_safe(p, h, ®.names) { struct reg_name * entry; entry = list_entry(p, struct reg_name, next); - list_del(&entry->next); + llist_del(&entry->next, ®.names); reg_name_destroy(entry); - reg.n_names--; } - list_for_each_safe(p, h, ®.ipcps) { + llist_for_each_safe(p, h, ®.ipcps) { struct reg_ipcp * entry; entry = list_entry(p, struct reg_ipcp, next); - list_del(&entry->next); + llist_del(&entry->next, ®.ipcps); reg_ipcp_destroy(entry); - reg.n_ipcps--; - } - - list_for_each_safe(p, h, ®.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(®.mtx); @@ -643,21 +627,15 @@ void reg_clear(void) void reg_fini(void) { - assert(list_is_empty(®.spawned)); - assert(list_is_empty(®.progs)); - assert(list_is_empty(®.procs)); - assert(list_is_empty(®.names)); - assert(list_is_empty(®.ipcps)); - assert(list_is_empty(®.flows)); + assert(llist_is_empty(®.spawned)); + assert(llist_is_empty(®.progs)); + assert(llist_is_empty(®.procs)); + assert(llist_is_empty(®.pools)); + assert(llist_is_empty(®.names)); + assert(llist_is_empty(®.ipcps)); + assert(llist_is_empty(®.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(®.cond) != 0) log_warn("Failed to destroy condvar."); @@ -677,8 +655,8 @@ int reg_create_flow(struct flow_info * info) pthread_mutex_lock(®.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), ®.flows); pthread_mutex_unlock(®.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(®.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, ®.flows); - bmp_release(reg.flow_ids, flow_id); + bmp_release(reg.ids, flow_id); pthread_mutex_unlock(®.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(®.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), ®.ipcps); + llist_add_at(&entry->next, + __reg_after_spawned(info->pid), + ®.spawned); pthread_mutex_unlock(®.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(®.mtx); - if (reg.n_ipcps == 0) { - *ipcps = NULL; + if (llist_is_empty(®.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, ®.ipcps) { + llist_for_each(p, ®.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(®.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), ®.names); pthread_mutex_unlock(®.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, ®.names); pthread_mutex_unlock(®.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(®.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(®.mtx); return 0; - fail_name: - free(*msg); - *msg = NULL; - fail: - return -1; + no_name: + pthread_mutex_unlock(®.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(®.mtx); + + llist_for_each(p, ®.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(®.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(®.mtx); + + f = __reg_get_flow(flow_id); + if (f != NULL) + strcpy(buf, f->name); + + pthread_mutex_unlock(®.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(®.mtx); + + f = __reg_get_flow(flow_id); + if (f != NULL) + strcpy(f->name, name); + + pthread_mutex_unlock(®.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(®.mtx); - if (reg.n_names == 0) + if (llist_is_empty(®.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, ®.names) { + llist_for_each(p, ®.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(®.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(®.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(®.mtx); + return -1; + } + llist_add(&pool->next, ®.pools); + } + + reg_pool_ref(pool); + + pthread_mutex_unlock(®.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), ®.procs); pthread_cond_broadcast(®.cond); @@ -1092,7 +1157,7 @@ int reg_create_proc(const struct proc_info * info) return 0; - fail_proc: + fail: pthread_mutex_unlock(®.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, ®.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, ®.pools); + reg_pool_destroy(pool); + } } spawn = __reg_get_spawned(pid); if (spawn != NULL) { - list_del(&spawn->next); - reg.n_spawned--; + llist_del(&spawn->next, ®.spawned); free(spawn); } ipcp = __reg_get_ipcp(pid); if (ipcp != NULL) { - list_del(&ipcp->next); - reg.n_ipcps--; + llist_del(&ipcp->next, ®.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(®.mtx); + + proc = __reg_get_proc(pid); + if (proc != NULL) + ret = reg_proc_is_privileged(proc); + + pthread_mutex_unlock(®.mtx); + + return ret; +} + +uid_t reg_get_proc_uid(pid_t pid) +{ + struct reg_proc * proc; + uid_t ret = 0; + + pthread_mutex_lock(®.mtx); + + proc = __reg_get_proc(pid); + if (proc != NULL && !is_ouroboros_member_uid(proc->info.uid)) + ret = proc->info.uid; + + pthread_mutex_unlock(®.mtx); + + return ret; +} + void reg_kill_all_proc(int signal) { pthread_mutex_lock(®.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), ®.spawned); pthread_mutex_unlock(®.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), ®.progs); exists: pthread_mutex_unlock(®.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, ®.progs); pthread_mutex_unlock(®.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(®.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(®.mtx); - - return 0; - finish: pthread_mutex_unlock(®.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(®.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(®.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(®.cond); pthread_mutex_unlock(®.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(®.mtx); @@ -1897,7 +1994,7 @@ int reg_wait_flow_accepting(enum hash_algo algo, pthread_cleanup_push(__cleanup_mutex_unlock, ®.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(®.cond); + + pthread_mutex_unlock(®.mtx); + + return 0; + + fail_flow: + pthread_mutex_unlock(®.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(®.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(®.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(®.mtx); + + flow = __reg_get_flow(flow_id); + + ret = flow != NULL && flow->direct; + + pthread_mutex_unlock(®.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(®.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(®.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(®.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(®.mtx); + + return ret; +} + +int reg_flow_get_epoch(int flow_id) +{ + struct reg_flow * flow; + int epoch = -1; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL && flow->rk.encrypted) + epoch = flow->rk.epoch; + + pthread_mutex_unlock(®.mtx); + + return epoch; +} + +bool reg_flow_rekey_pending(int flow_id) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = flow->rk.has_pending; + + pthread_mutex_unlock(®.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(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + pid = flow->info.n_1_pid; + + pthread_mutex_unlock(®.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(®.mtx); + + llist_for_each(p, ®.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(®.mtx); + + return n; +} + +void reg_flow_clear_in_flight(int flow_id) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + flow->rk.in_flight = false; + + pthread_mutex_unlock(®.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(®.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(®.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(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = flow->rk.initiator && flow->rk.in_flight; + + pthread_mutex_unlock(®.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(®.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(®.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(®.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(®.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(®.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(®.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(®.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(®.mtx); + + return admit; +} + +void reg_flow_rekey_arr_done(int flow_id, + bool is_req) +{ + struct reg_flow * flow; + + pthread_mutex_lock(®.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(®.mtx); +} + +bool reg_flow_owned_by(int flow_id, + uid_t uid) +{ + struct reg_flow * flow; + bool ret = false; + + pthread_mutex_lock(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + ret = uid_may_access(uid, flow->info.uid); + + pthread_mutex_unlock(®.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(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow != NULL) + __notify_proc(flow->info.n_pid, flow_id, event); + + pthread_mutex_unlock(®.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(®.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(®.mtx); +} + +int reg_respond_flow_direct(int flow_id, + buffer_t * pbuf) +{ + struct reg_flow * flow; + + assert(pbuf != NULL); + + pthread_mutex_lock(®.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(®.cond); + + pthread_mutex_unlock(®.mtx); + + return 0; + fail: + pthread_mutex_unlock(®.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(®.mtx); + + flow = __reg_get_flow(flow_id); + if (flow == NULL) + goto fail; + + assert(flow->direct); + + pthread_cleanup_push(__cleanup_mutex_unlock, ®.mtx); + + while (flow != NULL && flow->rsp_data.data == NULL) { + ret = -__timedwait(®.cond, ®.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(®.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(®.mtx); + return; + } + flow->info.state = FLOW_DEALLOCATED; + info->state = FLOW_DEALLOCATED; + reg_flow_update(flow, info); + pthread_mutex_unlock(®.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(®.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(®.cond, ®.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(®, 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(®.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(®.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(®.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(®.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(®.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(®.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(®.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) |
