diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 38eb4af..a822f84 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -118,6 +118,31 @@ if(HAVE_COVERAGE_TOOLS) endif(HAVE_COVERAGE_TOOLS) +# ============================================================================ +# test_create_empty_client_keytab +# ============================================================================ +# Integration test: run the built create-empty-client-keytab binary and verify +# it creates the per-UID keytab (and refuses to clobber it on a second run). +# This binary performs no hardening in its own code path, so instrumenting and +# running it captures create-empty-client-keytab.c main() in the report the +# same way client-keytab-name is captured above. + +add_executable(test_create_empty_client_keytab test_create_empty_client_keytab.c) + +target_include_directories(test_create_empty_client_keytab PRIVATE ${TEST_INCLUDES}) +target_compile_definitions(test_create_empty_client_keytab PRIVATE + "CLIENT_EMPTY_CLIENT_KEYTAB_BIN=\"$\"") +add_coverage_flags(test_create_empty_client_keytab) + +add_test(NAME test_create_empty_client_keytab COMMAND test_create_empty_client_keytab) + +# Instrument the create-empty-client-keytab binary itself so its main() and the +# path/validation helpers it calls are captured by the coverage report. +if(HAVE_COVERAGE_TOOLS) + add_coverage_flags(create-empty-client-keytab) +endif(HAVE_COVERAGE_TOOLS) + + # ============================================================================ # test_kcron_caps # ============================================================================ @@ -217,12 +242,16 @@ set(UNIT_TEST_BINARIES $ $ $ $ - $) + $ + $ + $) add_custom_target(test_binaries DEPENDS test_kcron_filename test_kcron_keytab test_kcron_caps test_kcron_keytab_file test_client_keytab_name + test_create_empty_client_keytab + create-empty-client-keytab COMMENT "Building every unit test binary") if(BUILD_TESTING AND HAVE_COVERAGE_TOOLS) diff --git a/test/test_create_empty_client_keytab.c b/test/test_create_empty_client_keytab.c new file mode 100644 index 0000000..2b7cb16 --- /dev/null +++ b/test/test_create_empty_client_keytab.c @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2026 Fermi Forward Discovery Group + +/** + * test_create_empty_client_keytab.c - Integration test for the + * create-empty-client-keytab(8) binary. + * + * create-empty-client-keytab derives //client.keytab from the + * calling UID, validates that the per-UID directory exists, and creates an + * empty but valid keytab there. It performs no hardening of its own, so the + * built binary can be exercised end to end exactly like client-keytab-name. + * + * The test is environment-adaptive: it works whether or not the per-UID + * directory already exists. + * + * * When the directory exists (or can be created), the first run creates the + * keytab (exit 0, mode 0600) and the second run is refused (exit 1, + * "already exists") - covering the creation path and the O_EXCL refusal. + * * When the directory is absent and cannot be created here (typical for an + * unprivileged user under a root-owned base), the run is refused (exit 1) - + * covering the validate_keytab_directory() rejection path. + * + * The binary path is injected at build time via CLIENT_EMPTY_CLIENT_KEYTAB_BIN. + */ + +#define __PROGRAM_NAME "test_create_empty_client_keytab" + +/* clang-format off */ +#include "autoconf.h" +#include "kcron_filename.h" +/* clang-format on */ + +#include +#include + +#include "test_framework.h" + +#ifndef CLIENT_EMPTY_CLIENT_KEYTAB_BIN +#define CLIENT_EMPTY_CLIENT_KEYTAB_BIN "create-empty-client-keytab" +#endif + +/* + * run_binary - Capture the binary's stdout into @out. + * @out: Destination buffer. + * @n: Size of @out. + * + * Returns 0 if the binary exited successfully, EXIT_FAILURE on refusal, or -1 + * if the binary could not be launched. + */ +static int run_binary(char *out, size_t n) { + FILE *pipe; + char line[FILE_PATH_MAX_LENGTH + 3]; + size_t total = 0; + + pipe = popen(CLIENT_EMPTY_CLIENT_KEYTAB_BIN, "r"); + if (pipe == NULL) { + return -1; + } + + while (fgets(line, sizeof(line), pipe) != NULL) { + size_t linelen = strlen(line); + if (total + linelen >= n) { + break; + } + memcpy(out + total, line, linelen); + total += linelen; + } + out[total] = '\0'; + + return (pclose(pipe) == 0) ? 0 : EXIT_FAILURE; +} + +/* + * create_empty_client_keytab_runs - Drive create-empty-client-keytab end to end. + * + * Covers main(): allocation success, get_filenames(), and either the creation + * success path (validate_keytab_directory + create_keytab_file both return 0) + * with the subsequent "already exists" refusal, or the validate_keytab_directory + * rejection path when the per-UID directory is not available. + */ +TEST(create_empty_client_keytab_runs) { + char keytab_dir[FILE_PATH_MAX_LENGTH + 3]; + char keytab_filename[FILE_PATH_MAX_LENGTH + 3]; + char keytab[FILE_PATH_MAX_LENGTH + 3]; + char out[FILE_PATH_MAX_LENGTH + 3]; + struct stat st; + int rc; + + (void)memset(keytab_dir, 0, sizeof(keytab_dir)); + (void)memset(keytab_filename, 0, sizeof(keytab_filename)); + (void)memset(keytab, 0, sizeof(keytab)); + (void)memset(out, 0, sizeof(out)); + + /* Derive the same path the binary will use. */ + TEST_ASSERT_EQ(get_filenames(keytab_dir, keytab_filename, keytab), 0, "path derivation must succeed"); + + /* Start from a known state so the first run exercises the creation path. */ + (void)unlink(keytab); + + /* + * Ensure the per-UID directory is present. If it already exists we proceed to + * the creation path; if it is missing we try to create it, and if that is not + * permitted we fall back to the rejection assertion below. + */ + if (stat(keytab_dir, &st) != 0) { + if (mkdir(keytab_dir, 0700) != 0) { + /* The per-UID directory does not exist and cannot be created here (the + * base directory is typically not writable by an unprivileged user). The + * binary must reject the missing directory. */ + rc = run_binary(out, sizeof(out)); + TEST_ASSERT_EQ(rc, EXIT_FAILURE, "a missing per-UID directory must be rejected"); + return; + } + } + + /* Per-UID directory present: the first run creates the keytab. */ + rc = run_binary(out, sizeof(out)); + TEST_ASSERT_EQ(rc, 0, "creation must succeed when the per-UID directory exists"); + + /* The keytab must now exist at the derived path, be a regular file, and be + * created mode 0600. */ + TEST_ASSERT_EQ(stat(keytab, &st), 0, "the keytab must be created"); + if (stat(keytab, &st) == 0) { + TEST_ASSERT_EQ((int)(S_ISREG(st.st_mode)), 1, "created entry must be a regular file"); + TEST_ASSERT_EQ((int)(st.st_mode & 07777), 0600, "the keytab must be mode 0600"); + } + + /* A second run must refuse to clobber the now-existing keytab (O_EXCL). */ + rc = run_binary(out, sizeof(out)); + TEST_ASSERT_EQ(rc, EXIT_FAILURE, "recreating an existing keytab must be rejected"); + + /* Clean up so later runs start from the same known state. */ + (void)unlink(keytab); +} + +/* ============================================================================ + * Test Runner + * ============================================================================ */ + +int main(int argc, char **argv) { + int result; + + TEST_INIT(10, false, false); /* timeout, verbose, duration */ + + RUN_TEST(create_empty_client_keytab_runs); + + result = TEST_EXECUTE(); + return result; +} diff --git a/test/test_kcron_filename.c b/test/test_kcron_filename.c index 1c6871f..75f3cb1 100644 --- a/test/test_kcron_filename.c +++ b/test/test_kcron_filename.c @@ -26,6 +26,8 @@ #include #include +#include +#include #include "test_framework.h" @@ -165,6 +167,55 @@ TEST(get_filenames_path_ends_with_filename) { TEST_ASSERT_EQ(strncmp(keytab + (path_len - name_len), keytab_filename, name_len), 0, "path must end with the keytab filename"); } +/* + * expect_exit - Fork, run @fn in the child, and report success in the parent. + * @fn: function to invoke that is expected to call exit() (never returns). + * + * get_client_dirname() and get_filenames() call exit() on their NULL-pointer + * guards. A forked child letting those functions exit would fail the test, so + * each guard runs in a grandchild whose non-zero termination is captured here. + */ +static void expect_exit(void (*fn)(void)) { + pid_t pid = fork(); + if (pid == 0) { + fn(); + /* Unreachable: every guarded path calls exit(). */ + _exit(7); + } + int status = 0; + waitpid(pid, &status, 0); + TEST_ASSERT_NOT_EQ(status, 0, "the guarded helper must terminate via exit()"); +} + +static void thunks_client_dirname_null(void) { + const int rc = get_client_dirname(NULL); + (void)rc; +} +static void thunks_filenames_dir_null(void) { + char name[FILE_PATH_MAX_LENGTH]; + const int rc = get_filenames(NULL, name, name); + (void)rc; +} +static void thunks_filenames_name_null(void) { + char dir[FILE_PATH_MAX_LENGTH]; + const int rc = get_filenames(dir, NULL, NULL); + (void)rc; +} +static void thunks_filenames_path_null(void) { + char dir[FILE_PATH_MAX_LENGTH]; + char name[FILE_PATH_MAX_LENGTH]; + const int rc = get_filenames(dir, name, NULL); + (void)rc; +} + +TEST(client_dirname_null_is_rejected) { expect_exit(thunks_client_dirname_null); } + +TEST(filenames_dir_null_is_rejected) { expect_exit(thunks_filenames_dir_null); } + +TEST(filenames_name_null_is_rejected) { expect_exit(thunks_filenames_name_null); } + +TEST(filenames_path_null_is_rejected) { expect_exit(thunks_filenames_path_null); } + /* ============================================================================ * Test Runner * ============================================================================ @@ -186,6 +237,12 @@ int main(int argc, char **argv) { RUN_TEST(get_filenames_reproducible); RUN_TEST(get_filenames_path_ends_with_filename); + /* get_client_dirname / get_filenames NULL guards (each exits on failure) */ + RUN_TEST(client_dirname_null_is_rejected); + RUN_TEST(filenames_dir_null_is_rejected); + RUN_TEST(filenames_name_null_is_rejected); + RUN_TEST(filenames_path_null_is_rejected); + result = TEST_EXECUTE(); return result; } diff --git a/test/test_kcron_keytab.c b/test/test_kcron_keytab.c index b93952a..e4df72e 100644 --- a/test/test_kcron_keytab.c +++ b/test/test_kcron_keytab.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "test_framework.h" @@ -114,6 +115,51 @@ TEST(write_empty_keytab_writes_only_two_bytes) { TEST_ASSERT_EQ(nread, 2, "file must contain exactly two bytes"); } +/* + * write_empty_keytab() rejects a non-positive or stdio file descriptor before + * writing anything. Those guards call exit(), so each is exercised in a + * grandchild whose non-zero termination is captured here rather than allowed to + * fail the enclosing test. + * + * Note: the descriptor is validated before any write, so passing fd 0/1/2 never + * touches the inherited /dev/null-backed stdout/stderr. + */ +static void expect_exit_write(void (*fn)(void)) { + pid_t pid = fork(); + if (pid == 0) { + fn(); + _exit(7); + } + int status = 0; + waitpid(pid, &status, 0); + TEST_ASSERT_NOT_EQ(status, 0, "write_empty_keytab must reject the descriptor"); +} + +static void guard_neg(void) { + const int rc = write_empty_keytab(-1); + (void)rc; +} +static void guard_stdin(void) { + const int rc = write_empty_keytab(0); + (void)rc; +} +static void guard_stdout(void) { + const int rc = write_empty_keytab(1); + (void)rc; +} +static void guard_stderr(void) { + const int rc = write_empty_keytab(2); + (void)rc; +} + +TEST(write_empty_keytab_neg_descriptor_rejected) { expect_exit_write(guard_neg); } + +TEST(write_empty_keytab_stdin_descriptor_rejected) { expect_exit_write(guard_stdin); } + +TEST(write_empty_keytab_stdout_descriptor_rejected) { expect_exit_write(guard_stdout); } + +TEST(write_empty_keytab_stderr_descriptor_rejected) { expect_exit_write(guard_stderr); } + /* ============================================================================ * Test Runner * ============================================================================ @@ -128,6 +174,12 @@ int main(int argc, char **argv) { RUN_TEST(write_empty_keytab_writes_exact_magic); RUN_TEST(write_empty_keytab_writes_only_two_bytes); + /* write_empty_keytab descriptor guards (each exits on failure) */ + RUN_TEST(write_empty_keytab_neg_descriptor_rejected); + RUN_TEST(write_empty_keytab_stdin_descriptor_rejected); + RUN_TEST(write_empty_keytab_stdout_descriptor_rejected); + RUN_TEST(write_empty_keytab_stderr_descriptor_rejected); + result = TEST_EXECUTE(); return result; } diff --git a/test/test_kcron_keytab_file.c b/test/test_kcron_keytab_file.c index 5970fb4..df12d0a 100644 --- a/test/test_kcron_keytab_file.c +++ b/test/test_kcron_keytab_file.c @@ -289,9 +289,10 @@ TEST(create_keytab_file_existing_directory_rejected) { (void)memset(path, 0, sizeof(path)); (void)snprintf(name, sizeof(name), "client.keytab"); - /* Put a *directory* where the file is expected: openat(O_CREAT | O_EXCL) - * against a directory fails with EISDIR, exercising the non-EEXIST error - * branch instead of only the already-exists path. */ + /* Put a *directory* where the file is expected. With O_CREAT | O_EXCL the + * pre-existing entry yields EEXIST, so this exercises the "already exists" + * message branch (same outcome as create_keytab_file_existing_file_is_rejected + * but through a directory instead of a regular file). */ (void)snprintf(sub, sizeof(sub), "%s/client.keytab", dir); if (mkdir(sub, 0700) != 0) { free(dir); @@ -306,6 +307,32 @@ TEST(create_keytab_file_existing_directory_rejected) { free(dir); } +TEST(create_keytab_file_enotdir_rejected) { + char *dir = make_temp_dir(); + char name[FILE_PATH_MAX_LENGTH]; + char block[FILE_PATH_MAX_LENGTH]; + char path[FILE_PATH_MAX_LENGTH]; + int rc; + + (void)memset(name, 0, sizeof(name)); + (void)memset(block, 0, sizeof(block)); + (void)memset(path, 0, sizeof(path)); + (void)snprintf(name, sizeof(name), "blocker/child"); + + /* A regular file occupies the leading path component, so openat() on + * "blocker/child" fails with ENOTDIR rather than EEXIST, exercising the + * generic error message branch (strerror) rather than the already-exists one. */ + (void)snprintf(block, sizeof(block), "%s/blocker", dir); + make_regular_file(block); + + (void)snprintf(path, sizeof(path), "%s/blocker/child", dir); + rc = create_keytab_file(dir, name, path); + TEST_ASSERT_EQ(rc, 1, "a non-directory path component must be rejected"); + + (void)unlink(block); + free(dir); +} + /* ============================================================================ * Test Runner * ============================================================================ */ @@ -338,6 +365,7 @@ int main(int argc, char **argv) { RUN_TEST(create_keytab_file_existing_file_is_rejected); RUN_TEST(create_keytab_file_unreadable_directory_rejected); RUN_TEST(create_keytab_file_existing_directory_rejected); + RUN_TEST(create_keytab_file_enotdir_rejected); result = TEST_EXECUTE(); return result;