diff --git a/CMakeModules/FindLibPAM.cmake b/CMakeModules/FindLibPAM.cmake index ff78fbe0..acf04758 100644 --- a/CMakeModules/FindLibPAM.cmake +++ b/CMakeModules/FindLibPAM.cmake @@ -62,6 +62,14 @@ else() if(LIBPAM_INCLUDE_DIR AND LIBPAM_LIBRARY) set(LIBPAM_FOUND TRUE) + + # check if the function pam_start_confdir is in pam_appl.h header (added in PAM 1.4) + file(STRINGS ${LIBPAM_INCLUDE_DIR}/security/pam_appl.h PAM_CONFDIR REGEX "pam_start_confdir") + if ("${PAM_CONFDIR}" STREQUAL "") + set(LIBPAM_HAVE_CONFDIR FALSE) + else() + set(LIBPAM_HAVE_CONFDIR TRUE) + endif() else() set(LIBPAM_FOUND FALSE) endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0f9b4d17..965fb6c9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,10 @@ if(ENABLE_SSH_TLS) list(APPEND tests test_auth test_two_channels test_ks_ts test_ec test_ed25519 test_replace test_endpt_share_clients test_tls test_crl test_ch test_runtime_changes test_client_ssh test_client_tls) + + if (LIBPAM_HAVE_CONFDIR) + list(APPEND tests test_pam) + endif() endif() foreach(src IN LISTS libsrc) @@ -23,22 +27,22 @@ foreach(src IN LISTS libsrc) endforeach() add_library(testobj OBJECT ${test_srcs} ${compatsrc}) -# add -Wl,--wrap flags -set(test test_client_ssh) -set(${test}_mock_funcs connect ssh_connect ssh_userauth_none ssh_userauth_kbdint ssh_is_connected +# set the mocked functions for the tests +set(mock_tests test_client_ssh test_client_tls test_pam) +set(test_client_ssh_mock_funcs connect ssh_connect ssh_userauth_none ssh_userauth_kbdint ssh_is_connected ssh_channel_open_session ssh_channel_request_subsystem ssh_channel_is_close ssh_channel_write ssh_channel_poll_timeout ssh_userauth_password nc_handshake_io nc_ctx_check_and_fill ssh_userauth_try_publickey ssh_userauth_publickey nc_sock_listen_inet nc_sock_accept_binds nc_accept_callhome_ssh_sock) -set(${test}_wrap_link_flags "-Wl") -foreach(mock_func IN LISTS ${test}_mock_funcs) - set(${test}_wrap_link_flags "${${test}_wrap_link_flags},--wrap=${mock_func}") -endforeach() +set(test_client_tls_mock_funcs connect SSL_connect nc_send_hello_io nc_handshake_io nc_ctx_check_and_fill) +set(test_pam_mock_funcs pam_start) -set(test test_client_tls) -set(${test}_mock_funcs connect SSL_connect nc_send_hello_io nc_handshake_io nc_ctx_check_and_fill) -set(${test}_wrap_link_flags "-Wl") -foreach(mock_func IN LISTS ${test}_mock_funcs) - set(${test}_wrap_link_flags "${${test}_wrap_link_flags},--wrap=${mock_func}") +# add -Wl,--wrap flags to tests that require it +set(moc_funcs test_client_ssh_mock_funcs test_client_tls_mock_funcs test_pam_mock_funcs) +foreach(mock_test IN LISTS mock_tests) + set(${mock_test}_wrap_link_flags "-Wl") + foreach(mock_func IN LISTS ${mock_test}_mock_funcs) + set(${mock_test}_wrap_link_flags "${${mock_test}_wrap_link_flags},--wrap=${mock_func}") + endforeach() endforeach() foreach(test_name IN LISTS tests) @@ -58,3 +62,16 @@ endif() include_directories(${CMAKE_SOURCE_DIR}/src ${PROJECT_BINARY_DIR}) configure_file("${PROJECT_SOURCE_DIR}/tests/config.h.in" "${PROJECT_BINARY_DIR}/tests/config.h" ESCAPE_QUOTES @ONLY) + +# compile PAM test module +add_library(pam_netconf SHARED ${CMAKE_SOURCE_DIR}/tests/pam/pam_netconf.c) +set_target_properties(pam_netconf PROPERTIES PREFIX "") +target_link_libraries(pam_netconf ${LIBPAM_LIBRARIES}) + +# generate PAM configuration file +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/netconf.conf + "#%PAM-1.4\n" + "auth required ${CMAKE_CURRENT_BINARY_DIR}/pam_netconf.so\n" + "account required ${CMAKE_CURRENT_BINARY_DIR}/pam_netconf.so\n" + "password required ${CMAKE_CURRENT_BINARY_DIR}/pam_netconf.so\n" +) diff --git a/tests/library_valgrind.supp b/tests/library_valgrind.supp index 6206dbab..6b160dd0 100644 --- a/tests/library_valgrind.supp +++ b/tests/library_valgrind.supp @@ -9,21 +9,19 @@ fun:ly_ctx_new } { - CI:test_pam:pam_start_confdir + CI:test_pam:__wrap_pam_start Memcheck:Leak match-leak-kinds: definite fun:malloc ... fun:server_thread fun:start_thread - fun:clone } { - test_pam:pam_start_confdir + test_pam:__wrap_pam_start Memcheck:Leak match-leak-kinds: definite fun:malloc ... fun:server_thread - fun:clone } diff --git a/tests/pam/pam_netconf.c b/tests/pam/pam_netconf.c new file mode 100644 index 00000000..dd30fe07 --- /dev/null +++ b/tests/pam/pam_netconf.c @@ -0,0 +1,311 @@ +/** + * @file pam_netconf.c + * @author Roman Janota + * @brief libnetconf2 Linux PAM test module + * + * @copyright + * Copyright (c) 2022 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#include +#include +#include +#include + +#include "config.h" + +#define N_MESSAGES 2 +#define N_REQUESTS 2 + +/** + * @brief Exchange module's messages for user's replies. + * + * @param[in] pam_h PAM handle. + * @param[in] n_messages Number of messages. + * @param[in] msg Module's messages for the user. + * @param[out] resp User's responses. + * @return PAM_SUCCESS on success; + * @return PAM error otherwise. + */ +static int +nc_pam_mod_call_clb(pam_handle_t *pam_h, int n_messages, const struct pam_message **msg, struct pam_response **resp) +{ + struct pam_conv *conv; + int r; + + /* the callback can be accessed through the handle */ + r = pam_get_item(pam_h, PAM_CONV, (void *) &conv); + if (r != PAM_SUCCESS) { + return r; + } + return conv->conv(n_messages, msg, resp, conv->appdata_ptr); +} + +/** + * @brief Validate the user's responses. + * + * @param[in] username Username. + * @param[in] reversed_username User's response to the first challenge. + * @param[in] eq_ans User's response to the second challenge. + * @return PAM_SUCCESS on success; + * @return PAM_AUTH_ERR whenever the user's replies are incorrect. + */ +static int +nc_pam_mod_auth(const char *username, char *reversed_username, char *eq_ans) +{ + int i, j, r; + size_t len; + char *buffer; + + len = strlen(reversed_username); + buffer = calloc(len + 1, sizeof *buffer); + if (!buffer) { + fprintf(stderr, "Memory allocation error.\n"); + return PAM_BUF_ERR; + } + + /* reverse the user's response */ + for (i = len - 1, j = 0; i >= 0; i--) { + buffer[j++] = reversed_username[i]; + } + buffer[j] = '\0'; + + if (!strcmp(username, buffer) && !strcmp(eq_ans, "2")) { + /* it's a match */ + r = PAM_SUCCESS; + } else { + r = PAM_AUTH_ERR; + } + + free(buffer); + return r; +} + +/** + * @brief Free the user's responses. + * + * @param[in] resp Responses. + * @param[in] n Number of responses to be freed. + */ +static void +nc_pam_mod_resp_free(struct pam_response *resp, int n) +{ + int i; + + if (!resp) { + return; + } + + for (i = 0; i < n; i++) { + free((resp + i)->resp); + } + free(resp); +} + +/** + * @brief Test module's implementation of "auth" service. + * + * Prepare prompts for the client and decide based on his + * answers whether to allow or disallow access. + * + * @param[in] pam_h PAM handle. + * @param[in] flags Flags. + * @param[in] argc Count of module options defined in the PAM configuration file. + * @param[in] argv Module options. + * @return PAM_SUCCESS on success; + * @return PAM error otherwise. + */ +API int +pam_sm_authenticate(pam_handle_t *pam_h, int flags, int argc, const char **argv) +{ + int r; + const char *username; + char *reversed_username = NULL, *eq_ans = NULL; + struct pam_message echo_msg, no_echo_msg, unexpected_type_msg, info_msg, err_msg; + const struct pam_message *msg[N_MESSAGES]; + struct pam_response *resp = NULL; + + (void) flags; + (void) argc; + (void) argv; + + /* get the username and if it's not known then the user will be prompted to enter it */ + r = pam_get_user(pam_h, &username, NULL); + if (r != PAM_SUCCESS) { + fprintf(stderr, "Unable to get username.\n"); + r = PAM_AUTHINFO_UNAVAIL; + goto cleanup; + } + + /* prepare the messages */ + echo_msg.msg_style = PAM_PROMPT_ECHO_ON; + echo_msg.msg = "Enter your username backwards: "; + no_echo_msg.msg_style = PAM_PROMPT_ECHO_OFF; + no_echo_msg.msg = "Enter the result to 1+1: "; + unexpected_type_msg.msg_style = PAM_AUTH_ERR; + unexpected_type_msg.msg = "Arbitrary test message"; + info_msg.msg_style = PAM_TEXT_INFO; + info_msg.msg = "Test info message"; + err_msg.msg_style = PAM_ERROR_MSG; + err_msg.msg = "Test error message"; + + /* tests */ + printf("[TEST #1] Too many PAM messages. Output:\n"); + r = nc_pam_mod_call_clb(pam_h, 500, msg, &resp); + if (r == PAM_SUCCESS) { + fprintf(stderr, "[TEST #1] Failed.\n"); + r = PAM_AUTH_ERR; + goto cleanup; + } + printf("[TEST #1] Passed.\n\n"); + + printf("[TEST #2] Negative number of PAM messages. Output:\n"); + r = nc_pam_mod_call_clb(pam_h, -1, msg, &resp); + if (r == PAM_SUCCESS) { + fprintf(stderr, "[TEST #2] Failed.\n"); + r = PAM_AUTH_ERR; + goto cleanup; + } + printf("[TEST #2] Passed.\n\n"); + + printf("[TEST #3] 0 PAM messages. Output:\n"); + r = nc_pam_mod_call_clb(pam_h, 0, msg, &resp); + if (r == PAM_SUCCESS) { + fprintf(stderr, "[TEST #3] Failed.\n"); + r = PAM_AUTH_ERR; + goto cleanup; + } + printf("[TEST #3] Passed.\n\n"); + + printf("[TEST #4] Unexpected message type. Output:\n"); + msg[0] = &unexpected_type_msg; + r = nc_pam_mod_call_clb(pam_h, N_MESSAGES, msg, &resp); + if (r == PAM_SUCCESS) { + fprintf(stderr, "[TEST #4] Failed.\n"); + r = PAM_AUTH_ERR; + goto cleanup; + } + printf("[TEST #4] Passed.\n\n"); + + printf("[TEST #5] Info and error messages. Output:\n"); + msg[0] = &info_msg; + msg[1] = &err_msg; + r = nc_pam_mod_call_clb(pam_h, N_MESSAGES, msg, &resp); + if (r == PAM_SUCCESS) { + fprintf(stderr, "[TEST #5] Failed.\n"); + r = PAM_AUTH_ERR; + goto cleanup; + } + printf("[TEST #5] Passed.\n\n"); + + printf("[TEST #6] Authentication attempt with an expired token. Output:\n"); + /* store the correct messages */ + msg[0] = &echo_msg; + msg[1] = &no_echo_msg; + + /* get responses */ + r = nc_pam_mod_call_clb(pam_h, N_MESSAGES, msg, &resp); + if (r != PAM_SUCCESS) { + fprintf(stderr, "[TEST #6] Failed.\n"); + goto cleanup; + } + + reversed_username = resp[0].resp; + eq_ans = resp[1].resp; + + /* validate the responses */ + r = nc_pam_mod_auth(username, reversed_username, eq_ans); + + /* not authenticated */ + if (r != PAM_SUCCESS) { + fprintf(stderr, "[TEST #6] Failed.\n"); + r = PAM_AUTH_ERR; + } + +cleanup: + /* free the responses */ + nc_pam_mod_resp_free(resp, N_REQUESTS); + return r; +} + +/** + * @brief Test module's silly implementation of "account" service. + * + * @param[in] pam_h PAM handle. + * @param[in] flags Flags. + * @param[in] argc The count of module options defined in the PAM configuration file. + * @param[in] argv Module options. + * @return PAM_NEW_AUTHTOK_REQD on success; + * @return PAM error otherwise. + */ +API int +pam_sm_acct_mgmt(pam_handle_t *pam_h, int flags, int argc, const char *argv[]) +{ + int r; + const void *username; + + (void) flags; + (void) argc; + (void) argv; + + /* get and check the username */ + r = pam_get_item(pam_h, PAM_USER, &username); + if (r != PAM_SUCCESS) { + return r; + } + if (!strcmp((const char *)username, "test")) { + return PAM_NEW_AUTHTOK_REQD; + } + return PAM_SYSTEM_ERR; +} + +/** + * @brief Test module's silly implementation of "password" service. + * + * @param[in] pam_h PAM handle. + * @param[in] flags Flags. + * @param[in] argc The count of module options defined in the PAM configuration file. + * @param[in] argv Module options. + * @return PAM_SUCCESS on success; + * @return PAM error otherwise. + */ +API int +pam_sm_chauthtok(pam_handle_t *pam_h, int flags, int argc, const char *argv[]) +{ + int r; + const void *username; + + (void) argc; + (void) argv; + + /* the function is called twice, each time with a different flag, + * in the first call just check the username if it matches */ + if (flags & PAM_PRELIM_CHECK) { + r = pam_get_item(pam_h, PAM_USER, &username); + if (r != PAM_SUCCESS) { + return r; + } + if (!strcmp((const char *)username, "test")) { + return PAM_SUCCESS; + } else { + return PAM_SYSTEM_ERR; + } + + /* change the authentication token in the second call */ + } else if (flags & PAM_UPDATE_AUTHTOK) { + r = pam_set_item(pam_h, PAM_AUTHTOK, "test"); + if (r == PAM_SUCCESS) { + printf("[TEST #6] Passed.\n\n"); + } else { + fprintf(stderr, "[TEST #6] Failed.\n"); + } + return r; + } + return PAM_SYSTEM_ERR; +} diff --git a/tests/test_pam.c b/tests/test_pam.c new file mode 100644 index 00000000..a257a01a --- /dev/null +++ b/tests/test_pam.c @@ -0,0 +1,222 @@ +/** + * @file test_pam.c + * @author Roman Janota + * @brief libnetconf2 SSH Keyboard Interactive auth using PAM test + * + * @copyright + * Copyright (c) 2023 CESNET, z.s.p.o. + * + * This source code is licensed under BSD 3-Clause License (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "tests/config.h" + +#define NC_ACCEPT_TIMEOUT 2000 +#define NC_PS_POLL_TIMEOUT 2000 + +struct ly_ctx *ctx; + +struct test_state { + pthread_barrier_t barrier; +}; + +/* mock pam_start to just call pam_start_confdir instead */ +int __real_pam_start(const char *service_name, const char *user, const struct pam_conv *pam_conversation, pam_handle_t **pamh); +int +__wrap_pam_start(const char *service_name, const char *user, const struct pam_conv *pam_conversation, pam_handle_t **pamh) +{ + return pam_start_confdir(service_name, user, pam_conversation, BUILD_DIR "/tests", pamh); +} + +static void * +server_thread(void *arg) +{ + int ret; + NC_MSG_TYPE msgtype; + struct nc_session *session; + struct nc_pollsession *ps; + struct test_state *state = arg; + + ps = nc_ps_new(); + assert_non_null(ps); + + /* accept a session and add it to the poll session structure */ + pthread_barrier_wait(&state->barrier); + msgtype = nc_accept(NC_ACCEPT_TIMEOUT, ctx, &session); + assert_int_equal(msgtype, NC_MSG_HELLO); + + ret = nc_ps_add_session(ps, session); + assert_int_equal(ret, 0); + + do { + ret = nc_ps_poll(ps, NC_PS_POLL_TIMEOUT, NULL); + assert_int_equal(ret & NC_PSPOLL_RPC, NC_PSPOLL_RPC); + } while (!(ret & NC_PSPOLL_SESSION_TERM)); + + nc_ps_clear(ps, 1, NULL); + nc_ps_free(ps); + return NULL; +} + +static char * +auth_interactive(const char *UNUSED(auth_name), const char *UNUSED(instruction), + const char *prompt, int UNUSED(echo), void *UNUSED(priv)) +{ + /* send the replies to keyboard-interactive authentication */ + if (strstr(prompt, "backwards")) { + return strdup("tset"); + } else if (strstr(prompt, "1+1")) { + return strdup("2"); + } else { + return NULL; + } +} + +static void * +client_thread(void *arg) +{ + int ret; + struct nc_session *session = NULL; + struct test_state *state = arg; + + /* skip all hostkey and known_hosts checks */ + nc_client_ssh_set_knownhosts_mode(NC_SSH_KNOWNHOSTS_SKIP); + + ret = nc_client_set_schema_searchpath(MODULES_DIR); + assert_int_equal(ret, 0); + + ret = nc_client_ssh_set_username("test"); + assert_int_equal(ret, 0); + + /* set keyboard-interactive authentication callback */ + nc_client_ssh_set_auth_interactive_clb(auth_interactive, NULL); + + pthread_barrier_wait(&state->barrier); + session = nc_connect_ssh("127.0.0.1", 10005, NULL); + assert_non_null(session); + + nc_session_free(session, NULL); + return NULL; +} + +static void +test_nc_pam(void **state) +{ + int ret, i; + pthread_t tids[2]; + + assert_non_null(state); + + ret = pthread_create(&tids[0], NULL, client_thread, *state); + assert_int_equal(ret, 0); + ret = pthread_create(&tids[1], NULL, server_thread, *state); + assert_int_equal(ret, 0); + + for (i = 0; i < 2; i++) { + pthread_join(tids[i], NULL); + } +} + +static int +setup_f(void **state) +{ + int ret; + struct lyd_node *tree = NULL; + struct test_state *test_state; + + nc_verbosity(NC_VERB_VERBOSE); + + /* init barrier */ + test_state = malloc(sizeof *test_state); + assert_non_null(test_state); + + ret = pthread_barrier_init(&test_state->barrier, NULL, 2); + assert_int_equal(ret, 0); + + *state = test_state; + + ret = ly_ctx_new(MODULES_DIR, 0, &ctx); + assert_int_equal(ret, 0); + + ret = nc_server_init_ctx(&ctx); + assert_int_equal(ret, 0); + + ret = nc_server_config_load_modules(&ctx); + assert_int_equal(ret, 0); + + ret = nc_server_config_add_address_port(ctx, "endpt", NC_TI_LIBSSH, "127.0.0.1", 10005, &tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_add_ssh_hostkey(ctx, "endpt", "hostkey", TESTS_DIR "/data/key_ecdsa", NULL, &tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_add_ssh_user_interactive(ctx, "endpt", "test", &tree); + assert_int_equal(ret, 0); + + ret = nc_server_ssh_set_pam_conf_filename("netconf.conf"); + assert_int_equal(ret, 0); + + /* configure the server based on the data */ + ret = nc_server_config_setup_data(tree); + assert_int_equal(ret, 0); + + ret = nc_server_init(); + assert_int_equal(ret, 0); + + /* initialize client */ + ret = nc_client_init(); + assert_int_equal(ret, 0); + + lyd_free_all(tree); + + return 0; +} + +static int +teardown_f(void **state) +{ + int ret = 0; + struct test_state *test_state; + + assert_non_null(state); + test_state = *state; + + ret = pthread_barrier_destroy(&test_state->barrier); + assert_int_equal(ret, 0); + + free(*state); + nc_client_destroy(); + nc_server_destroy(); + ly_ctx_destroy(ctx); + + return 0; +} + +int +main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_nc_pam, setup_f, teardown_f) + }; + + setenv("CMOCKA_TEST_ABORT", "1", 1); + return cmocka_run_group_tests(tests, NULL, NULL); +}