diff options
Diffstat (limited to 'src/lib/tests')
| -rw-r--r-- | src/lib/tests/CMakeLists.txt | 33 | ||||
| -rw-r--r-- | src/lib/tests/auth_test.c | 743 | ||||
| -rw-r--r-- | src/lib/tests/auth_test_ml_dsa.c | 356 | ||||
| -rw-r--r-- | src/lib/tests/auth_test_slh_dsa.c | 367 | ||||
| -rw-r--r-- | src/lib/tests/bitmap_test.c | 27 | ||||
| -rw-r--r-- | src/lib/tests/btree_test.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/crc32_test.c | 72 | ||||
| -rw-r--r-- | src/lib/tests/crypt_test.c | 555 | ||||
| -rw-r--r-- | src/lib/tests/hash_test.c | 349 | ||||
| -rw-r--r-- | src/lib/tests/kex_test.c | 1194 | ||||
| -rw-r--r-- | src/lib/tests/kex_test_ml_kem.c | 549 | ||||
| -rw-r--r-- | src/lib/tests/keyrot_test.c | 1083 | ||||
| -rw-r--r-- | src/lib/tests/md5_test.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/sha3_test.c | 2 | ||||
| -rw-r--r-- | src/lib/tests/shm_rbuff_test.c | 113 | ||||
| -rw-r--r-- | src/lib/tests/sockets_test.c | 102 | ||||
| -rw-r--r-- | src/lib/tests/time_test.c | 529 | ||||
| -rw-r--r-- | src/lib/tests/time_utils_test.c | 164 | ||||
| -rw-r--r-- | src/lib/tests/tpm_test.c | 104 | ||||
| -rw-r--r-- | src/lib/tests/tw_test.c | 663 |
20 files changed, 6632 insertions, 377 deletions
diff --git a/src/lib/tests/CMakeLists.txt b/src/lib/tests/CMakeLists.txt index 9e23b0ee..002d94af 100644 --- a/src/lib/tests/CMakeLists.txt +++ b/src/lib/tests/CMakeLists.txt @@ -1,27 +1,36 @@ get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) get_filename_component(PARENT_DIR ${PARENT_PATH} NAME) +compute_test_prefix() + create_test_sourcelist(${PARENT_DIR}_tests test_suite.c # Add new tests here + auth_test.c + auth_test_ml_dsa.c + auth_test_slh_dsa.c bitmap_test.c btree_test.c - crc32_test.c + crypt_test.c + hash_test.c + kex_test.c + kex_test_ml_kem.c + keyrot_test.c md5_test.c sha3_test.c - shm_rbuff_test.c - time_utils_test.c + sockets_test.c + time_test.c + tpm_test.c + tw_test.c ) -add_executable(${PARENT_DIR}_test EXCLUDE_FROM_ALL ${${PARENT_DIR}_tests}) +add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests}) -target_link_libraries(${PARENT_DIR}_test ouroboros-common) +target_include_directories(${PARENT_DIR}_test PRIVATE + ${CMAKE_SOURCE_DIR}/src/lib) -add_dependencies(check ${PARENT_DIR}_test) +disable_test_logging_for_target(${PARENT_DIR}_test) +target_link_libraries(${PARENT_DIR}_test ouroboros-common) -set(tests_to_run ${${PARENT_DIR}_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(${test_name} ${C_TEST_PATH}/${PARENT_DIR}_test ${test_name}) -endforeach (test) +ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests}) diff --git a/src/lib/tests/auth_test.c b/src/lib/tests/auth_test.c new file mode 100644 index 00000000..af7cf81c --- /dev/null +++ b/src/lib/tests/auth_test.c @@ -0,0 +1,743 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the 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/. + */ + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/crypt.h> +#include <ouroboros/name.h> +#include <ouroboros/random.h> +#include <ouroboros/utils.h> + +#include <test/certs/ecdsa.h> + +#include <string.h> + +#define TEST_MSG_SIZE 1500 + +static int test_auth_create_destroy_ctx(void) +{ + struct auth_ctx * ctx; + + TEST_START(); + + ctx = auth_create_ctx(); + if (ctx == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create; + } + + auth_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_create: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_crt(void) +{ + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(root_ca_crt_ec, &crt) < 0) { + printf("Failed to load certificate string.\n"); + goto fail_load; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_crypt_get_pubkey_crt(void) +{ + void * pk; + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) { + printf("Failed to load server certificate from string.\n"); + goto fail_load; + } + + if (crypt_get_pubkey_crt(crt, &pk) < 0) { + printf("Failed to get public key from certificate.\n"); + goto fail_get_pubkey; + } + + crypt_free_key(pk); + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_get_pubkey: + crypt_free_crt(crt); + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_check_crt_name(void) +{ + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) { + printf("Failed to load certificate from string.\n"); + goto fail_load; + } + + if (crypt_check_crt_name(crt, "test-1.unittest.o7s") < 0) { + printf("Failed to verify correct name.\n"); + goto fail_check; + } + + if (crypt_check_crt_name(crt, "bogus.name") == 0) { + printf("Failed to detect incorrect name.\n"); + goto fail_check; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_check: + crypt_free_crt(crt); + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_crt_name_confusion(void) +{ + char name[NAME_SIZE + 1]; + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(confused_crt_ec, &crt) < 0) { + printf("Failed to load name-confusion certificate.\n"); + goto fail_load; + } + + /* Must extract the real CN, not the "CN=" decoy in the O field. */ + if (crypt_get_crt_name(crt, name) < 0) { + printf("Failed to extract name from certificate.\n"); + goto fail_check; + } + + if (strcmp(name, "attacker.unittest.o7s") != 0) { + printf("Extracted '%s', expected real CN.\n", name); + goto fail_check; + } + + /* The decoy name in the O field must never authenticate. */ + if (crypt_check_crt_name(crt, "victim.unittest.o7s") == 0) { + printf("Accepted spoofed name from O field.\n"); + goto fail_check; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_check: + crypt_free_crt(crt); + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_privkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_privkey_str(server_pkp_ec, &key) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_pubkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_pubkey_str(server_pk_ec, &key) < 0) { + printf("Failed to load server public key from string.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_crypt_check_pubkey_crt(void) +{ + void * pk; + void * crt_pk; + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) { + printf("Failed to load public certificate from string.\n"); + goto fail_crt; + } + + if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) { + printf("Failed to load public key from string.\n"); + goto fail_pubkey; + } + + if (crypt_get_pubkey_crt(crt, &crt_pk) < 0) { + printf("Failed to get public key from certificate.\n"); + goto fail_get_pubkey; + } + + if (crypt_cmp_key(pk, crt_pk) != 0) { + printf("Public keys do not match .\n"); + goto fail_check; + } + + + crypt_free_key(crt_pk); + crypt_free_key(pk); + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_check: + crypt_free_key(crt_pk); + fail_get_pubkey: + crypt_free_key(pk); + fail_pubkey: + crypt_free_crt(crt); + fail_crt: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_store_add(void) +{ + struct auth_ctx * ctx; + void * _root_ca_crt; + + TEST_START(); + + ctx = auth_create_ctx(); + if (ctx == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create; + } + + if (crypt_load_crt_str(root_ca_crt_ec, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load; + } + + if (auth_add_crt_to_store(ctx, _root_ca_crt) < 0) { + printf("Failed to add root crt to auth store.\n"); + goto fail_add; + } + + crypt_free_crt(_root_ca_crt); + auth_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_add: + crypt_free_crt(_root_ca_crt); + fail_load: + crypt_free_crt(_root_ca_crt); + fail_create: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_verify_crt(void) +{ + struct auth_ctx * auth; + void * _server_crt; + void * _signed_server_crt; + void * _root_ca_crt; + void * _im_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(server_crt_ec, &_server_crt) < 0) { + printf("Failed to load self-signed crt from string.\n"); + goto fail_load_server_crt; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed_server_crt; + } + + if (crypt_load_crt_str(root_ca_crt_ec, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load_root_ca_crt; + } + + if (crypt_load_crt_str(im_ca_crt_ec, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca_crt; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _signed_server_crt) < 0) { + printf("Failed to verify signed crt with ca crt.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _server_crt) == 0) { + printf("Failed to detect untrusted crt.\n"); + goto fail_verify; + } + + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_server_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca_crt: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca_crt: + crypt_free_crt(_signed_server_crt); + fail_load_signed_server_crt: + crypt_free_crt(_server_crt); + fail_load_server_crt: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_verify_crt_missing_root_ca(void) +{ + struct auth_ctx * auth; + void * _signed_server_crt; + void * _im_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed; + } + + if (crypt_load_crt_str(im_ca_crt_ec, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca; + } + + /* Add only the intermediate CA - root CA is missing */ + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_add; + } + + if (auth_verify_crt(auth, _signed_server_crt) == 0) { + printf("Verification should fail without root CA.\n"); + goto fail_add; + } + + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_signed_server_crt); + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_add: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca: + crypt_free_crt(_signed_server_crt); + fail_load_signed: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* auth_verify_crt_pin: pin must lie in the verified chain (NULL: any) */ +static int test_verify_crt_pin(void) +{ + struct auth_ctx * auth; + void * _root_ca_crt; + void * _im_ca_crt; + void * _signed_server_crt; + void * _other_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(root_ca_crt_ec, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load_root_ca; + } + + if (crypt_load_crt_str(im_ca_crt_ec, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed; + } + + if (crypt_load_crt_str(other_ca_crt_ec, &_other_ca_crt) < 0) { + printf("Failed to load out-of-chain crt from string.\n"); + goto fail_load_other; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _im_ca_crt) < 0) { + printf("Failed to accept pin on intermediate CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _root_ca_crt) < 0) { + printf("Failed to accept pin on root CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _other_ca_crt) == 0) { + printf("Failed to reject out-of-chain pin.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, NULL) < 0) { + printf("Failed to accept NULL (any) pin.\n"); + goto fail_verify; + } + + crypt_free_crt(_other_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_other_ca_crt); + fail_load_other: + crypt_free_crt(_signed_server_crt); + fail_load_signed: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int test_auth_sign(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to generate random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_ec, &pkp) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) { + printf("Failed to load public key.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + if (auth_verify_sig(pk, 0, msg, sig) < 0) { + printf("Failed to verify signature.\n"); + goto fail_verify; + } + + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + return TEST_RC_FAIL; +} + +int test_auth_bad_signature(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + buffer_t fake_sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to generate random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_ec, &pkp) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_ec, &pk) < 0) { + printf("Failed to load public key.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + fake_sig.data = malloc(sig.len); + if (fake_sig.data == NULL) { + printf("Failed to allocate memory for fake signature.\n"); + goto fail_malloc; + } + + fake_sig.len = sig.len; + if (random_buffer(fake_sig.data, fake_sig.len) < 0) { + printf("Failed to generate random fake signature.\n"); + goto fail_malloc; + } + + if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) { + printf("Failed to detect bad signature.\n"); + goto fail_verify; + } + + freebuf(fake_sig); + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(fake_sig); + fail_malloc: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + return TEST_RC_FAIL; +} + +#define SSC_BUF_SIZE 4096 /* OpenSSL version my return different lengths */ +int test_crt_str(void) +{ + char str[SSC_BUF_SIZE]; + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) { + printf("Failed to load certificate from string.\n"); + goto fail_load; + } + + if (crypt_crt_str(crt, str) < 0) { + printf("Failed to convert certificate to string.\n"); + goto fail_to_str; + } + + printf("Certificate string:\n%s\n", str); + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + + fail_to_str: + crypt_free_crt(crt); + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int auth_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_auth_create_destroy_ctx(); +#ifdef HAVE_OPENSSL + ret |= test_load_free_crt(); + ret |= test_check_crt_name(); + ret |= test_crt_name_confusion(); + ret |= test_crypt_get_pubkey_crt(); + ret |= test_load_free_privkey(); + ret |= test_load_free_pubkey(); + ret |= test_crypt_check_pubkey_crt(); + ret |= test_store_add(); + ret |= test_verify_crt(); + ret |= test_verify_crt_missing_root_ca(); + ret |= test_verify_crt_pin(); + ret |= test_auth_sign(); + ret |= test_auth_bad_signature(); + ret |= test_crt_str(); +#else + (void) test_load_free_crt; + (void) test_check_crt_name; + (void) test_crt_name_confusion; + (void) test_crypt_get_pubkey_crt; + (void) test_load_free_privkey; + (void) test_load_free_pubkey; + (void) test_crypt_check_pubkey_crt; + (void) test_store_add; + (void) test_verify_crt; + (void) test_verify_crt_missing_root_ca; + (void) test_verify_crt_pin; + (void) test_auth_sign; + (void) test_auth_bad_signature; + (void) test_crt_str; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/auth_test_ml_dsa.c b/src/lib/tests/auth_test_ml_dsa.c new file mode 100644 index 00000000..cc72e61b --- /dev/null +++ b/src/lib/tests/auth_test_ml_dsa.c @@ -0,0 +1,356 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the ML-DSA-65 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/. + */ + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/crypt.h> +#include <ouroboros/random.h> +#include <ouroboros/utils.h> + +#include <test/certs/ml_dsa.h> + +#define TEST_MSG_SIZE 1500 + +static int test_auth_create_destroy_ctx(void) +{ + struct auth_ctx * ctx; + + TEST_START(); + + ctx = auth_create_ctx(); + if (ctx == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create; + } + + auth_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_create: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_crt(void) +{ + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(root_ca_crt_ml, &crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_privkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_privkey_str(server_pkp_ml, &key) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_pubkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_pubkey_str(server_pk_ml, &key) < 0) { + printf("Failed to load server public key from string.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_verify_crt(void) +{ + struct auth_ctx * auth; + void * _server_crt; + void * _signed_server_crt; + void * _root_ca_crt; + void * _im_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(server_crt_ml, &_server_crt) < 0) { + printf("Failed to load self-signed crt from string.\n"); + goto fail_load_server_crt; + } + + if (crypt_load_crt_str(signed_server_crt_ml, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed_server_crt; + } + + if (crypt_load_crt_str(root_ca_crt_ml, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load_root_ca_crt; + } + + if (crypt_load_crt_str(im_ca_crt_ml, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca_crt; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _signed_server_crt) < 0) { + printf("Failed to verify signed crt with ca crt.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _server_crt) == 0) { + printf("Failed to detect untrusted crt.\n"); + goto fail_verify; + } + + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_server_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca_crt: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca_crt: + crypt_free_crt(_signed_server_crt); + fail_load_signed_server_crt: + crypt_free_crt(_server_crt); + fail_load_server_crt: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_auth_sign(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to generate random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_ml, &pkp) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_ml, &pk) < 0) { + printf("Failed to load public key from string.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + if (auth_verify_sig(pk, 0, msg, sig) < 0) { + printf("Failed to verify signature.\n"); + goto fail_verify; + } + + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + return TEST_RC_FAIL; +} + +static int test_auth_bad_signature(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + buffer_t fake_sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to generate random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_ml, &pkp) < 0) { + printf("Failed to load server key pair from string.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_ml, &pk) < 0) { + printf("Failed to load public key from string.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + fake_sig.data = malloc(sig.len); + if (fake_sig.data == NULL) { + printf("Failed to allocate memory for fake signature.\n"); + goto fail_malloc; + } + + fake_sig.len = sig.len; + if (random_buffer(fake_sig.data, fake_sig.len) < 0) { + printf("Failed to generate random fake signature.\n"); + goto fail_malloc; + } + + if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) { + printf("Failed to detect bad ML-DSA-65 signature.\n"); + goto fail_verify; + } + + freebuf(fake_sig); + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(fake_sig); + fail_malloc: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + return TEST_RC_FAIL; +} + +int auth_test_ml_dsa(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL_ML_DSA + ret |= test_auth_create_destroy_ctx(); + ret |= test_load_free_crt(); + ret |= test_load_free_privkey(); + ret |= test_load_free_pubkey(); + ret |= test_verify_crt(); + ret |= test_auth_sign(); + ret |= test_auth_bad_signature(); +#else + (void) test_auth_create_destroy_ctx; + (void) test_load_free_crt; + (void) test_load_free_privkey; + (void) test_load_free_pubkey; + (void) test_verify_crt; + (void) test_auth_sign; + (void) test_auth_bad_signature; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/auth_test_slh_dsa.c b/src/lib/tests/auth_test_slh_dsa.c new file mode 100644 index 00000000..511d20fe --- /dev/null +++ b/src/lib/tests/auth_test_slh_dsa.c @@ -0,0 +1,367 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the SLH-DSA-SHA2-128s 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/. + */ + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/crypt.h> +#include <ouroboros/random.h> +#include <ouroboros/utils.h> + +#include <test/certs/slh_dsa.h> + +#define TEST_MSG_SIZE 1500 + +static int test_auth_create_destroy_ctx(void) +{ + struct auth_ctx * ctx; + + TEST_START(); + + ctx = auth_create_ctx(); + if (ctx == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create; + } + + auth_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_create: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_crt(void) +{ + void * crt; + + TEST_START(); + + if (crypt_load_crt_str(root_ca_crt_slh, &crt) < 0) { + printf("Failed to load root crt.\n"); + goto fail_load; + } + + crypt_free_crt(crt); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_privkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_privkey_str(server_pkp_slh, &key) < 0) { + printf("Failed to load server key pair.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_load_free_pubkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_pubkey_str(server_pk_slh, &key) < 0) { + printf("Failed to load server public key.\n"); + goto fail_load; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_load: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_verify_crt(void) +{ + struct auth_ctx * auth; + void * _server_crt; + void * _signed_server_crt; + void * _root_ca_crt; + void * _im_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(server_crt_slh, + &_server_crt) < 0) { + printf("Failed to load self-signed crt.\n"); + goto fail_load_server_crt; + } + + if (crypt_load_crt_str(signed_server_crt_slh, + &_signed_server_crt) < 0) { + printf("Failed to load signed crt.\n"); + goto fail_load_signed_server_crt; + } + + if (crypt_load_crt_str(root_ca_crt_slh, + &_root_ca_crt) < 0) { + printf("Failed to load root crt.\n"); + goto fail_load_root_ca_crt; + } + + if (crypt_load_crt_str(im_ca_crt_slh, + &_im_ca_crt) < 0) { + printf("Failed to load im crt.\n"); + goto fail_load_im_ca_crt; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add im ca crt.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _signed_server_crt) < 0) { + printf("Failed to verify signed crt.\n"); + goto fail_verify; + } + + if (auth_verify_crt(auth, _server_crt) == 0) { + printf("Failed to detect untrusted crt.\n"); + goto fail_verify; + } + + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_server_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca_crt: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca_crt: + crypt_free_crt(_signed_server_crt); + fail_load_signed_server_crt: + crypt_free_crt(_server_crt); + fail_load_server_crt: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_auth_sign(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to gen random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_slh, + &pkp) < 0) { + printf("Failed to load server key pair.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_slh, + &pk) < 0) { + printf("Failed to load public key.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + if (auth_verify_sig(pk, 0, msg, sig) < 0) { + printf("Failed to verify signature.\n"); + goto fail_verify; + } + + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_auth_bad_signature(void) +{ + uint8_t buf[TEST_MSG_SIZE]; + void * pkp; + void * pk; + buffer_t msg; + buffer_t sig; + buffer_t fake_sig; + + TEST_START(); + + msg.data = buf; + msg.len = sizeof(buf); + + if (random_buffer(msg.data, msg.len) < 0) { + printf("Failed to gen random message.\n"); + goto fail_init; + } + + if (crypt_load_privkey_str(server_pkp_slh, + &pkp) < 0) { + printf("Failed to load server key pair.\n"); + goto fail_init; + } + + if (crypt_load_pubkey_str(server_pk_slh, + &pk) < 0) { + printf("Failed to load public key.\n"); + goto fail_pubkey; + } + + if (auth_sign(pkp, 0, msg, &sig) < 0) { + printf("Failed to sign message.\n"); + goto fail_sign; + } + + fake_sig.data = malloc(sig.len); + if (fake_sig.data == NULL) { + printf("Failed to alloc fake sig buf.\n"); + goto fail_malloc; + } + + fake_sig.len = sig.len; + if (random_buffer(fake_sig.data, + fake_sig.len) < 0) { + printf("Failed to gen random fake sig.\n"); + goto fail_malloc; + } + + if (auth_verify_sig(pk, 0, msg, fake_sig) == 0) { + printf("Failed to detect bad sig.\n"); + goto fail_verify; + } + + freebuf(fake_sig); + freebuf(sig); + + crypt_free_key(pk); + crypt_free_key(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + freebuf(fake_sig); + fail_malloc: + freebuf(sig); + fail_sign: + crypt_free_key(pk); + fail_pubkey: + crypt_free_key(pkp); + fail_init: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int auth_test_slh_dsa(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL_SLH_DSA + ret |= test_auth_create_destroy_ctx(); + ret |= test_load_free_crt(); + ret |= test_load_free_privkey(); + ret |= test_load_free_pubkey(); + ret |= test_verify_crt(); + ret |= test_auth_sign(); + ret |= test_auth_bad_signature(); +#else + (void) test_auth_create_destroy_ctx; + (void) test_load_free_crt; + (void) test_load_free_privkey; + (void) test_load_free_pubkey; + (void) test_verify_crt; + (void) test_auth_sign; + (void) test_auth_bad_signature; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/bitmap_test.c b/src/lib/tests/bitmap_test.c index 0815ecff..81735a41 100644 --- a/src/lib/tests/bitmap_test.c +++ b/src/lib/tests/bitmap_test.c @@ -1,5 +1,5 @@ /* - * Ouroboros - Copyright (C) 2016 - 2021 + * Ouroboros - Copyright (C) 2016 - 2026 * * Test of the bitmap * @@ -27,7 +27,8 @@ #define BITMAP_SIZE 200 -int bitmap_test(int argc, char ** argv) +int bitmap_test(int argc, + char ** argv) { struct bmp * bmp; ssize_t bits = BITMAP_SIZE; @@ -60,27 +61,23 @@ int bitmap_test(int argc, char ** argv) if (!bmp_is_id_valid(bmp, id)) { if (i < BITMAP_SIZE + offset) { printf("Failed valid ID %d (%zd).\n", i, id); - bmp_destroy(bmp); - return -1; + goto fail; } if (id >= offset && id < bits + offset) { printf("Valid ID %zd returned invalid.\n", id); - bmp_destroy(bmp); - return -1; + goto fail; } continue; } if (!bmp_is_id_used(bmp, id)) { printf("ID not marked in use.\n"); - bmp_destroy(bmp); - return -1; + goto fail; } if (id != i) { printf("Wrong ID returned.\n"); - bmp_destroy(bmp); - return -1; + goto fail; } } @@ -89,20 +86,24 @@ int bitmap_test(int argc, char ** argv) if (bmp_release(bmp, r)) { printf("Failed to release ID.\n"); - return -1; + goto fail; } id = bmp_allocate(bmp); if (!bmp_is_id_valid(bmp, id)) continue; + if (id != r) { printf("Wrong prev ID returned.\n"); - bmp_destroy(bmp); - return -1; + goto fail; } } bmp_destroy(bmp); return 0; + + fail: + bmp_destroy(bmp); + return -1; } diff --git a/src/lib/tests/btree_test.c b/src/lib/tests/btree_test.c index 9dc59d32..d1a72af8 100644 --- a/src/lib/tests/btree_test.c +++ b/src/lib/tests/btree_test.c @@ -1,5 +1,5 @@ /* - * Ouroboros - Copyright (C) 2016 - 2021 + * Ouroboros - Copyright (C) 2016 - 2026 * * Test of the B-tree implementation * diff --git a/src/lib/tests/crc32_test.c b/src/lib/tests/crc32_test.c deleted file mode 100644 index a0f70423..00000000 --- a/src/lib/tests/crc32_test.c +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2021 - * - * Test of the CRC32 function - * - * 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 <ouroboros/crc32.h> - -#include <stdlib.h> -#include <stdint.h> -#include <assert.h> -#include <string.h> -#include <stdio.h> - -/* - * Test vectors calculated at - * https://www.lammertbies.nl/comm/info/crc-calculation.html - */ - -int crc32_test(int argc, - char ** argv) -{ - uint32_t crc = 0; - int i = 0; - - (void) argc; - (void) argv; - - crc32(&crc, "0", 1); - if (crc != 0xF4DBDF21) - return -1; - - crc = 0; - - crc32(&crc, "123456789", 9); - if (crc != 0xCBF43926) - return -1; - - crc = 0; - - crc32(&crc, "987654321", 9); - if (crc != 0x015F0201) - return -1; - - crc32(&crc, "123456789", 9); - if (crc != 0x806B60E3) - return -1; - - crc = 0; - - crc32(&crc, &i , 1); - if (crc != 0xD202EF8D) - return -1; - - return 0; -} diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c new file mode 100644 index 00000000..2d752238 --- /dev/null +++ b/src/lib/tests/crypt_test.c @@ -0,0 +1,555 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the cryptography 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/. + */ + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/random.h> +#include <ouroboros/crypt.h> +#include <ouroboros/utils.h> + +#include <stdio.h> + +#define TEST_PACKET_SIZE 1500 +#define TEST_N_PACKETS 1000 + +extern const uint16_t crypt_supported_nids[]; +extern const uint16_t md_supported_nids[]; + +static int test_crypt_create_destroy(void) +{ + struct crypt_ctx * ctx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk = { + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + + TEST_START(); + + memset(key, 0, sizeof(key)); + + ctx = crypt_create_ctx(&sk); + if (ctx == NULL) { + printf("Failed to initialize cryptography.\n"); + goto fail; + } + + crypt_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_crypt_encrypt_decrypt(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t out; + buffer_t out2; + const char * cipher; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to initialize TX cryptography.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to initialize RX cryptography.\n"); + goto fail_tx; + } + + in.len = sizeof(pkt); + in.data = pkt; + + if (crypt_encrypt(tx, in, &out) < 0) { + printf("Encryption failed.\n"); + goto fail_encrypt; + } + + if (out.len < in.len) { + printf("Encryption returned too little data.\n"); + goto fail_chk; + } + + if (crypt_decrypt(rx, out, &out2) < 0) { + printf("Decryption failed.\n"); + goto fail_decrypt; + } + + if (out2.len != in.len) { + printf("Decrypted data length does not match original.\n"); + goto fail_chk2; + } + + if (memcmp(in.data, out2.data, in.len) != 0) { + printf("Decrypted data does not match original.\n"); + goto fail_chk2; + } + + freebuf(out2); + freebuf(out); + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_chk2: + freebuf(out2); + fail_decrypt: + fail_chk: + freebuf(out); + fail_encrypt: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_encrypt_decrypt_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_encrypt_decrypt(crypt_supported_nids[i]); + + return ret; +} + +static int test_crypt_multi_packet(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + int i; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + in.len = sizeof(pkt); + in.data = pkt; + + for (i = 0; i < TEST_N_PACKETS; i++) { + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed at packet %d.\n", i); + goto fail_rx; + } + + if (crypt_decrypt(rx, enc, &dec) < 0) { + printf("Decryption failed at packet %d.\n", i); + freebuf(enc); + goto fail_rx; + } + + if (dec.len != in.len || + memcmp(in.data, dec.data, in.len) != 0) { + printf("Data mismatch at packet %d.\n", i); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(dec); + freebuf(enc); + } + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_multi_packet_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_multi_packet(crypt_supported_nids[i]); + + return ret; +} + +static int test_crypt_aad_tamper(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + /* Only AEAD ciphers bind the selector as AAD. */ + if (crypt_get_tagsz(tx) == 0) { + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + } + + in.len = sizeof(pkt); + in.data = pkt; + + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed.\n"); + goto fail_rx; + } + + /* Flip a seq byte: epoch/node stay valid so the AEAD tag rejects. */ + enc.data[5] ^= 0x01; + + if (crypt_decrypt(rx, enc, &dec) == 0) { + printf("Decryption accepted a tampered selector.\n"); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(enc); + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_aad_tamper_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_aad_tamper(crypt_supported_nids[i]); + + return ret; +} + +#ifdef HAVE_OPENSSL +#include <openssl/evp.h> +#include <openssl/obj_mac.h> + +static int test_cipher_nid_values(void) +{ + int i; + + TEST_START(); + + /* Loop over all supported ciphers and verify NIDs match OpenSSL's */ + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) { + uint16_t our_nid = crypt_supported_nids[i]; + const char * str = crypt_nid_to_str(our_nid); + const EVP_CIPHER * cipher; + int openssl_nid; + + if (str == NULL) { + printf("crypt_nid_to_str failed for NID %u\n", our_nid); + goto fail; + } + + cipher = EVP_get_cipherbyname(str); + if (cipher == NULL) { + printf("OpenSSL doesn't recognize cipher '%s'\n", str); + goto fail; + } + + openssl_nid = EVP_CIPHER_nid(cipher); + + if (our_nid != openssl_nid) { + printf("NID mismatch for '%s': ours=%u, OpenSSL=%d\n", + str, our_nid, openssl_nid); + goto fail; + } + + /* Test reverse conversion */ + if (crypt_str_to_nid(str) != our_nid) { + printf("crypt_str_to_nid failed for '%s'\n", str); + goto fail; + } + } + + /* Test error cases */ + if (crypt_str_to_nid("invalid") != NID_undef) { + printf("crypt_str_to_nid: no NID_undef for invalid.\n"); + goto fail; + } + + if (crypt_nid_to_str(9999) != NULL) { + printf("crypt_nid_to_str should return NULL for invalid NID\n"); + goto fail; + } + + if (crypt_str_to_nid(NULL) != NID_undef) { + printf("crypt_str_to_nid should return NID_undef for NULL\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_md_nid_values(void) +{ + int i; + + TEST_START(); + + for (i = 0; md_supported_nids[i] != NID_undef; i++) { + uint16_t our_nid = md_supported_nids[i]; + const EVP_MD * md; + int openssl_nid; + + md = EVP_get_digestbynid(our_nid); + if (md == NULL) { + printf("OpenSSL doesn't recognize NID %u\n", our_nid); + goto fail; + } + + openssl_nid = EVP_MD_nid(md); + if (our_nid != openssl_nid) { + printf("NID mismatch: ours=%u, OpenSSL=%d\n", + our_nid, openssl_nid); + goto fail; + } + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif + +static int test_crypt_headsz(void) +{ + struct crypt_ctx * ctx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk = { + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + int headsz; + + TEST_START(); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail; + } + + ctx = crypt_create_ctx(&sk); + if (ctx == NULL) { + printf("Failed to initialize cryptography.\n"); + goto fail; + } + + headsz = crypt_get_headsz(ctx); + if (headsz != 6) { + printf("Unexpected header size: %d (expected 6).\n", headsz); + goto fail_ctx; + } + + crypt_destroy_ctx(ctx); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_ctx: + crypt_destroy_ctx(ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int crypt_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_crypt_create_destroy(); + ret |= test_encrypt_decrypt_all(); +#ifdef HAVE_OPENSSL + ret |= test_cipher_nid_values(); + ret |= test_md_nid_values(); + ret |= test_multi_packet_all(); + ret |= test_aad_tamper_all(); + ret |= test_crypt_headsz(); +#else + (void) test_multi_packet_all; + (void) test_aad_tamper_all; + (void) test_crypt_headsz; + + return TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/hash_test.c b/src/lib/tests/hash_test.c new file mode 100644 index 00000000..a2ba62cc --- /dev/null +++ b/src/lib/tests/hash_test.c @@ -0,0 +1,349 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the hashing 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/. + */ + +#include <ouroboros/hash.h> +#include <test/test.h> + +#include <stdlib.h> +#include <stdint.h> +#include <assert.h> +#include <string.h> +#include <stdio.h> + +/* + * Test vectors calculated at + * https://www.lammertbies.nl/comm/info/crc-calculation.html + */ + +struct vec_entry { + char * in; + char * out; +}; + +struct mix_entry { + uint64_t in; + uint64_t out; +}; + +static int test_crc8(void) +{ + int ret = 0; + + struct vec_entry vec [] = { + { "", "00" }, + { "123456789", "df" }, + { NULL, NULL } + }; + + struct vec_entry * cur = vec; + + TEST_START(); + + while (cur->in != NULL) { + uint8_t crc; + char res[3]; + + str_hash(HASH_CRC8, &crc, cur->in); + + sprintf(res, "%02x", crc); + if (strcmp(res, cur->out) != 0) { + printf("Hash failed %s != %s.\n", res, cur->out); + ret |= -1; + } + + ++cur; + } + + TEST_END(ret); + + return ret; +} + +static int test_crc16(void) +{ + int ret = 0; + + struct vec_entry vec [] = { + { "", "ffff" }, + { "123456789", "29b1" }, + { NULL, NULL } + }; + + struct vec_entry * cur = vec; + + TEST_START(); + + while (cur->in != NULL) { + uint8_t crc[2]; + char res[5]; + + str_hash(HASH_CRC16, crc, cur->in); + + sprintf(res, "%02x%02x", crc[0], crc[1]); + if (strcmp(res, cur->out) != 0) { + printf("Hash failed %s != %s.\n", res, cur->out); + ret |= -1; + } + + ++cur; + } + + TEST_END(ret); + + return ret; +} + +static int test_crc32(void) +{ + int ret = 0; + + struct vec_entry vec [] = { + { "0", "f4dbdf21" }, + { "123456789", "cbf43926" }, + { "987654321", "015f0201" }, + { NULL, NULL } + }; + + struct vec_entry * cur = vec; + + TEST_START(); + + while (cur->in != NULL) { + uint8_t crc[4]; + char res[9]; + + str_hash(HASH_CRC32, crc, cur->in); + + sprintf(res, HASH_FMT32, HASH_VAL32(crc)); + if (strcmp(res, cur->out) != 0) { + printf("Hash failed %s != %s.\n", res, cur->out); + ret |= -1; + } + + ++cur; + } + + TEST_END(ret); + + return ret; +} + +static int test_crc64(void) +{ + int ret = 0; + + struct vec_entry vec [] = { + { "", "0000000000000000" }, + { "123456789", "ae8b14860a799888" }, + { "0123456789abcdef", + "091485ca7018730e" }, + { NULL, NULL } + }; + + struct vec_entry * cur = vec; + + TEST_START(); + + while (cur->in != NULL) { + uint8_t crc[8]; + char res[17]; + + str_hash(HASH_CRC64, crc, cur->in); + + sprintf(res, HASH_FMT64, HASH_VAL64(crc)); + if (strcmp(res, cur->out) != 0) { + printf("Hash failed %s != %s.\n", res, cur->out); + ret |= -1; + } + + ++cur; + } + + TEST_END(ret); + + return ret; +} + +static int test_md5(void) +{ + int ret = 0; + + struct vec_entry vec [] = {{ + "abc", + "900150983cd24fb0d6963f7d28e17f72" + }, { + "The quick brown fox jumps over the lazy dog", + "9e107d9d372bb6826bd81d3542a419d6" + }, { + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "8215ef0796a20bcaaae116d3876c664a" + }, { + NULL, + NULL + }}; + + struct vec_entry * cur = vec; + + TEST_START(); + + + while (cur->in != NULL) { + uint8_t md5[16]; + char res[33]; + + str_hash(HASH_MD5, md5, cur->in); + + sprintf(res, HASH_FMT128, HASH_VAL128(md5)); + if (strcmp(res, cur->out) != 0) { + printf("Hash failed %s != %s.\n", res, cur->out); + ret |= -1; + } + + ++cur; + } + + TEST_END(ret); + + return ret; +} + +static int test_sha3(void) +{ + int ret = 0; + + uint8_t sha3[64]; + char res[129]; + + char * in = "abc"; + + char * out = + "e642824c3f8cf24ad09234ee7d3c766f" + "c9a3a5168d0c94ad73b46fdf"; + + TEST_START(); + + str_hash(HASH_SHA3_224, sha3, in); + + sprintf(res, HASH_FMT224, HASH_VAL224(sha3)); + if (strcmp(res, out) != 0) { + printf("SHA3-224 failed %s != %s", res, out); + ret |= -1; + } + + out = + "3a985da74fe225b2045c172d6bd390bd" + "855f086e3e9d525b46bfe24511431532"; + + str_hash(HASH_SHA3_256, sha3, in); + + sprintf(res, HASH_FMT256, HASH_VAL256(sha3)); + if (strcmp(res, out) != 0) { + printf("SHA3-256 failed %s != %s.\n", res, out); + ret |= -1; + } + + out = + "ec01498288516fc926459f58e2c6ad8d" + "f9b473cb0fc08c2596da7cf0e49be4b2" + "98d88cea927ac7f539f1edf228376d25"; + + str_hash(HASH_SHA3_384, sha3, in); + + sprintf(res, HASH_FMT384, HASH_VAL384(sha3)); + if (strcmp(res, out) != 0) { + printf("SHA3-384failed %s != %s.'n", res, out); + ret |= -1; + } + + out = + "b751850b1a57168a5693cd924b6b096e" + "08f621827444f70d884f5d0240d2712e" + "10e116e9192af3c91a7ec57647e39340" + "57340b4cf408d5a56592f8274eec53f0"; + + str_hash(HASH_SHA3_512, sha3, in); + + sprintf(res, HASH_FMT512, HASH_VAL512(sha3)); + if (strcmp(res, out) != 0) { + printf("SHA3-512 failed %s != %s.\n", res, out); + ret |= -1; + } + + TEST_END(ret); + + return ret; +} + +static int test_mix64(void) +{ + int ret = 0; + + struct mix_entry vec [] = { + { 0x0000000000000000ULL, 0x0000000000000000ULL }, + { 0x123456789abcdefeULL, 0xb1943cfea4f78f08ULL } + }; + + size_t n = sizeof(vec) / sizeof(vec[0]); + size_t i; + + TEST_START(); + + for (i = 0; i < n; i++) { + uint64_t res = hash_mix64(vec[i].in); + + if (res != vec[i].out) { + printf("Mix failed %016llx != %016llx.\n", + (unsigned long long) res, + (unsigned long long) vec[i].out); + ret |= -1; + } + } + + TEST_END(ret); + + return ret; +} + +int hash_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_crc8(); + + ret |= test_crc16(); + + ret |= test_crc32(); + + ret |= test_crc64(); + + ret |= test_md5(); + + ret |= test_sha3(); + + ret |= test_mix64(); + + return ret; +} diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c new file mode 100644 index 00000000..0a00ccab --- /dev/null +++ b/src/lib/tests/kex_test.c @@ -0,0 +1,1194 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the key exchange 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/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/utils.h> +#include <ouroboros/crypt.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#ifdef HAVE_OPENSSL +#include <openssl/evp.h> +#include <openssl/x509.h> +#endif + +/* Test configuration strings */ +#define KEX_CONFIG_CUSTOM \ + "kex=X25519\n" + +#define KEX_CONFIG_NONE \ + "none\n" + +#define KEX_CONFIG_NO_ENC \ + "encryption=none\n" + +#define KEX_CONFIG_WHITESPACE \ + "# Comment line\n" \ + "kex = X448" \ + "\n" \ + "# Another comment\n" + +#define KEX_CONFIG_CIPHER \ + "kex=X25519\n" \ + "cipher=chacha20-poly1305\n" + +#define KEX_CONFIG_DIGEST \ + "kex=X25519\n" \ + "digest=sha384\n" + +#define KEX_CONFIG_AUTH \ + "auth=required\n" + +#define KEX_CONFIG_AUTH_INVALID \ + "auth=mandatory\n" + +#define KEX_CONFIG_AUTH_OPTIONAL \ + "auth=optional\n" + +#define KEX_CONFIG_AUTH_THEN_NO_ENC \ + "auth=required\n" \ + "digest=sha512\n" \ + "encryption=none\n" + +#define KEX_CONFIG_NO_ENC_THEN_AUTH \ + "encryption=none\n" \ + "auth=required\n" \ + "digest=sha512\n" + +#define KEX_CONFIG_CACERT \ + "cacert=/etc/ouroboros/security/cacert/ca.crt\n" + +#define KEX_CONFIG_UNKNOWN_KEY \ + "autth=required\n" + +/* Test key material for key loading tests */ +#define X25519_PRIVKEY_PEM \ + "-----BEGIN PRIVATE KEY-----\n" \ + "MC4CAQAwBQYDK2VuBCIEIJDd3+/0k2IZlaH5sZ9Z2e5J8dV2U0nsXaSUm70ZaMhL\n" \ + "-----END PRIVATE KEY-----\n" + +#define X25519_PUBKEY_PEM \ + "-----BEGIN PUBLIC KEY-----\n" \ + "MCowBQYDK2VuAyEAKYLIycSZtLFlwAX07YWWgBAYhEnRxHfgK1TVw9+mtBs=\n" \ + "-----END PUBLIC KEY-----\n" + +/* Helper macro to open string constant as FILE stream */ +#define FMEMOPEN_STR(str) fmemopen((void *) (str), strlen(str), "r") + +extern const uint16_t kex_supported_nids[]; + +int parse_sec_config(struct sec_config * cfg, + FILE * fp); + +static int test_kex_create_destroy(void) +{ + struct sec_config cfg; + + TEST_START(); + + memset(&cfg, 0, sizeof(cfg)); + cfg.x.nid = NID_X9_62_prime256v1; + cfg.x.str = kex_nid_to_str(cfg.x.nid); + cfg.c.nid = NID_aes_256_gcm; + cfg.c.str = crypt_nid_to_str(cfg.c.nid); + + if (cfg.x.nid == NID_undef || cfg.c.nid == NID_undef) { + printf("Failed to initialize kex config.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_dh_pkp_create_destroy(void) +{ + struct sec_config kex; + void * pkp; + uint8_t buf[CRYPT_KEY_BUFSZ]; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, "prime256v1"); + + if (kex_pkp_create(&kex, &pkp, buf) < 0) { + printf("Failed to create DH PKP.\n"); + goto fail; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_get_algo_from_pk(const char * algo) +{ + struct sec_config kex; + void * pkp; + buffer_t pk; + ssize_t len; + uint8_t buf[CRYPT_KEY_BUFSZ]; + char extracted_algo[256]; + + TEST_START("(%s)", algo); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + + len = kex_pkp_create(&kex, &pkp, buf); + if (len < 0) { + printf("Failed to create key pair.\n"); + goto fail; + } + + pk.len = (size_t) len; + pk.data = buf; + + /* Use raw decode for hybrid KEMs, DER for others */ + if (IS_HYBRID_KEM(algo)) { + if (kex_get_algo_from_pk_raw(pk, extracted_algo) < 0) { + printf("Failed to extract algo from pk.\n"); + goto fail_pkp; + } + } else { + if (kex_get_algo_from_pk_der(pk, extracted_algo) < 0) { + printf("Failed to extract algo from pk.\n"); + goto fail_pkp; + } + } + + /* All algorithms should now return the specific group name */ + if (strcmp(extracted_algo, algo) != 0) { + printf("Algo mismatch: expected %s, got %s.\n", + algo, extracted_algo); + goto fail_pkp; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp: + kex_pkp_destroy(pkp); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_get_algo_from_pk_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]); + ret |= test_kex_get_algo_from_pk(algo); + } + + return ret; +} + +static int test_kex_dhe_derive(const char * algo) +{ + struct sec_config kex; + void * pkp1; + void * pkp2; + buffer_t pk1; + buffer_t pk2; + ssize_t len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s1[SYMMKEYSZ]; + uint8_t s2[SYMMKEYSZ]; + + TEST_START("(%s)", algo); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + SET_KEX_KDF_NID(&kex, NID_sha256); + + len = kex_pkp_create(&kex, &pkp1, buf1); + if (len < 0) { + printf("Failed to create first key pair for %s.\n", algo); + goto fail; + } + + pk1.len = (size_t) len; + pk1.data = buf1; + + len = kex_pkp_create(&kex, &pkp2, buf2); + if (len < 0) { + printf("Failed to create second key pair for %s.\n", algo); + goto fail_pkp1; + } + + pk2.len = (size_t) len; + pk2.data = buf2; + + if (kex_dhe_derive(&kex, pkp1, pk2, s1) < 0) { + printf("Failed to derive first key for %s.\n", algo); + goto fail_pkp2; + } + + if (kex_dhe_derive(&kex, pkp2, pk1, s2) < 0) { + printf("Failed to derive second key for %s.\n", algo); + goto fail_pkp2; + } + + if (memcmp(s1, s2, SYMMKEYSZ) != 0) { + printf("Derived keys do not match for %s.\n", algo); + goto fail_pkp2; + } + + kex_pkp_destroy(pkp2); + kex_pkp_destroy(pkp1); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp2: + kex_pkp_destroy(pkp2); + fail_pkp1: + kex_pkp_destroy(pkp1); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_validate_algo(void) +{ + TEST_START(); + + if (kex_validate_algo("prime256v1") != 0) { + printf("prime256v1 should be valid.\n"); + goto fail; + } + + if (kex_validate_algo("X25519") != 0) { + printf("X25519 should be valid.\n"); + goto fail; + } + +#ifdef HAVE_OPENSSL_ML_KEM + if (kex_validate_algo("ML-KEM-768") != 0) { + printf("ML-KEM-768 should be valid.\n"); + goto fail; + } +#endif + + if (kex_validate_algo("ffdhe2048") != 0) { + printf("ffdhe2048 should be valid.\n"); + goto fail; + } + + if (kex_validate_algo("invalid_algo") == 0) { + printf("invalid_algo should be rejected.\n"); + goto fail; + } + + if (kex_validate_algo("rsa2048") == 0) { + printf("rsa2048 should be rejected.\n"); + goto fail; + } + + if (kex_validate_algo(NULL) == 0) { + printf("NULL should be rejected.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_dhe_corrupted_pubkey(const char * algo) +{ + struct sec_config kex; + void * pkp; + buffer_t pk; + ssize_t len; + uint8_t buf[CRYPT_KEY_BUFSZ]; + uint8_t s[SYMMKEYSZ]; + + TEST_START("(%s)", algo); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + SET_KEX_KDF_NID(&kex, NID_sha256); + + len = kex_pkp_create(&kex, &pkp, buf); + if (len < 0) { + printf("Failed to create key pair.\n"); + goto fail; + } + + pk.len = (size_t) len; + pk.data = buf; + + /* Corrupt the public key */ + buf[0] ^= 0xFF; + buf[len - 1] ^= 0xFF; + + if (kex_dhe_derive(&kex, pkp, pk, s) == 0) { + printf("Should fail with corrupted public key.\n"); + goto fail_pkp; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp: + kex_pkp_destroy(pkp); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_dhe_wrong_algo(void) +{ + struct sec_config kex1; + struct sec_config kex2; + void * pkp1; + void * pkp2; + buffer_t pk2; + ssize_t len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s[SYMMKEYSZ]; + const char * algo1 = "X25519"; + const char * algo2 = "X448"; + + TEST_START("(%s vs %s)", algo1, algo2); + + memset(&kex1, 0, sizeof(kex1)); + memset(&kex2, 0, sizeof(kex2)); + SET_KEX_ALGO(&kex1, algo1); + SET_KEX_ALGO(&kex2, algo2); + SET_KEX_KDF_NID(&kex1, NID_sha256); + SET_KEX_KDF_NID(&kex2, NID_sha256); + + if (kex_pkp_create(&kex1, &pkp1, buf1) < 0) { + printf("Failed to create first key pair.\n"); + goto fail; + } + + len = kex_pkp_create(&kex2, &pkp2, buf2); + if (len < 0) { + printf("Failed to create second key pair.\n"); + goto fail_pkp1; + } + + pk2.len = (size_t) len; + pk2.data = buf2; + + /* Try to derive with mismatched algorithms */ + if (kex_dhe_derive(&kex1, pkp1, pk2, s) == 0) { + printf("Should fail with mismatched algorithms.\n"); + goto fail_pkp2; + } + + kex_pkp_destroy(pkp2); + kex_pkp_destroy(pkp1); + + TEST_SUCCESS("(%s vs %s)", algo1, algo2); + + return TEST_RC_SUCCESS; + fail_pkp2: + kex_pkp_destroy(pkp2); + fail_pkp1: + kex_pkp_destroy(pkp1); + fail: + TEST_FAIL("(%s vs %s)", algo1, algo2); + return TEST_RC_FAIL; +} + +static int test_kex_load_dhe_privkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_privkey_str(X25519_PRIVKEY_PEM, &key) < 0) { + printf("Failed to load X25519 private key.\n"); + goto fail; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_load_dhe_pubkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_pubkey_str(X25519_PUBKEY_PEM, &key) < 0) { + printf("Failed to load X25519 public key.\n"); + goto fail; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +#ifdef HAVE_OPENSSL +#include <openssl/obj_mac.h> + +static int test_kex_nid_values(void) +{ + int i; + + TEST_START(); + + /* Verify all KEX algorithm NIDs match OpenSSL's */ + for (i = 0; kex_supported_nids[i] != NID_undef; i++) { + uint16_t our_nid = kex_supported_nids[i]; + const char * kex_name; + int openssl_nid; + + kex_name = kex_nid_to_str(our_nid); + if (kex_name == NULL) { + printf("kex_nid_to_str failed for NID %u\n", our_nid); + goto fail; + } + + /* Test reverse conversion */ + if (kex_str_to_nid(kex_name) != our_nid) { + printf("kex_str_to_nid failed for '%s'\n", kex_name); + goto fail; + } + + /* Get OpenSSL's NID for this name */ + openssl_nid = OBJ_txt2nid(kex_name); + if (openssl_nid != NID_undef) { + /* OpenSSL recognizes this algorithm */ + if (our_nid != openssl_nid) { + printf("NID mismatch for '%s': " + "ours=%d, OpenSSL=%d\n", + kex_name, our_nid, openssl_nid); + goto fail; + } + } else { + /* Verify no NID collision with different algorithm */ + const char * ossl_name = OBJ_nid2sn(our_nid); + if (ossl_name != NULL && + strcmp(ossl_name, kex_name) != 0) { + printf("NID collision for '%d': " + "ours=%s, OpenSSL=%s\n", + our_nid, kex_name, ossl_name); + goto fail; + } + } + } + + /* Test error cases */ + if (kex_str_to_nid("invalid") != NID_undef) { + printf("kex_str_to_nid should return NID_undef for invalid\n"); + goto fail; + } + + if (kex_nid_to_str(9999) != NULL) { + printf("kex_nid_to_str should return NULL for invalid NID\n"); + goto fail; + } + + if (kex_str_to_nid(NULL) != NID_undef) { + printf("kex_str_to_nid should return NID_undef for NULL\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif + +static int test_kex_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]); + + /* KEM tests are in kex_test_ml_kem.c */ + if (IS_KEM_ALGORITHM(algo)) + continue; + + ret |= test_kex_dhe_derive(algo); + } + + return ret; +} + +static int test_kex_dhe_corrupted_pubkey_all(void) +{ + int ret = 0; + int i; + + /* Test corruption for all DHE algorithms */ + /* KEM error injection tests are in kex_test_ml_kem.c */ + 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_kex_dhe_corrupted_pubkey(algo); + } + + return ret; +} + +static int test_kex_parse_config_empty(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR("\n"); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse empty config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.x.str, "prime256v1") != 0) { + printf("Empty config should use prime256v1.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_custom(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_CUSTOM); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse custom config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.x.str, "X25519") != 0) { + printf("Algorithm not set correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* The old bare 'none' keyword must be rejected loudly */ +static int test_kex_parse_config_none_rejected(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_NONE); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Bare 'none' keyword should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_no_enc(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_NO_ENC); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse encryption=none config.\n"); + fclose(fp); + goto fail; + } + + if (kex.x.nid != NID_undef || kex.c.nid != NID_undef) { + printf("encryption=none should disable encryption.\n"); + fclose(fp); + goto fail; + } + + if (kex.d.nid != NID_sha256) { + printf("encryption=none should keep the digest.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_whitespace(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_WHITESPACE); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse config with comments.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.x.str, "X448") != 0) { + printf("Algorithm with whitespace not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_cipher(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_CIPHER); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse cipher config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.x.str, "X25519") != 0) { + printf("Algorithm not set correctly.\n"); + fclose(fp); + goto fail; + } + + if (kex.c.nid != NID_chacha20_poly1305) { + printf("Cipher not set correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_digest(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_DIGEST); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse digest config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.x.str, "X25519") != 0) { + printf("Algorithm not set correctly.\n"); + fclose(fp); + goto fail; + } + + if (kex.d.nid != NID_sha384) { + printf("Digest not set correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_auth(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth config.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("auth=required not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_auth_invalid(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH_INVALID); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Invalid auth value should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* A caller-seeded req_auth survives parsing when no auth= line is set */ +static int test_kex_parse_config_auth_seed(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + kex.a.req = true; + + fp = FMEMOPEN_STR(KEX_CONFIG_NO_ENC); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse config.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("Seeded req_auth should survive parsing.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* An explicit auth=optional clears a caller-seeded req_auth */ +static int test_kex_parse_config_auth_optional(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + kex.a.req = true; + + fp = FMEMOPEN_STR(KEX_CONFIG_AUTH_OPTIONAL); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth=optional config.\n"); + fclose(fp); + goto fail; + } + + if (kex.a.req) { + printf("auth=optional should clear req_auth.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* encryption=none must not drop auth=required or the digest */ +static int test_kex_parse_config_auth_no_enc(const char * config) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(config); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse auth + encryption=none.\n"); + fclose(fp); + goto fail; + } + + if (!kex.a.req) { + printf("encryption=none should not drop required auth.\n"); + fclose(fp); + goto fail; + } + + if (kex.x.nid != NID_undef) { + printf("encryption=none should disable encryption.\n"); + fclose(fp); + goto fail; + } + + if (kex.d.nid != NID_sha512) { + printf("encryption=none should keep the digest.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_cacert(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_CACERT); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse cacert config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.a.cacert, + "/etc/ouroboros/security/cacert/ca.crt") != 0) { + printf("cacert not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + if (kex.a.req) { + printf("cacert must not imply req_auth.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_unknown_key(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_UNKNOWN_KEY); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Unknown key should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int kex_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_kex_create_destroy(); + ret |= test_kex_parse_config_empty(); + ret |= test_kex_parse_config_none_rejected(); + ret |= test_kex_parse_config_no_enc(); + ret |= test_kex_parse_config_auth(); + ret |= test_kex_parse_config_auth_invalid(); + ret |= test_kex_parse_config_auth_seed(); + ret |= test_kex_parse_config_auth_optional(); + ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_AUTH_THEN_NO_ENC); + ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_NO_ENC_THEN_AUTH); + ret |= test_kex_parse_config_cacert(); + ret |= test_kex_parse_config_unknown_key(); +#ifdef HAVE_OPENSSL + ret |= test_kex_parse_config_custom(); + ret |= test_kex_parse_config_whitespace(); + ret |= test_kex_parse_config_cipher(); + ret |= test_kex_parse_config_digest(); + ret |= test_kex_nid_values(); + ret |= test_kex_dh_pkp_create_destroy(); + ret |= test_kex_all(); + ret |= test_kex_validate_algo(); + ret |= test_kex_get_algo_from_pk_all(); + ret |= test_kex_dhe_wrong_algo(); + ret |= test_kex_dhe_corrupted_pubkey_all(); + ret |= test_kex_load_dhe_privkey(); + ret |= test_kex_load_dhe_pubkey(); +#else + (void) test_kex_parse_config_custom; + (void) test_kex_parse_config_whitespace; + (void) test_kex_parse_config_cipher; + (void) test_kex_parse_config_digest; + (void) test_kex_dh_pkp_create_destroy; + (void) test_kex_all; + (void) test_kex_validate_algo; + (void) test_kex_get_algo_from_pk_all; + (void) test_kex_dhe_wrong_algo(); + (void) test_kex_dhe_corrupted_pubkey_all; + (void) test_kex_load_dhe_privkey; + (void) test_kex_load_dhe_pubkey; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/kex_test_ml_kem.c b/src/lib/tests/kex_test_ml_kem.c new file mode 100644 index 00000000..7761c3dc --- /dev/null +++ b/src/lib/tests/kex_test_ml_kem.c @@ -0,0 +1,549 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the post-quantum key exchange 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/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "config.h" + +#include <test/test.h> +#include <ouroboros/utils.h> +#include <ouroboros/crypt.h> +#include <ouroboros/random.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#ifdef HAVE_OPENSSL +#include <openssl/evp.h> +#include <openssl/x509.h> +#endif + +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]; +} + +/* ML-KEM-768 test key material */ + +#define MLKEM768_PRIVKEY_PEM \ + "-----BEGIN PRIVATE KEY-----\n" \ + "MIIJvgIBADALBglghkgBZQMEBAIEggmqMIIJpgRA+QIIiQLQkS5fl5RluSmgXRjZ\n" \ + "YU16W4TVt0dmnBP41rLTTRT3S8CRtkb+xmoFAcWTfEzbdr5pp3g2CBRx+APXTwSC\n" \ + "CWBll6AecTd1Kqdyix3zNQcthDBP0XnwdTHDqkKuFzMP58Y+0gc9Bo+W0xBOK2ZK\n" \ + "gcAmix3YLJuDS8Teep/Tdc7KIm5AaLNoI8BIMgKC/ASsW8kC+78BV4OIgqNWurS9\n" \ + "BrTiCmiag7c+6DsVDJHJ4kfcccwUDBKiW0v+LAkk1HXBcx6usrwuFC0H3ICli2sC\n" \ + "o5DfGL7g4kWHhobXjAZnxn298C8FGmLQK5kah4nZiJ+MuHqrirziCGTLKkY1a8vC\n" \ + "GFgzfHIcvB4dtyi9dxZmWpSXqDf2AVNgqrD2C7WQEULQOKxm/I8Mw31Yp8TC6SAP\n" \ + "RzM4cBAXF00W4Rce05O0am/ga5dStAhikMESyckCoEGlPFFXOmjy1HmOasI+AbGk\n" \ + "2BKp6cfbImbjd0ePdCSFEgIQwAQHm7+4UoZR2JmNwSI1AC2P4FMRAIaD2A69i6LC\n" \ + "kFniGcOog5m09nw5FqZmeEfNs6yyFGSX16D1YyjuooAFGlU0FFX7aKwsYM8t1gkS\n" \ + "YSUfMxIW9yzhSW4vZHuGyxlxBMr1y51RZrW8gnvW5p/Ip5yDBJRahY6KMWT15C14\n" \ + "C2rIe8U+d4Xi5IMI3D1JNpwFebYhKs3/ManxoU7Fwwa0GzQrgLYU5KhqO8/hopnl\n" \ + "8mQH+BPh+TR5lqYawS7HZXFJE8JzOnCtOSgB6Hz2U7oG9ik8h0FRqVD3ak20EmZU\n" \ + "c7gpGW8Odc51uaIBzDu4ej4dGgwo4awYaX4ugLOutHqGqRfCjIVb6XQ4m35p4KKi\n" \ + "qBVQ211aIhavUIgNECJ7WUETilXyyHLB9x3EFJdidEfSRUxLYJNAC5XM2WFCyhnE\n" \ + "pKmossSNq6ZOqBjPegE0J6zfNg65dR/OlIdGVDgrVTIpwYAUzBMW2nTnCa00EmPj\n" \ + "F7tRscHI8qb/QlnRVEUN+S+A2CtVIH1c666zOoRFRI9G4bmVoa8k2x0ANB51tCns\n" \ + "vAYqkMybIgMvWwbqoAxeW0G1O3qObGXtgs94BzhAEM3RbG/hy3GR1qUNSk/qyDKc\n" \ + "t1qpiaao0aLVsnpb28eBIk6+q0I82reGdV31OYvUpnVxRbRPFXEFs5PNS3s/7I8a\n" \ + "SlSLUGOh+mhrUzDPSJCzgEvOmrwrRxe3F52tS0nAt6Z5zKToASHphoISUi7lGX1F\n" \ + "Owx62qhSqqlI98bKqh7yQRZYrHXqE0bscAHCcIaZ8RVya42JHDCoQWyxqBuLOWEl\n" \ + "+Fz6vI5DqEnJkA7ke49EvBAOJ58lxAXQIV5remtzYGPKdyG2oamiFHiLVQDzGX/l\n" \ + "aFNMGXRWcK4/Y3mnkJvx9QGtq6KstQN/J4a51ZeX5YwNBcoY9UcFS6kHRW5rR3UM\n" \ + "tEZj5VN8BL9nyWM9h7hUSHQboaxO7M5qswfXB8f21xR16T40Ki4nawx/6zHGCQsc\n" \ + "uKr5SaCV88tghqJYHBorU5iKB5KsLDSHqYYrNo/Vy8W6kMA2jGAO24d4G32DSshR\n" \ + "sEF9W1nuAHK/5ste01G5KmX2KhdZBE37oGhM98HRQ6hU8qwuKrhdV7vZis5C8LXY\n" \ + "7MbDyDt1NnFqWFc6lYeVa6eRcmYzeAbXahrxwiiaLIdHXD95aZ/0S6+tKBGgQzwm\n" \ + "ZsbwdXhl+n+yqDNE6Sow2bwueqhDwZVWoMCv5SK+HAGPtcZ7UU9oWrqpiL085m7F\n" \ + "5G49KJUEZadVtj4Z9zrkeQkida+4I7v3Y3MzsWsGJww7YhTDJpsxxmSm85bHwx98\n" \ + "hZXSqckJTL4c2nBzgrBlukIT9Wl+qItMthVvABPzp4wGZhdgKrEIRl3yCnhhUgpL\n" \ + "lUxYegwWDMEjZxKlSbIyl5p9lCS8w2lsBzsQ2FJiAy/MWLa56aA+wFs3C8smZ6Cf\n" \ + "p5NWa8Rm+k898GWBxZivhF03CBOZ42du0YUZdCPoA5V1KC6bh4JyWFI49VFbQFMG\n" \ + "gwAqc0ErAH3iMammKC9746WWagnUIG3o8LygZrusuGeTohXJhVUTJDw2s0rzNhbw\n" \ + "5IyookkY5BWENKFKTIgdBxvYelOKwbGE8Z36FEW0ABlmx7SRCKWlNVjSEAIXmMiQ\n" \ + "VLdQF33QVYD9RR5chja254VuJH4plo+5JwiKWz8LlCIBm7CVkifZMLofmMk3s3L4\n" \ + "sXtE+Bhfm5Plk3RrgDdlHH+hK7gk61XGdynGjDY7aLtCKZ0SMsVskSLom1pbIR5M\n" \ + "KLYsQ1Pse4mhfDOFCkWFLI5TShGMuIoo1k7XeIE6g8QoUlV5EXyWHHhIVaE4yWGP\n" \ + "AVgEp0UswKFeeo3SoCAeADA3U88ymxpBJp73yDIqok5dM3SgkjfPWZDkgkAI8WHs\n" \ + "CKKeqrSOs1kkE3JXtE7kcTHT6XHo162TmgGkqMVwOQ3EmR6FRpYxJhZvuVbjJsSx\n" \ + "YjW3ScnR4Zivoi7q95ypco331pIlIZpqV0NydUpMyQaz1cnoPKYDh1xa6LhcqEKK\n" \ + "8a68iXjQgzgqQBDABonVybNDtlJ5lnTTuKhak8PBFAmmhj1JdrPqoIvQRCmLaark\n" \ + "J7/q9RLtk6kTOJ0qtLe2qqwCxJwyoMd2Q5F4+xTWZHu90ljRdcnYewarqcKzoL27\n" \ + "tcpTOmVz88I1hYVUJEV7aB36QMhTS1dquTqJZCD0hBPWAMToEoD4OFvKWmbFmzaW\n" \ + "xrMc4ECYeDAAKYs2YqoXSLfAixBmZjb6UDB61l2GA58pFJW0ZwN8S5tApA2NRi+7\n" \ + "oC/zgMgBGHft6E0+OUVb8It89pY1t7ybq5+fkBvEixDId3f1pK3gqcaYqG/YhoMJ\n" \ + "MJWkqYxCNGmdZ8gFo46V6K+4xZUblQWKypN6+RYO4kDh0koppWGEULjgBoCH+V8E\n" \ + "7GcoE8SRdQY1BIMoRVWb8Ur8ZYIVU8lqgaZPlWM3oRCiWk0kRxexFF0i5WlILIK9\n" \ + "GT8saX+bmRd9KSy3JrpPhQn59CpJBRxz8WKdJ3wwtqE/2TbxQhLooEWHYVrZEG5E\n" \ + "SkIoOkUAJUR+CzLLFDMdUE8w3CasE4ys+hco7AA5TAms24A1FXcxMgNb6VHA0bi5\n" \ + "c8rPCZvjubLXR4A0/A2Ualo4cy3UAr9k0rbZOJnjqk8eExkeaxbyh42cJpU75i4O\n" \ + "NLYsRZJkg9bkCpPgZKb707sPZO72CX3h/lQdXVgGkZ7Tqd1qzM+JOhSWvrYiBLa+\n" \ + "5IKSmFwT+5sw1InEesXwRN09000U90vAkbZG/sZqBQHFk3xM23a+aad4NggUcfgD\n" \ + "108=\n" \ + "-----END PRIVATE KEY-----\n" + +#define MLKEM768_PUBKEY_PEM \ + "-----BEGIN PUBLIC KEY-----\n" \ + "MIIEsjALBglghkgBZQMEBAIDggShAMPIO3U2cWpYVzqVh5Vrp5FyZjN4BtdqGvHC\n" \ + "KJosh0dcP3lpn/RLr60oEaBDPCZmxvB1eGX6f7KoM0TpKjDZvC56qEPBlVagwK/l\n" \ + "Ir4cAY+1xntRT2hauqmIvTzmbsXkbj0olQRlp1W2Phn3OuR5CSJ1r7gju/djczOx\n" \ + "awYnDDtiFMMmmzHGZKbzlsfDH3yFldKpyQlMvhzacHOCsGW6QhP1aX6oi0y2FW8A\n" \ + "E/OnjAZmF2AqsQhGXfIKeGFSCkuVTFh6DBYMwSNnEqVJsjKXmn2UJLzDaWwHOxDY\n" \ + "UmIDL8xYtrnpoD7AWzcLyyZnoJ+nk1ZrxGb6Tz3wZYHFmK+EXTcIE5njZ27RhRl0\n" \ + "I+gDlXUoLpuHgnJYUjj1UVtAUwaDACpzQSsAfeIxqaYoL3vjpZZqCdQgbejwvKBm\n" \ + "u6y4Z5OiFcmFVRMkPDazSvM2FvDkjKiiSRjkFYQ0oUpMiB0HG9h6U4rBsYTxnfoU\n" \ + "RbQAGWbHtJEIpaU1WNIQAheYyJBUt1AXfdBVgP1FHlyGNrbnhW4kfimWj7knCIpb\n" \ + "PwuUIgGbsJWSJ9kwuh+YyTezcvixe0T4GF+bk+WTdGuAN2Ucf6EruCTrVcZ3KcaM\n" \ + "Njtou0IpnRIyxWyRIuibWlshHkwotixDU+x7iaF8M4UKRYUsjlNKEYy4iijWTtd4\n" \ + "gTqDxChSVXkRfJYceEhVoTjJYY8BWASnRSzAoV56jdKgIB4AMDdTzzKbGkEmnvfI\n" \ + "MiqiTl0zdKCSN89ZkOSCQAjxYewIop6qtI6zWSQTcle0TuRxMdPpcejXrZOaAaSo\n" \ + "xXA5DcSZHoVGljEmFm+5VuMmxLFiNbdJydHhmK+iLur3nKlyjffWkiUhmmpXQ3J1\n" \ + "SkzJBrPVyeg8pgOHXFrouFyoQorxrryJeNCDOCpAEMAGidXJs0O2UnmWdNO4qFqT\n" \ + "w8EUCaaGPUl2s+qgi9BEKYtpquQnv+r1Eu2TqRM4nSq0t7aqrALEnDKgx3ZDkXj7\n" \ + "FNZke73SWNF1ydh7BqupwrOgvbu1ylM6ZXPzwjWFhVQkRXtoHfpAyFNLV2q5Oolk\n" \ + "IPSEE9YAxOgSgPg4W8paZsWbNpbGsxzgQJh4MAApizZiqhdIt8CLEGZmNvpQMHrW\n" \ + "XYYDnykUlbRnA3xLm0CkDY1GL7ugL/OAyAEYd+3oTT45RVvwi3z2ljW3vJurn5+Q\n" \ + "G8SLEMh3d/WkreCpxpiob9iGgwkwlaSpjEI0aZ1nyAWjjpXor7jFlRuVBYrKk3r5\n" \ + "Fg7iQOHSSimlYYRQuOAGgIf5XwTsZygTxJF1BjUEgyhFVZvxSvxlghVTyWqBpk+V\n" \ + "YzehEKJaTSRHF7EUXSLlaUgsgr0ZPyxpf5uZF30pLLcmuk+FCfn0KkkFHHPxYp0n\n" \ + "fDC2oT/ZNvFCEuigRYdhWtkQbkRKQig6RQAlRH4LMssUMx1QTzDcJqwTjKz6Fyjs\n" \ + "ADlMCazbgDUVdzEyA1vpUcDRuLlzys8Jm+O5stdHgDT8DZRqWjhzLdQCv2TSttk4\n" \ + "meOqTx4TGR5rFvKHjZwmlTvmLg40tixFkmSD1uQKk+BkpvvTuw9k7vYJfeH+VB1d\n" \ + "WAaRntOp\n" \ + "-----END PUBLIC KEY-----\n" + +/* Helper macro to open string constant as FILE stream */ +#define FMEMOPEN_STR(str) fmemopen((void *) (str), strlen(str), "r") + +static int test_kex_load_kem_privkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_privkey_str(MLKEM768_PRIVKEY_PEM, &key) < 0) { + printf("Failed to load ML-KEM-768 private key.\n"); + goto fail; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_load_kem_pubkey(void) +{ + void * key; + + TEST_START(); + + if (crypt_load_pubkey_str(MLKEM768_PUBKEY_PEM, &key) < 0) { + printf("Failed to load ML-KEM-768 public key.\n"); + goto fail; + } + + crypt_free_key(key); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_kem(const char * algo) +{ + struct sec_config kex; + void * pkp; + buffer_t pk; + buffer_t ct; + ssize_t len; + ssize_t ct_len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s1[SYMMKEYSZ]; + uint8_t s2[SYMMKEYSZ]; + int kdf; + + TEST_START("(%s)", algo); + + kdf = get_random_kdf(); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + + len = kex_pkp_create(&kex, &pkp, buf1); + if (len < 0) { + printf("Failed to create key pair for %s.\n", algo); + goto fail; + } + + pk.len = (size_t) len; + pk.data = buf1; + + if (IS_HYBRID_KEM(algo)) + ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + else + ct_len = kex_kem_encap(pk, buf2, kdf, s1); + + if (ct_len < 0) { + printf("Failed to encapsulate for %s.\n", algo); + goto fail_pkp; + } + + ct.len = (size_t) ct_len; + ct.data = buf2; + + if (kex_kem_decap(pkp, ct, kdf, s2) < 0) { + printf("Failed to decapsulate for %s.\n", algo); + goto fail_pkp; + } + + if (memcmp(s1, s2, SYMMKEYSZ) != 0) { + printf("Shared secrets don't match for %s.\n", algo); + goto fail_pkp; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp: + kex_pkp_destroy(pkp); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_kem_corrupted_ciphertext(const char * algo) +{ + struct sec_config kex; + void * pkp; + buffer_t pk; + buffer_t ct; + ssize_t len; + ssize_t ct_len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s1[SYMMKEYSZ]; + uint8_t s2[SYMMKEYSZ]; + int kdf; + + TEST_START("(%s)", algo); + + kdf = get_random_kdf(); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + + len = kex_pkp_create(&kex, &pkp, buf1); + if (len < 0) { + printf("Failed to create key pair.\n"); + goto fail; + } + + pk.len = (size_t) len; + pk.data = buf1; + + if (IS_HYBRID_KEM(algo)) + ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + else + ct_len = kex_kem_encap(pk, buf2, kdf, s1); + + if (ct_len < 0) { + printf("Failed to encapsulate.\n"); + goto fail_pkp; + } + + ct.len = (size_t) ct_len; + ct.data = buf2; + + /* Corrupt the ciphertext */ + buf2[0] ^= 0xFF; + buf2[ct_len - 1] ^= 0xFF; + + /* ML-KEM uses implicit rejection */ + if (kex_kem_decap(pkp, ct, kdf, s2) < 0) { + printf("Decapsulation failed unexpectedly.\n"); + goto fail_pkp; + } + + /* The shared secrets should NOT match with corrupted CT */ + if (memcmp(s1, s2, SYMMKEYSZ) == 0) { + printf("Corrupted ciphertext produced same secret.\n"); + goto fail_pkp; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp: + kex_pkp_destroy(pkp); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_kem_wrong_keypair(const char * algo) +{ + struct sec_config kex; + void * pkp1; + void * pkp2; + buffer_t pk1; + buffer_t ct; + ssize_t len; + ssize_t ct_len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t buf3[CRYPT_KEY_BUFSZ]; + uint8_t s1[SYMMKEYSZ]; + uint8_t s2[SYMMKEYSZ]; + + TEST_START("(%s)", algo); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + + len = kex_pkp_create(&kex, &pkp1, buf1); + if (len < 0) { + printf("Failed to create first key pair.\n"); + goto fail; + } + + pk1.len = (size_t) len; + pk1.data = buf1; + + if (kex_pkp_create(&kex, &pkp2, buf2) < 0) { + printf("Failed to create second key pair.\n"); + goto fail_pkp1; + } + + if (IS_HYBRID_KEM(algo)) + ct_len = kex_kem_encap_raw(pk1, buf3, NID_sha256, s1); + else + ct_len = kex_kem_encap(pk1, buf3, NID_sha256, s1); + + if (ct_len < 0) { + printf("Failed to encapsulate.\n"); + goto fail_pkp2; + } + + ct.len = (size_t) ct_len; + ct.data = buf3; + + if (kex_kem_decap(pkp2, ct, NID_sha256, s2) == 0) { + if (memcmp(s1, s2, SYMMKEYSZ) == 0) { + printf("Wrong keypair produced same secret.\n"); + goto fail_pkp2; + } + } + + kex_pkp_destroy(pkp2); + kex_pkp_destroy(pkp1); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp2: + kex_pkp_destroy(pkp2); + fail_pkp1: + kex_pkp_destroy(pkp1); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_kem_truncated_ciphertext(const char * algo) +{ + struct sec_config kex; + void * pkp; + buffer_t pk; + buffer_t ct; + ssize_t len; + ssize_t ct_len; + uint8_t buf1[CRYPT_KEY_BUFSZ]; + uint8_t buf2[CRYPT_KEY_BUFSZ]; + uint8_t s1[SYMMKEYSZ]; + uint8_t s2[SYMMKEYSZ]; + + TEST_START("(%s)", algo); + + memset(&kex, 0, sizeof(kex)); + SET_KEX_ALGO(&kex, algo); + + len = kex_pkp_create(&kex, &pkp, buf1); + if (len < 0) { + printf("Failed to create key pair.\n"); + goto fail; + } + + pk.len = (size_t) len; + pk.data = buf1; + + if (IS_HYBRID_KEM(algo)) + ct_len = kex_kem_encap_raw(pk, buf2, NID_sha256, s1); + else + ct_len = kex_kem_encap(pk, buf2, NID_sha256, s1); + + if (ct_len < 0) { + printf("Failed to encapsulate.\n"); + goto fail_pkp; + } + + /* Truncate the ciphertext */ + ct.len = (size_t) ct_len / 2; + ct.data = buf2; + + if (kex_kem_decap(pkp, ct, NID_sha256, s2) == 0) { + printf("Should fail with truncated ciphertext.\n"); + goto fail_pkp; + } + + kex_pkp_destroy(pkp); + + TEST_SUCCESS("(%s)", algo); + + return TEST_RC_SUCCESS; + fail_pkp: + kex_pkp_destroy(pkp); + fail: + TEST_FAIL("(%s)", algo); + return TEST_RC_FAIL; +} + +static int test_kex_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_kex_kem(algo); + } + + return ret; +} + +static int test_kex_kem_corrupted_ciphertext_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_kex_kem_corrupted_ciphertext(algo); + } + + return ret; +} + +static int test_kex_kem_wrong_keypair_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_kex_kem_wrong_keypair(algo); + } + + return ret; +} + +static int test_kex_kem_truncated_ciphertext_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_kex_kem_truncated_ciphertext(algo); + } + + return ret; +} + +int kex_test_ml_kem(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL_ML_KEM + ret |= test_kex_load_kem_privkey(); + ret |= test_kex_load_kem_pubkey(); + ret |= test_kex_kem_all(); + ret |= test_kex_kem_corrupted_ciphertext_all(); + ret |= test_kex_kem_wrong_keypair_all(); + ret |= test_kex_kem_truncated_ciphertext_all(); +#else + (void) test_kex_load_kem_privkey; + (void) test_kex_load_kem_pubkey; + (void) test_kex_kem_all; + (void) test_kex_kem_corrupted_ciphertext_all; + (void) test_kex_kem_wrong_keypair_all; + (void) test_kex_kem_truncated_ciphertext_all; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/lib/tests/keyrot_test.c b/src/lib/tests/keyrot_test.c new file mode 100644 index 00000000..1c9f741b --- /dev/null +++ b/src/lib/tests/keyrot_test.c @@ -0,0 +1,1083 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the key-rotation schedule + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "config.h" + +#include <test/test.h> + +#ifdef HAVE_OPENSSL +#include <ouroboros/crypt.h> +#include <ouroboros/pthread.h> + +#include "crypt/keyrot.h" + +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +static const uint8_t SEED_A[SYMMKEYSZ] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 +}; + +static int test_create_destroy(void) +{ + struct keyrot * kr; + + TEST_START(); + + kr = keyrot_create(SEED_A, 0, 0); + if (kr == NULL) + goto fail; + + keyrot_destroy(kr); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_epoch_range(void) +{ + struct keyrot * a; + + TEST_START(); + + /* epoch is a 4-bit wire field; 16 and up must be refused. */ + if (keyrot_create(SEED_A, 16, 0) != NULL) + goto fail; + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + if (keyrot_rekey(a, SEED_A, 16) == 0) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tx_deterministic(void) +{ + struct keyrot * a; + struct keyrot * b; + uint8_t sela[KR_SELECTOR_LEN]; + uint8_t selb[KR_SELECTOR_LEN]; + uint8_t na[KR_NONCE_LEN]; + uint8_t nb[KR_NONCE_LEN]; + const uint8_t * ka; + const uint8_t * kb; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 0); + if (b == NULL) + goto fail_a; + + if (keyrot_tx_next(a, sela, &ka, na) != 0) + goto fail_b; + + if (keyrot_tx_next(b, selb, &kb, nb) != 0) + goto fail_b; + + if (memcmp(sela, selb, KR_SELECTOR_LEN) != 0) + goto fail_b; + + if (memcmp(ka, kb, SYMMKEYSZ) != 0) + goto fail_b; + + if (memcmp(na, nb, KR_NONCE_LEN) != 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_selector_layout(void) +{ + struct keyrot * a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + + TEST_START(); + + a = keyrot_create(SEED_A, 3, 0); + if (a == NULL) + goto fail; + + /* First packet: epoch 3, node 0, seq 0 */ + if (keyrot_tx_next(a, sel, &k, nonce) != 0) + goto fail_a; + + if ((sel[0] >> 4) != 3) /* epoch */ + goto fail_a; + + if ((((sel[0] & 0x0F) << 8) | sel[1]) != 0) /* node */ + goto fail_a; + + if (sel[2] != 0 || sel[3] != 0 || sel[4] != 0 || sel[5] != 0) + goto fail_a; + + /* Second packet: seq advances to 1 */ + if (keyrot_tx_next(a, sel, &k, nonce) != 0) + goto fail_a; + + if (sel[5] != 1) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_nodes_left_initial(void) +{ + struct keyrot * a; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + if (keyrot_tx_nodes_left(a) != KEY_NODE_COUNT) + goto fail_a; + + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_roundtrip(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t ntx[KR_NONCE_LEN]; + uint8_t nrx[KR_NONCE_LEN]; + uint8_t ktx[SYMMKEYSZ]; + const uint8_t * ptx; + const uint8_t * prx; + struct kr_rx rx; + int i; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + for (i = 0; i < 256; i++) { + if (keyrot_tx_next(a, sel, &ptx, ntx) != 0) + goto fail_b; + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0) + goto fail_b; + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + if (memcmp(ntx, nrx, KR_NONCE_LEN) != 0) + goto fail_b; + } + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_direction_separation(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sela[KR_SELECTOR_LEN]; + uint8_t selb[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + uint8_t ka[SYMMKEYSZ]; + const uint8_t * pa; + const uint8_t * pb; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + if (keyrot_tx_next(a, sela, &pa, n) != 0) + goto fail_b; + + memcpy(ka, pa, SYMMKEYSZ); + if (keyrot_tx_next(b, selb, &pb, n) != 0) + goto fail_b; + + /* Same position, different role -> different leaf key */ + if (memcmp(ka, pb, SYMMKEYSZ) == 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Build a selector by hand (test knows the wire format). */ +static void mk_sel(uint8_t epoch, + uint16_t node, + uint32_t seq, + uint8_t sel[KR_SELECTOR_LEN]) +{ + sel[0] = (uint8_t) ((epoch << 4) | ((node >> 8) & 0x0F)); + sel[1] = (uint8_t) (node & 0xFF); + sel[2] = (uint8_t) (seq >> 24); + sel[3] = (uint8_t) (seq >> 16); + sel[4] = (uint8_t) (seq >> 8); + sel[5] = (uint8_t) (seq); +} + +static int test_random_access(void) +{ + struct keyrot * b; + uint8_t s0[KR_SELECTOR_LEN]; + uint8_t s5[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + uint8_t k_first[SYMMKEYSZ]; + uint8_t k_node5[SYMMKEYSZ]; + const uint8_t * p; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 0, s0); + mk_sel(0, 5, 12345, s5); /* a far-ahead node, mid-span */ + + /* Jump straight to node 0 */ + if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0) + goto fail_b; + + memcpy(k_first, p, SYMMKEYSZ); + + /* Jump forward to node 5 (simulates a burst skip) */ + if (keyrot_rx_lookup(b, s5, &p, n, &rx) != 0) + goto fail_b; + + memcpy(k_node5, p, SYMMKEYSZ); + + /* Different nodes must yield different keys */ + if (memcmp(k_first, k_node5, SYMMKEYSZ) == 0) + goto fail_b; + + /* Jump back to node 0: still works, identical (no wedge) */ + if (keyrot_rx_lookup(b, s0, &p, n, &rx) != 0) + goto fail_b; + + if (memcmp(k_first, p, SYMMKEYSZ) != 0) + goto fail_b; + + /* Out-of-range node must be rejected */ + mk_sel(0, KEY_NODE_COUNT, 0, s0); + if (keyrot_rx_lookup(b, s0, &p, n, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static const uint8_t SEED_B[SYMMKEYSZ] = { + 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, + 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, + 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, + 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0 +}; + +/* + * Look up and commit one within-node counter on epoch 0. Returns 0 on + * accept, 1 on a rejected commit (replay or too old), and -1 if the + * lookup itself failed - kept distinct so a reject assertion can never + * pass on an unrelated lookup miss. + */ +static int commit_ctr(struct keyrot * kr, + uint32_t ctr) +{ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + mk_sel(0, 0, ctr, sel); + + if (keyrot_rx_lookup(kr, sel, &k, n, &rx) != 0) + return -1; + + return keyrot_rx_commit(kr, &rx) == 0 ? 0 : 1; +} + +static int test_replay_window(void) +{ + struct keyrot * b; + struct keyrot * c; + uint32_t base; + uint32_t jump; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* Fresh counters accepted; an immediate replay is rejected. */ + if (commit_ctr(b, 100) != 0) + goto fail_b; + + if (commit_ctr(b, 100) != 1) + goto fail_b; + + /* In-window reorder: accepted once, rejected on replay. */ + if (commit_ctr(b, 105) != 0) + goto fail_b; + + if (commit_ctr(b, 102) != 0) + goto fail_b; + + if (commit_ctr(b, 102) != 1) + goto fail_b; + + /* Too-old boundary: the window edge is rejected, just inside is not. */ + base = 4 * KEY_REPLAY_WINDOW; + if (commit_ctr(b, base) != 0) + goto fail_b; + + if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64)) != 1) + goto fail_b; + + if (commit_ctr(b, base - (KEY_REPLAY_WINDOW - 64) + 1) != 0) + goto fail_b; + + /* + * RFC 6479 slack-word regression: two low counters, then a + * forward jump of a full bitmap that aliases their slot, then a + * replay of a low counter. Without the reserved slack word this + * replay is wrongly accepted. + */ + c = keyrot_create(SEED_A, 0, 1); + if (c == NULL) + goto fail_b; + + if (commit_ctr(c, 70) != 0) + goto fail_c; + + if (commit_ctr(c, 74) != 0) + goto fail_c; + + jump = KEY_REPLAY_WINDOW + 63; + if (commit_ctr(c, jump) != 0) + goto fail_c; + + if (commit_ctr(c, 74) != 1) + goto fail_c; + + keyrot_destroy(c); + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_c: + keyrot_destroy(c); + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_lookup_no_commit(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + int i; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 100, sel); + + /* Repeated lookups are pre-AEAD and must not consume the slot. */ + for (i = 0; i < 4; i++) { + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + } + + /* The slot is still fresh, so the first commit accepts ... */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + /* ... and only the commit advanced it, so the next is a replay. */ + if (keyrot_rx_commit(b, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_commit_prev_batch(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* Capture a packet under cur (epoch 0). */ + mk_sel(0, 0, 7, sel); + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + /* Re-key: the captured batch becomes prev and the flag clears. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* The straggler commits under prev without claiming a switch. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + /* prev still holds a replay window: its replay is rejected. */ + if (keyrot_rx_commit(b, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_replay_forward_clear(void) +{ + struct keyrot * d; + uint32_t low; + uint32_t alias; + uint32_t jump; + + TEST_START(); + + d = keyrot_create(SEED_A, 0, 1); + if (d == NULL) + goto fail; + + /* alias shares low's slot a window away; the jump must clear it. */ + low = 10; + alias = low + KEY_REPLAY_WINDOW; + jump = alias + KEY_REPLAY_WINDOW / 2; + + if (commit_ctr(d, low) != 0) + goto fail_d; + + if (commit_ctr(d, jump) != 0) + goto fail_d; + + if (commit_ctr(d, alias) != 0) + goto fail_d; + + if (commit_ctr(d, alias) != 1) + goto fail_d; + + keyrot_destroy(d); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_d: + keyrot_destroy(d); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_rekey_overlap(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t old_sel[KR_SELECTOR_LEN]; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t ntx[KR_NONCE_LEN]; + uint8_t nrx[KR_NONCE_LEN]; + uint8_t ktx[SYMMKEYSZ]; + const uint8_t * ptx; + const uint8_t * prx; + struct kr_rx rx; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + /* Send one gen-0 packet; keep its selector for the overlap. */ + if (keyrot_tx_next(a, old_sel, &ptx, ntx) != 0) + goto fail_b; + + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0) + goto fail_b; + + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + + /* Both ends re-key to epoch 1 with a fresh seed. */ + if (keyrot_rekey(a, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* TX is gated until promotion; promote a to emit the new epoch. */ + keyrot_tx_promote(a); + + /* New gen-1 traffic works. */ + if (keyrot_tx_next(a, sel, &ptx, ntx) != 0) + goto fail_b; + + memcpy(ktx, ptx, SYMMKEYSZ); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) != 0) + goto fail_b; + + if (memcmp(ktx, prx, SYMMKEYSZ) != 0) + goto fail_b; + + /* A straggling gen-0 packet still decrypts (overlap window). */ + if (keyrot_rx_lookup(b, old_sel, &prx, nrx, &rx) != 0) + goto fail_b; + + /* An unknown epoch is rejected. */ + mk_sel(7, 0, 0, sel); + if (keyrot_rx_lookup(b, sel, &prx, nrx, &rx) == 0) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tx_gate(void) +{ + struct keyrot * a; /* role 0 */ + struct keyrot * b; /* role 1 */ + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * p; + struct kr_rx rx; + + TEST_START(); + + a = keyrot_create(SEED_A, 0, 0); + if (a == NULL) + goto fail; + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail_a; + + /* Both re-key to epoch 1; TX must stay on epoch 0 until promoted. */ + if (keyrot_rekey(a, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + /* a's TX still stamps the old epoch (0). */ + if (keyrot_tx_next(a, sel, &p, n) != 0) + goto fail_b; + + if ((sel[0] >> 4) != 0) + goto fail_b; + + /* b decrypts the old-epoch packet via its prev batch. */ + if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0) + goto fail_b; + + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + /* b has not yet seen the new epoch from a. */ + if (keyrot_peer_switched(b)) + goto fail_b; + + /* a promotes; its TX now stamps the new epoch (1). */ + keyrot_tx_promote(a); + if (keyrot_tx_next(a, sel, &p, n) != 0) + goto fail_b; + + if ((sel[0] >> 4) != 1) + goto fail_b; + + /* b sees the new epoch and reports the peer switched. */ + if (keyrot_rx_lookup(b, sel, &p, n, &rx) != 0) + goto fail_b; + + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (!keyrot_peer_switched(b)) + goto fail_b; + + keyrot_destroy(b); + keyrot_destroy(a); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail_a: + keyrot_destroy(a); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_peer_switched_commit_only(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + /* A re-key clears the flag until a packet is seen on cur. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + mk_sel(1, 0, 0, sel); + + /* Lookup is pre-AEAD: selecting a key must not flip the flag. */ + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + if (keyrot_peer_switched(b)) + goto fail_b; + + /* Commit runs post-AEAD and is what records the peer switched. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + if (!keyrot_peer_switched(b)) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_commit_evicted(void) +{ + struct keyrot * b; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t n[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + + TEST_START(); + + b = keyrot_create(SEED_A, 0, 1); + if (b == NULL) + goto fail; + + mk_sel(0, 0, 3, sel); + if (keyrot_rx_lookup(b, sel, &k, n, &rx) != 0) + goto fail_b; + + /* Two re-keys drop the captured batch from both cur and prev. */ + if (keyrot_rekey(b, SEED_B, 1) != 0) + goto fail_b; + + if (keyrot_rekey(b, SEED_A, 2) != 0) + goto fail_b; + + /* Commit on an evicted batch is a silent no-op, not a fault. */ + if (keyrot_rx_commit(b, &rx) != 0) + goto fail_b; + + keyrot_destroy(b); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_b: + keyrot_destroy(b); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* + * Concurrency: many TX threads + RX + re-key share one keyrot. The + * (epoch, counter) the TX side stamps must be globally unique (no AEAD + * nonce reuse). Capped below 16 re-keys so epoch maps 1:1 to a batch and + * the wire epoch never wraps (a wrapped epoch under a fresh key is not + * reuse but would false-trip the uniqueness check). Run under TSan to + * catch data races the static reviews can't. + */ +#define CT_THREADS 4 +#define CT_PKTS 2000 +#define CT_REKEYS 8 + +struct ct_rec { + uint8_t epoch; + uint64_t ctr; +}; + +struct ct_arg { + struct keyrot * kr; + struct ct_rec * recs; + size_t n; +}; + +static void * ct_tx_thread(void * a) +{ + struct ct_arg * arg = a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + uint64_t ctr; + size_t i; + size_t j; + + for (i = 0; i < CT_PKTS; i++) { + if (keyrot_tx_next(arg->kr, sel, &k, nonce) != 0) + continue; + + ctr = 0; + for (j = 0; j < 8; j++) + ctr = (ctr << 8) | nonce[j]; + + arg->recs[arg->n].epoch = (uint8_t) (sel[0] >> 4); + arg->recs[arg->n].ctr = ctr; + arg->n++; + } + + return NULL; +} + +static void * ct_rx_thread(void * a) +{ + struct keyrot * kr = a; + uint8_t sel[KR_SELECTOR_LEN]; + uint8_t nonce[KR_NONCE_LEN]; + const uint8_t * k; + struct kr_rx rx; + size_t i; + + /* Exercise rx_lookup against re-key reclaim; results ignored. */ + for (i = 0; i < CT_PKTS; i++) { + mk_sel((uint8_t) (i % 16), 0, (uint32_t) i, sel); + if (keyrot_rx_lookup(kr, sel, &k, nonce, &rx) == 0) + (void) keyrot_rx_commit(kr, &rx); + } + + return NULL; +} + +static void * ct_rekey_thread(void * a) +{ + struct keyrot * kr = a; + struct timespec t; + int e; + + t.tv_sec = 0; + t.tv_nsec = 2 * 1000 * 1000; /* 2 ms */ + + for (e = 1; e <= CT_REKEYS; e++) { + nanosleep(&t, NULL); + if (keyrot_rekey(kr, (e & 1) ? SEED_B : SEED_A, + (uint8_t) e) != 0) + break; + keyrot_tx_promote(kr); + } + + return NULL; +} + +static int ct_cmp(const void * x, + const void * y) +{ + const struct ct_rec * a = x; + const struct ct_rec * b = y; + + if (a->epoch != b->epoch) + return a->epoch < b->epoch ? -1 : 1; + + if (a->ctr != b->ctr) + return a->ctr < b->ctr ? -1 : 1; + + return 0; +} + +static int test_concurrent_nonce_unique(void) +{ + struct keyrot * kr; + struct ct_arg arg[CT_THREADS]; + pthread_t tx[CT_THREADS]; + pthread_t rx; + pthread_t rk; + struct ct_rec * all; + size_t total; + size_t i; + bool reuse = false; + + TEST_START(); + + kr = keyrot_create(SEED_A, 0, 0); + if (kr == NULL) + goto fail; + + all = malloc(sizeof(*all) * CT_THREADS * CT_PKTS); + if (all == NULL) + goto fail_kr; + + for (i = 0; i < CT_THREADS; i++) { + arg[i].kr = kr; + arg[i].n = 0; + arg[i].recs = all + i * CT_PKTS; + } + + for (i = 0; i < CT_THREADS; i++) + pthread_create(&tx[i], NULL, ct_tx_thread, &arg[i]); + + pthread_create(&rx, NULL, ct_rx_thread, kr); + pthread_create(&rk, NULL, ct_rekey_thread, kr); + + for (i = 0; i < CT_THREADS; i++) + pthread_join(tx[i], NULL); + + pthread_join(rx, NULL); + pthread_join(rk, NULL); + + total = 0; + for (i = 0; i < CT_THREADS; i++) { + memmove(all + total, all + i * CT_PKTS, + arg[i].n * sizeof(*all)); + total += arg[i].n; + } + + qsort(all, total, sizeof(*all), ct_cmp); + + for (i = 1; i < total; i++) + if (ct_cmp(&all[i - 1], &all[i]) == 0) { + printf("(epoch %u, ctr %llu) reused\n", + all[i].epoch, + (unsigned long long) all[i].ctr); + reuse = true; + break; + } + + free(all); + + if (reuse) + goto fail_kr; + + keyrot_destroy(kr); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_kr: + keyrot_destroy(kr); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +#endif /* HAVE_OPENSSL */ + +int keyrot_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL + ret |= test_create_destroy(); + ret |= test_epoch_range(); + ret |= test_tx_deterministic(); + ret |= test_selector_layout(); + ret |= test_nodes_left_initial(); + ret |= test_roundtrip(); + ret |= test_direction_separation(); + ret |= test_random_access(); + ret |= test_peer_switched_commit_only(); + ret |= test_commit_evicted(); + ret |= test_replay_window(); + ret |= test_lookup_no_commit(); + ret |= test_commit_prev_batch(); + ret |= test_replay_forward_clear(); + ret |= test_rekey_overlap(); + ret |= test_tx_gate(); + ret |= test_concurrent_nonce_unique(); +#endif + return ret; +} diff --git a/src/lib/tests/md5_test.c b/src/lib/tests/md5_test.c index b5ad127f..ea3e12b3 100644 --- a/src/lib/tests/md5_test.c +++ b/src/lib/tests/md5_test.c @@ -1,5 +1,5 @@ /* - * Ouroboros - Copyright (C) 2016 - 2021 + * Ouroboros - Copyright (C) 2016 - 2026 * * Test of the MD5 function * diff --git a/src/lib/tests/sha3_test.c b/src/lib/tests/sha3_test.c index 4860cd9b..ccd4e12a 100644 --- a/src/lib/tests/sha3_test.c +++ b/src/lib/tests/sha3_test.c @@ -1,5 +1,5 @@ /* - * Ouroboros - Copyright (C) 2016 - 2021 + * Ouroboros - Copyright (C) 2016 - 2026 * * Test of the SHA3 function * diff --git a/src/lib/tests/shm_rbuff_test.c b/src/lib/tests/shm_rbuff_test.c deleted file mode 100644 index a3ed1449..00000000 --- a/src/lib/tests/shm_rbuff_test.c +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2021 - * - * Test of the shm_rbuff - * - * 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 200112L - -#include "config.h" - -#include <ouroboros/shm_rbuff.h> - -#include <errno.h> -#include <stdio.h> -#include <unistd.h> - -int shm_rbuff_test(int argc, - char ** argv) -{ - struct shm_rbuff * rb; - size_t i; - - (void) argc; - (void) argv; - - printf("Test: create rbuff..."); - - rb = shm_rbuff_create(getpid(), 1); - if (rb == NULL) - goto err; - - printf("success.\n\n"); - printf("Test: write a value..."); - - if (shm_rbuff_write(rb, 1) < 0) - goto error; - - printf("success.\n\n"); - printf("Test: check queue length is 1..."); - - if (shm_rbuff_queued(rb) != 1) - goto error; - - printf("success.\n\n"); - printf("Test: read a value..."); - - if (shm_rbuff_read(rb) != 1) - goto error; - - printf("success.\n\n"); - printf("Test: check queue is empty..."); - - if (shm_rbuff_read(rb) != -EAGAIN) - goto error; - - printf("success.\n\n"); - printf("Test: fill the queue..."); - - for (i = 0; i < SHM_RBUFF_SIZE - 1; ++i) { - if (shm_rbuff_queued(rb) != i) - goto error; - if (shm_rbuff_write(rb, 1) < 0) - goto error; - } - - printf("success.\n\n"); - printf("Test: check queue is full..."); - - if (shm_rbuff_queued(rb) != SHM_RBUFF_SIZE - 1) - goto error; - - printf("success [%zd entries].\n\n", shm_rbuff_queued(rb)); - - printf("Test: check queue is full by writing value..."); - if (!(shm_rbuff_write(rb, 1) < 0)) - goto error; - - printf("success [%zd entries].\n\n", shm_rbuff_queued(rb)); - - /* empty the rbuff */ - while (shm_rbuff_read(rb) >= 0) - ; - - shm_rbuff_destroy(rb); - - return 0; - - error: - /* empty the rbuff */ - while (shm_rbuff_read(rb) >= 0) - ; - - shm_rbuff_destroy(rb); - err: - printf("failed.\n\n"); - return -1; -} diff --git a/src/lib/tests/sockets_test.c b/src/lib/tests/sockets_test.c new file mode 100644 index 00000000..c00bfdc1 --- /dev/null +++ b/src/lib/tests/sockets_test.c @@ -0,0 +1,102 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Tests for socket.c + * + * 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 200112L +#endif + +#include <ouroboros/sockets.h> +#include <test/test.h> + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +#define TEST_PID 1234 +#define TEST_PID_STR "1234" +#define TEST_SERVER_PATH "/tmp/test.sock" +#define TEST_SERVER_PREFIX "/tmp/ouroboros/test." +#define TEST_SOCK_PATH_PREFIX "var/run/ouroboros/test." + +static int test_sock_path(void) +{ + char * path; + char * exp = TEST_SOCK_PATH_PREFIX TEST_PID_STR SOCK_PATH_SUFFIX; + + TEST_START(); + + path = sock_path(TEST_PID, TEST_SOCK_PATH_PREFIX); + if (path == NULL) { + printf("Path is NULL.\n"); + goto fail_path; + } + + if (strcmp(path, exp) != 0) { + printf("Expected path '%s', got '%s'.\n", exp, path); + goto fail_cmp; + } + + free(path); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + fail_cmp: + free(path); + fail_path: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_server_socket_open(void) +{ + int sockfd; + + TEST_START(); + + sockfd = server_socket_open(TEST_SERVER_PATH); + if (sockfd < 0) { + printf("Failed to open server socket.\n"); + goto fail_sock; + } + + close(sockfd); + + unlink(TEST_SERVER_PATH); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + fail_sock: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int sockets_test(void) +{ + int ret = 0; + + ret |= test_sock_path(); + ret |= test_server_socket_open(); + + return ret; +} diff --git a/src/lib/tests/time_test.c b/src/lib/tests/time_test.c new file mode 100644 index 00000000..919cf075 --- /dev/null +++ b/src/lib/tests/time_test.c @@ -0,0 +1,529 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Test of the time utilities + * + * Dimitri Staessens <dimitri@ouroboros.rocks> + * Sander Vrijders <sander@ouroboros.rocks> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., http://www.fsf.org/about/contact/. + */ + +#define _POSIX_C_SOURCE 200809L + +#include <test/test.h> +#include <ouroboros/time.h> + +#include <stdio.h> + +static int ts_check(struct timespec * s, + time_t sec, + time_t nsec) +{ + return s->tv_sec == sec && s->tv_nsec == nsec; +} + +static int tv_check(struct timeval * v, + time_t sec, + time_t usec) +{ + return v->tv_sec == sec && v->tv_usec == usec; +} + + +static int test_time_ts_init(void) +{ + struct timespec s = TIMESPEC_INIT_S (100); + struct timespec ms = TIMESPEC_INIT_MS(100); + struct timespec us = TIMESPEC_INIT_US(100); + struct timespec ns = TIMESPEC_INIT_NS(100); + + TEST_START(); + + if (!ts_check(&s, 100, 0)) { + printf("timespec_init_s failed.\n"); + goto fail; + } + + if (!ts_check(&ms, 0, 100 * MILLION)) { + printf("timespec_init_ms failed.\n"); + goto fail; + } + + if (!ts_check(&us, 0, 100* 1000L)) { + printf("timespec_init_us failed.\n"); + goto fail; + } + + if (!ts_check(&ns, 0, 100)) { + printf("timespec_init_ns failed.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_time_tv_init(void) +{ + struct timeval s = TIMEVAL_INIT_S (100); + struct timeval ms = TIMEVAL_INIT_MS(100); + struct timeval us = TIMEVAL_INIT_US(100); + + TEST_START(); + + if (!tv_check(&s, 100, 0)) { + printf("timeval_init_s failed.\n"); + goto fail; + } + + if (!tv_check(&ms, 0, 100 * 1000L)) { + printf("timeval_init_ms failed.\n"); + goto fail; + } + + if (!tv_check(&us, 0, 100)) { + printf("timeval_init_us failed.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ts_diff(void) +{ + struct timespec s0 = TIMESPEC_INIT_S (100); + struct timespec s1 = TIMESPEC_INIT_S (200); + struct timespec ms0 = TIMESPEC_INIT_MS(100); + struct timespec ms1 = TIMESPEC_INIT_MS(200); + struct timespec us0 = TIMESPEC_INIT_US(100); + struct timespec us1 = TIMESPEC_INIT_US(200); + struct timespec ns0 = TIMESPEC_INIT_NS(100); + struct timespec ns1 = TIMESPEC_INIT_NS(200); + struct timespec res; + + TEST_START(); + + ts_diff(&s0, &s1, &res); + if (!ts_check(&res, -100, 0)) { + printf("timespec_diff failed at s0 - s1.\n"); + goto fail; + } + + ts_diff(&s1, &s0, &res); + if (!ts_check(&res, 100, 0)) { + printf("timespec_diff failed at s1 - s0.\n"); + goto fail; + } + + ts_diff(&ms0, &ms1, &res); + if (!ts_check(&res, -1, 900 * MILLION)) { + printf("timespec_diff failed at ms0 - ms1.\n"); + goto fail; + } + + ts_diff(&ms1, &ms0, &res); + if (!ts_check(&res, 0, 100 * MILLION)) { + printf("timespec_diff failed at ms1 - ms0.\n"); + goto fail; + } + + ts_diff(&us0, &us1, &res); + if (!ts_check(&res, -1, 999900 * 1000L)) { + printf("timespec_diff failed at us0 - us1.\n"); + goto fail; + } + + ts_diff(&us1, &us0, &res); + if (!ts_check(&res, 0, 100 * 1000L)) { + printf("timespec_diff failed at us1 - us0.\n"); + goto fail; + } + + ts_diff(&ns0, &ns1, &res); + if (!ts_check(&res, -1, 999999900)) { + printf("timespec_diff failed at ns0 - ns1.\n"); + goto fail; + } + + ts_diff(&ns1, &ns0, &res); + if (!ts_check(&res, 0, 100)) { + printf("timespec_diff failed at ns1 - ns0.\n"); + goto fail; + } + + ts_diff(&s0, &ms0, &res); + if (!ts_check(&res, 99, 900 * MILLION)) { + printf("timespec_diff failed at s0 - ms0.\n"); + goto fail; + } + + ts_diff(&s0, &us0, &res); + if (!ts_check(&res, 99, 999900 * 1000L)) { + printf("timespec_diff failed at s0 - us0.\n"); + goto fail; + } + + ts_diff(&s0, &ns0, &res); + if (!ts_check(&res, 99, 999999900)) { + printf("timespec_diff failed at s0 - ns0.\n"); + goto fail; + } + + ts_diff(&ms0, &us0, &res); + if (!ts_check(&res, 0, 99900 * 1000L)) { + printf("timespec_diff failed at ms0 - us0.\n"); + goto fail; + } + + ts_diff(&ms0, &ns0, &res); + if (!ts_check(&res, 0, 99999900)) { + printf("timespec_diff failed at ms0 - ns0.\n"); + goto fail; + } + + ts_diff(&us0, &ns0, &res); + if (!ts_check(&res, 0, 99900)) { + printf("timespec_diff failed at us0 - ns0.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tv_diff(void) +{ + struct timeval s0 = TIMEVAL_INIT_S (100); + struct timeval s1 = TIMEVAL_INIT_S (200); + struct timeval ms0 = TIMEVAL_INIT_MS(100); + struct timeval ms1 = TIMEVAL_INIT_MS(200); + struct timeval us0 = TIMEVAL_INIT_US(100); + struct timeval us1 = TIMEVAL_INIT_US(200); + struct timeval res; + + TEST_START(); + + tv_diff(&s0, &s1, &res); + if (!tv_check(&res, -100, 0)) { + printf("timeval_diff failed at s0 - s1.\n"); + goto fail; + } + + tv_diff(&s1, &s0, &res); + if (!tv_check(&res, 100, 0)) { + printf("timeval_diff failed at s1 - s0.\n"); + goto fail; + } + + tv_diff(&ms0, &ms1, &res); + if (!tv_check(&res, -1, 900 * 1000L)) { + printf("timeval_diff failed at ms0 - ms1.\n"); + goto fail; + } + + tv_diff(&ms1, &ms0, &res); + if (!tv_check(&res, 0, 100 * 1000L)) { + printf("timeval_diff failed at ms1 - ms0.\n"); + goto fail; + } + + tv_diff(&us0, &us1, &res); + if (!tv_check(&res, -1, 999900)) { + printf("timeval_diff failed at us0 - us1.\n"); + goto fail; + } + + tv_diff(&us1, &us0, &res); + if (!tv_check(&res, 0, 100)) { + printf("timeval_diff failed at us1 - us0.\n"); + goto fail; + } + + tv_diff(&s0, &ms0, &res); + if (!tv_check(&res, 99, 900 * 1000L)) { + printf("timeval_diff failed at s0 - ms0.\n"); + goto fail; + } + + tv_diff(&s0, &us0, &res); + if (!tv_check(&res, 99, 999900)) { + printf("timeval_diff failed at s0 - us0.\n"); + goto fail; + } + + tv_diff(&ms0, &us0, &res); + if (!tv_check(&res, 0, 99900)) { + printf("timeval_diff failed at ms0 - us0.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_ts_diff_time(void) +{ + struct timespec s0 = TIMESPEC_INIT_S (100); + struct timespec s1 = TIMESPEC_INIT_S (200); + struct timespec ms0 = TIMESPEC_INIT_MS(100); + struct timespec ms1 = TIMESPEC_INIT_MS(200); + struct timespec us0 = TIMESPEC_INIT_US(100); + struct timespec us1 = TIMESPEC_INIT_US(200); + struct timespec ns0 = TIMESPEC_INIT_NS(100); + struct timespec ns1 = TIMESPEC_INIT_NS(200); + + TEST_START(); + + if (ts_diff_ms(&s0, &s1) != -100 * 1000L) { + printf("timespec_diff_ms failed at s0 - s1.\n"); + goto fail; + } + + if (ts_diff_ms(&s1, &s0) != 100 * 1000L) { + printf("timespec_diff_ms failed at s1 - s0.\n"); + goto fail; + } + + if (ts_diff_us(&s0, &s1) != -100 * MILLION) { + printf("timespec_diff_us failed at s1 - s0.\n"); + goto fail; + } + + if (ts_diff_us(&s1, &s0) != 100 * MILLION) { + printf("timespec_diff_us failed at s0 - s1.\n"); + goto fail; + } + + if (ts_diff_ns(&s0, &s1) != -100 * BILLION) { + printf("timespec_diff_ns failed at s0 - s1.\n"); + goto fail; + } + + if (ts_diff_ns(&s1, &s0) != 100 * BILLION) { + printf("timespec_diff_ns failed at s1 - s0.\n"); + goto fail; + } + + if (ts_diff_ms(&ms0, &ms1) != -100) { + printf("timespec_diff_ms failed at ms0 - ms1.\n"); + goto fail; + } + + if (ts_diff_ms(&ms1, &ms0) != 100) { + printf("timespec_diff_ms failed at ms1 - ms0.\n"); + goto fail; + } + + if (ts_diff_us(&ms0, &ms1) != -100 * 1000L) { + printf("timespec_diff_us failed at ms0 - ms1.\n"); + goto fail; + } + + if (ts_diff_us(&ms1, &ms0) != 100 * 1000L) { + printf("timespec_diff_us failed at ms1 - ms0.\n"); + goto fail; + } + + if (ts_diff_ns(&ms0, &ms1) != -100 * MILLION) { + printf("timespec_diff_ns failed at ms0 - ms1.\n"); + goto fail; + } + + if (ts_diff_ns(&ms1, &ms0) != 100 * MILLION) { + printf("timespec_diff_ns failed at ms1 - ms0.\n"); + goto fail; + } + + if (ts_diff_ms(&us0, &us1) != 0) { + printf("timespec_diff_ms failed at us0 - us1.\n"); + goto fail; + } + + if (ts_diff_ms(&us1, &us0) != 0) { + printf("timespec_diff_ms failed at us1 - us0.\n"); + goto fail; + } + + if (ts_diff_us(&us0, &us1) != -100) { + printf("timespec_diff_us failed at us0 - us1.\n"); + goto fail; + } + + if (ts_diff_us(&us1, &us0) != 100) { + printf("timespec_diff_us failed at us1 - us0.\n"); + goto fail; + } + + if (ts_diff_ns(&us0, &us1) != -100 * 1000L) { + printf("timespec_diff_ns failed at us0 - us1.\n"); + goto fail; + } + + if (ts_diff_ns(&us1, &us0) != 100 * 1000L) { + printf("timespec_diff_ns failed at us1 - us0.\n"); + goto fail; + } + + if (ts_diff_ms(&ns0, &ns1) != 0) { + printf("timespec_diff_ms failed at ns0 - ns1.\n"); + goto fail; + } + + if (ts_diff_ms(&ns1, &ns0) != 0) { + printf("timespec_diff_ms failed at ns1 - ns0.\n"); + goto fail; + } + + if (ts_diff_us(&ns0, &ns1) != 0) { + printf("timespec_diff_us failed at ns0 - ns1.\n"); + goto fail; + } + + if (ts_diff_us(&ns1, &ns0) != 0) { + printf("timespec_diff_us failed at ns1 - ns0.\n"); + goto fail; + } + + if (ts_diff_ns(&ns0, &ns1) != -100) { + printf("timespec_diff_ns failed at ns0 - ns1.\n"); + goto fail; + } + + if (ts_diff_ns(&ns1, &ns0) != 100) { + printf("timespec_diff_ns failed at ns1 - ns0.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tv_diff_time(void) +{ + struct timeval s0 = TIMEVAL_INIT_S (100); + struct timeval s1 = TIMEVAL_INIT_S (200); + struct timeval ms0 = TIMEVAL_INIT_MS(100); + struct timeval ms1 = TIMEVAL_INIT_MS(200); + struct timeval us0 = TIMEVAL_INIT_US(100); + struct timeval us1 = TIMEVAL_INIT_US(200); + + TEST_START(); + + if (tv_diff_ms(&s0, &s1) != -100 * 1000L) { + printf("timeval_diff_ms failed at s0 - s1.\n"); + goto fail; + } + + if (tv_diff_ms(&s1, &s0) != 100 * 1000L) { + printf("timeval_diff_ms failed at s1 - s0.\n"); + goto fail; + } + + if (tv_diff_us(&s0, &s1) != -100 * MILLION) { + printf("timeval_diff_us failed at s0 - s1.\n"); + goto fail; + } + + if (tv_diff_us(&s1, &s0) != 100 * MILLION) { + printf("timeval_diff_us failed at s1 - s0.\n"); + goto fail; + } + + if (tv_diff_ms(&ms0, &ms1) != -100) { + printf("timeval_diff_ms failed at ms0 - ms1.\n"); + goto fail; + } + + if (tv_diff_ms(&ms1, &ms0) != 100) { + printf("timeval_diff_ms failed at ms1 - ms0.\n"); + goto fail; + } + + if (tv_diff_us(&ms0, &ms1) != -100 * 1000L) { + printf("timeval_diff_us failed at ms0 - ms1.\n"); + goto fail; + } + + if (tv_diff_us(&ms1, &ms0) != 100 * 1000L) { + printf("timeval_diff_us failed at ms1 - ms0.\n"); + goto fail; + } + + if (tv_diff_ms(&us0, &us1) != 0) { + printf("timeval_diff_ms failed at us0 - us1.\n"); + goto fail; + } + + if (tv_diff_ms(&us1, &us0) != 0) { + printf("timeval_diff_ms failed at us1 - us0.\n"); + goto fail; + } + + if (tv_diff_us(&us0, &us1) != -100) { + printf("timeval_diff_us failed at us0 - us1.\n"); + goto fail; + } + + if (tv_diff_us(&us1, &us0) != 100) { + printf("timeval_diff_us failed at us1 - us0.\n"); + goto fail; + } + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int time_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_time_ts_init(); + ret |= test_time_tv_init(); + ret |= test_ts_diff(); + ret |= test_tv_diff(); + ret |= test_ts_diff_time(); + ret |= test_tv_diff_time(); + + return ret; +} diff --git a/src/lib/tests/time_utils_test.c b/src/lib/tests/time_utils_test.c deleted file mode 100644 index fa65c4dc..00000000 --- a/src/lib/tests/time_utils_test.c +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2021 - * - * Test of the time utilities - * - * Dimitri Staessens <dimitri@ouroboros.rocks> - * Sander Vrijders <sander@ouroboros.rocks> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., http://www.fsf.org/about/contact/. - */ - -#define _POSIX_C_SOURCE 200809L - -#include <ouroboros/time_utils.h> - -#include <stdio.h> - -static void ts_print(struct timespec * s) -{ - printf("timespec is %zd:%ld.\n", (ssize_t) s->tv_sec, s->tv_nsec); -} - -static void tv_print(struct timeval * v) -{ - printf("timeval is %zd:%zu.\n", (ssize_t) v->tv_sec, (size_t) v->tv_usec); -} - -static void ts_init(struct timespec * s, - time_t sec, - time_t nsec) -{ - s->tv_sec = sec; - s->tv_nsec = nsec; -} - -static void tv_init(struct timeval * v, - time_t sec, - time_t usec) -{ - v->tv_sec = sec; - v->tv_usec = usec; -} - -static int ts_check(struct timespec * s, - time_t sec, - time_t nsec) -{ - return s->tv_sec == sec && s->tv_nsec == nsec; -} - -static int tv_check(struct timeval * v, - time_t sec, - time_t usec) -{ - return v->tv_sec == sec && v->tv_usec == usec; -} - -int time_utils_test(int argc, - char ** argv) -{ - struct timespec s0; - struct timespec s1; - struct timespec s2; - - struct timeval v0; - struct timeval v1; - struct timeval v2; - - (void) argc; - (void) argv; - - ts_init(&s0, 0, 0); - ts_init(&s1, 5, 0); - - ts_add(&s0, &s1, &s2); - if (!ts_check(&s2, 5, 0)) { - printf("ts_add failed.\n"); - ts_print(&s2); - return -1; - } - - tv_init(&v0, 0, 0); - tv_init(&v1, 5, 0); - - tv_add(&v0, &v1, &v2); - if (!tv_check(&v2, 5, 0)) { - printf("tv_add failed.\n"); - tv_print(&v2); - return -1; - } - - ts_init(&s0, 0, 500 * MILLION); - ts_init(&s1, 0, 600 * MILLION); - - ts_add(&s0, &s1, &s2); - if (!ts_check(&s2, 1, 100 * MILLION)) { - printf("ts_add with nano overflow failed.\n"); - ts_print(&s2); - return -1; - } - - tv_init(&v0, 0, 500 * 1000); - tv_init(&v1, 0, 600 * 1000); - - tv_add(&v0, &v1, &v2); - if (!tv_check(&v2, 1, 100 * 1000)) { - printf("tv_add with nano overflow failed.\n"); - tv_print(&v2); - return -1; - } - - ts_init(&s0, 0, 0); - ts_init(&s1, 5, 0); - - ts_diff(&s0, &s1, &s2); - if (!ts_check(&s2, -5, 0)) { - printf("ts_diff failed.\n"); - ts_print(&s2); - return -1; - } - - tv_init(&v0, 0, 0); - tv_init(&v1, 5, 0); - - tv_diff(&v0, &v1, &v2); - if (!tv_check(&v2, -5, 0)) { - printf("tv_diff failed.\n"); - tv_print(&v2); - return -1; - } - - ts_init(&s0, 0, 500 * MILLION); - ts_init(&s1, 0, 600 * MILLION); - - ts_diff(&s0, &s1, &s2); - if (!ts_check(&s2, -1, 900 * MILLION)) { - printf("ts_diff with nano underflow failed.\n"); - ts_print(&s2); - return -1; - } - - tv_init(&v0, 0, 500 * 1000); - tv_init(&v1, 0, 600 * 1000); - - tv_diff(&v0, &v1, &v2); - if (!tv_check(&v2, -1, 900 * 1000)) { - printf("tv_diff with nano underflow failed.\n"); - tv_print(&v2); - return -1; - } - - return 0; -} diff --git a/src/lib/tests/tpm_test.c b/src/lib/tests/tpm_test.c new file mode 100644 index 00000000..7cc049cd --- /dev/null +++ b/src/lib/tests/tpm_test.c @@ -0,0 +1,104 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Tests for the threadpool manager + * + * 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 <ouroboros/tpm.h> + +#include <test/test.h> + +static void * test_func(void * o) +{ + (void) o; + + while(1) + sleep(1); + + return NULL; +} + +static int test_tpm_create_destroy(void) +{ + struct tpm *tpm; + + TEST_START(); + + tpm = tpm_create(2, 2, &test_func, NULL); + if (tpm == NULL) { + printf("Failed to initialize TPM.\n"); + goto fail; + } + + tpm_destroy(tpm); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tpm_start_stop(void * (* fn)(void *), + void * o) +{ + struct tpm *tpm; + + TEST_START(); + + tpm = tpm_create(2, 2, fn, o); + if (tpm == NULL) { + printf("Failed to initialize TPM.\n"); + goto fail_create; + } + + if (tpm_start(tpm) < 0) { + printf("Failed to start TPM.\n"); + goto fail_start; + } + + tpm_stop(tpm); + + tpm_destroy(tpm); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_start: + tpm_destroy(tpm); + fail_create: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int tpm_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_tpm_create_destroy(); + ret |= test_tpm_start_stop(&test_func, NULL); + + return ret; +} diff --git a/src/lib/tests/tw_test.c b/src/lib/tests/tw_test.c new file mode 100644 index 00000000..32c302c4 --- /dev/null +++ b/src/lib/tests/tw_test.c @@ -0,0 +1,663 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Generic timing-wheel 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/. + */ + +#if defined(__linux__) || defined(__CYGWIN__) +#define _DEFAULT_SOURCE +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include "config.h" + +#include <test/test.h> + +#include <ouroboros/time.h> +#include <ouroboros/tw.h> + +#include <stdint.h> +#include <stdio.h> +#include <time.h> + +struct payload { + struct tw_entry tw; + int fired; +}; + +struct cancel_payload { + struct tw_entry tw; + int fired; + struct tw_entry * sibling; +}; + +struct repost_payload { + struct tw_entry tw; + int fired; + struct payload * sibling; + uint64_t repost_at; +}; + +static void cb_count(void * arg) +{ + struct payload * p = arg; + p->fired++; +} + +static void cb_cancel_sibling(void * arg) +{ + struct cancel_payload * p = arg; + p->fired++; + tw_cancel(p->sibling); +} + +static void cb_repost_sibling(void * arg) +{ + struct repost_payload * p = arg; + p->fired++; + tw_post(&p->sibling->tw, p->repost_at, cb_count, p->sibling); +} + +static uint64_t now_ns(void) +{ + struct timespec ts; + clock_gettime(PTHREAD_COND_CLOCK, &ts); + return TS_TO_UINT64(ts); +} + +static void sleep_ns(uint64_t ns) +{ + struct timespec ts; + UINT64_TO_TS(ns, &ts); + nanosleep(&ts, NULL); +} + +static int test_tw_init_fini(void) +{ + TEST_START(); + + if (tw_init() < 0) { + printf("tw_init failed.\n"); + goto fail; + } + + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_post_fires_after_deadline(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 5 * MILLION, cb_count, &p); + + sleep_ns(20 * MILLION); + tw_move(); + + if (p.fired != 1) { + printf("expected 1 fire, got %d\n", p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_no_fire_before_deadline(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 100 * MILLION, cb_count, &p); + + sleep_ns(2 * MILLION); + tw_move(); + + if (p.fired != 0) { + printf("expected 0 fires, got %d\n", p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_cancel_prevents_fire(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 5 * MILLION, cb_count, &p); + tw_cancel(&p.tw); + + sleep_ns(20 * MILLION); + tw_move(); + + if (p.fired != 0) { + printf("cancelled entry fired %d times\n", p.fired); + goto fail_init; + } + + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_init: + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_cancel_unposted_is_noop(void) +{ + struct tw_entry e; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&e); + tw_cancel(&e); + tw_cancel(&e); + + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_fire_only_once(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p); + + sleep_ns(20 * MILLION); + tw_move(); + tw_move(); + tw_move(); + + if (p.fired != 1) { + printf("expected 1 fire, got %d after 3 moves\n", p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Multi-level: post a level-1 (>= 256ms) deadline; should still fire. */ +static int test_tw_post_level1_fires(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 300 * MILLION, cb_count, &p); + + if (p.tw.lvl != 1) { + printf("expected level 1 placement, got %zu\n", p.tw.lvl); + goto fail_post; + } + + sleep_ns(320 * MILLION); + tw_move(); + + if (p.fired != 1) { + printf("level-1 entry didn't fire (got %d)\n", p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_tw_many_entries_all_fire(void) +{ + struct payload pl[16]; + size_t i; + size_t total = 0; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + for (i = 0; i < 16; ++i) { + tw_init_entry(&pl[i].tw); + pl[i].fired = 0; + tw_post(&pl[i].tw, now_ns() + (1 + i) * MILLION, + cb_count, &pl[i]); + } + + sleep_ns(40 * MILLION); + tw_move(); + + for (i = 0; i < 16; ++i) + total += pl[i].fired; + + if (total != 16) { + printf("expected 16 fires, got %zu\n", total); + goto fail_post; + } + + for (i = 0; i < 16; ++i) + tw_cancel(&pl[i].tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + for (i = 0; i < 16; ++i) + tw_cancel(&pl[i].tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* tw_next_expiry signals empty wheel via tv_nsec == -1. */ +static int test_tw_next_expiry_empty(void) +{ + struct timespec out; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_next_expiry(&out); + if (out.tv_nsec != -1) { + printf("expected tv_nsec=-1, got %ld\n", (long) out.tv_nsec); + goto fail_init; + } + + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_init: + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* tw_next_expiry returns a deadline within the right ballpark. */ +static int test_tw_next_expiry_returns_deadline(void) +{ + struct payload p; + struct timespec out; + uint64_t target; + uint64_t out_ns; + int64_t skew; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + target = now_ns() + 50 * MILLION; + tw_post(&p.tw, target, cb_count, &p); + + tw_next_expiry(&out); + out_ns = TS_TO_UINT64(out); + + /* Level-0 quantization gives ±1 slot of skew. */ + skew = (int64_t)(out_ns) - (int64_t)(target); + if (skew < -2 * MILLION || skew > 4 * MILLION) { + printf("deadline not in -2..+4 ms, skew=%ld ns\n", (long) skew); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Repost: fire, then post again. */ +static int test_tw_repost_after_fire(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p); + sleep_ns(20 * MILLION); + tw_move(); + if (p.fired != 1) { + printf("first fire missed\n"); + goto fail_post; + } + + tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p); + sleep_ns(20 * MILLION); + tw_move(); + if (p.fired != 2) { + printf("second fire missed (fired=%d)\n", p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Double-post replaces the schedule; only the second fires. */ +static int test_tw_double_post_replaces(void) +{ + struct payload p; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&p.tw); + p.fired = 0; + + tw_post(&p.tw, now_ns() + 30 * MILLION, cb_count, &p); + tw_post(&p.tw, now_ns() + 3 * MILLION, cb_count, &p); + + sleep_ns(20 * MILLION); + tw_move(); + + if (p.fired != 1) { + printf("expected 1 fire after replace, got %d\n", p.fired); + goto fail_post; + } + + sleep_ns(40 * MILLION); + tw_move(); + + if (p.fired != 1) { + printf("first schedule fired after replace (got %d)\n", + p.fired); + goto fail_post; + } + + tw_cancel(&p.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&p.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Fire callback may safely cancel a sibling in the same slot. */ +static int test_tw_fire_cancels_sibling(void) +{ + struct cancel_payload a; + struct payload b; + uint64_t deadline; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&a.tw); + tw_init_entry(&b.tw); + a.fired = 0; + a.sibling = &b.tw; + b.fired = 0; + + deadline = now_ns() + 3 * MILLION; + tw_post(&a.tw, deadline, cb_cancel_sibling, &a); + tw_post(&b.tw, deadline, cb_count, &b); + + sleep_ns(20 * MILLION); + tw_move(); + + if (a.fired != 1) { + printf("a expected 1 fire, got %d\n", a.fired); + goto fail_post; + } + if (b.fired != 0) { + printf("b should not have fired (got %d)\n", b.fired); + goto fail_post; + } + + tw_cancel(&a.tw); + tw_cancel(&b.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&a.tw); + tw_cancel(&b.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Fire callback may safely repost a sibling to a future slot. */ +static int test_tw_fire_posts_sibling(void) +{ + struct repost_payload a; + struct payload b; + uint64_t deadline; + + TEST_START(); + + if (tw_init() < 0) + goto fail; + + tw_init_entry(&a.tw); + tw_init_entry(&b.tw); + a.fired = 0; + a.sibling = &b; + a.repost_at = now_ns() + 30 * MILLION; + b.fired = 0; + + deadline = now_ns() + 3 * MILLION; + tw_post(&a.tw, deadline, cb_repost_sibling, &a); + tw_post(&b.tw, deadline, cb_count, &b); + + sleep_ns(20 * MILLION); + tw_move(); + + if (a.fired != 1) { + printf("a expected 1 fire, got %d\n", a.fired); + goto fail_post; + } + if (b.fired != 0) { + printf("b fired before reposted deadline (got %d)\n", + b.fired); + goto fail_post; + } + + sleep_ns(25 * MILLION); + tw_move(); + + if (b.fired != 1) { + printf("b expected 1 fire after repost, got %d\n", + b.fired); + goto fail_post; + } + + tw_cancel(&a.tw); + tw_cancel(&b.tw); + tw_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_post: + tw_cancel(&a.tw); + tw_cancel(&b.tw); + tw_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int tw_test(int argc, + char ** argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_tw_init_fini(); + ret |= test_tw_post_fires_after_deadline(); + ret |= test_tw_no_fire_before_deadline(); + ret |= test_tw_cancel_prevents_fire(); + ret |= test_tw_cancel_unposted_is_noop(); + ret |= test_tw_fire_only_once(); + ret |= test_tw_post_level1_fires(); + ret |= test_tw_many_entries_all_fire(); + ret |= test_tw_next_expiry_empty(); + ret |= test_tw_next_expiry_returns_deadline(); + ret |= test_tw_repost_after_fire(); + ret |= test_tw_double_post_replaces(); + ret |= test_tw_fire_cancels_sibling(); + ret |= test_tw_fire_posts_sibling(); + + return ret; +} |
