From fd5770973f17f884317e70fddd743bf718227af0 Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Mon, 14 Jan 2019 17:37:10 +0100 Subject: tests: Add test for server interactive authentication Signed-off-by: Anderson Toshiyuki Sasaki --- tests/server/CMakeLists.txt | 1 + tests/server/torture_server_auth_kbdint.c | 763 ++++++++++++++++++++++++++++++ 2 files changed, 764 insertions(+) create mode 100644 tests/server/torture_server_auth_kbdint.c diff --git a/tests/server/CMakeLists.txt b/tests/server/CMakeLists.txt index 9430fd23..68f0ace2 100644 --- a/tests/server/CMakeLists.txt +++ b/tests/server/CMakeLists.txt @@ -8,6 +8,7 @@ add_subdirectory(test_server) set(LIBSSH_SERVER_TESTS torture_server + torture_server_auth_kbdint ) include_directories( diff --git a/tests/server/torture_server_auth_kbdint.c b/tests/server/torture_server_auth_kbdint.c new file mode 100644 index 00000000..829a8715 --- /dev/null +++ b/tests/server/torture_server_auth_kbdint.c @@ -0,0 +1,763 @@ +/* + * This file is part of the SSH Library + * + * Copyright (c) 2019 by Red Hat, Inc. + * + * Author: Anderson Toshiyuki Sasaki + * + * The SSH Library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or (at your + * option) any later version. + * + * The SSH Library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the SSH Library; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, + * MA 02111-1307, USA. + */ + +#include "config.h" + +#define LIBSSH_STATIC + +#include +#include +#include +#include + +#include "torture.h" +#include "torture_key.h" +#include "libssh/libssh.h" +#include "libssh/priv.h" +#include "libssh/session.h" + +#include +#include +#include + +#include "test_server.h" +#include "default_cb.h" + +#define TORTURE_KNOWN_HOSTS_FILE "libssh_torture_knownhosts" + +enum { + SUCCESS, + MORE, + FAILED +}; + +struct test_server_st { + struct torture_state *state; + struct server_state_st *ss; +}; + +#ifdef WITH_PCAP +static void set_pcap(struct session_data_st *sdata, + ssh_session session, + char *pcap_file) +{ + int rc = 0; + + if (sdata == NULL) { + return; + } + + if (pcap_file == NULL) { + return; + } + + sdata->pcap = ssh_pcap_file_new(); + if (sdata->pcap == NULL) { + return; + } + + rc = ssh_pcap_file_open(sdata->pcap, pcap_file); + if (rc == SSH_ERROR) { + fprintf(stderr, "Error opening pcap file\n"); + ssh_pcap_file_free(sdata->pcap); + sdata->pcap = NULL; + return; + } + ssh_set_pcap_file(session, sdata->pcap); +} + +static void cleanup_pcap(struct session_data_st *sdata) +{ + if (sdata == NULL) { + return; + } + + if (sdata->pcap == NULL) { + return; + } + + ssh_pcap_file_free(sdata->pcap); + sdata->pcap = NULL; +} +#endif + +static int process_stdout(socket_t fd, int revents, void *userdata) +{ + char buf[BUF_SIZE]; + int n = -1; + ssh_channel channel = (ssh_channel) userdata; + + if (channel != NULL && (revents & POLLIN) != 0) { + n = read(fd, buf, BUF_SIZE); + if (n > 0) { + ssh_channel_write(channel, buf, n); + } + } + + return n; +} + +static int process_stderr(socket_t fd, int revents, void *userdata) +{ + char buf[BUF_SIZE]; + int n = -1; + ssh_channel channel = (ssh_channel) userdata; + + if (channel != NULL && (revents & POLLIN) != 0) { + n = read(fd, buf, BUF_SIZE); + if (n > 0) { + ssh_channel_write_stderr(channel, buf, n); + } + } + + return n; +} + +static int authenticate_kbdint(ssh_session session, + ssh_message message, + void *userdata) +{ + int rc = 0; + int count; + int *step = NULL; + size_t expected_len; + + const char instruction[] = "Type the requested data"; + const char name[] = "Keyboard-Interactive Authentication\n"; + char initial_echo[] = {1, 0}; + char retype_echo[] = {0}; + const char *initial_prompt[2]; + const char *retype_prompt[1]; + int cmp; + + const char *answer; + + struct session_data_st *sdata = (struct session_data_st *)userdata; + + initial_prompt[0] = "username: "; + initial_prompt[1] = "password: "; + + /* Prompt for aditional prompts */ + retype_prompt[0] = "retype password: "; + + if ((session == NULL) || (message == NULL) || (sdata == NULL)) { + fprintf(stderr, "Null argument provided\n"); + goto failed; + } + + if (sdata->extra_data == NULL) { + goto failed; + } + + step = (int *)sdata->extra_data; + + switch (*step) { + case 0: + ssh_message_auth_interactive_request(message, name, instruction, 2, + initial_prompt, initial_echo); + rc = MORE; + goto end; + case 1: + count = ssh_userauth_kbdint_getnanswers(session); + if (count != 2) { + goto failed; + } + + if ((sdata->username == NULL) || (sdata->password == NULL)) { + goto failed; + } + + /* Get and compare username */ + expected_len = strlen(sdata->username); + if (expected_len <= 0) { + goto failed; + } + + answer = ssh_userauth_kbdint_getanswer(session, 0); + if (answer == NULL) { + goto failed; + } + + cmp = strncmp(answer, sdata->username, expected_len); + if (cmp != 0) { + goto failed; + } + + /* Get and compare password */ + expected_len = strlen(sdata->password); + if (expected_len <= 0) { + goto failed; + } + + answer = ssh_userauth_kbdint_getanswer(session, 1); + if (answer == NULL) { + goto failed; + } + + cmp = strncmp(answer, sdata->password, expected_len); + if (cmp != 0) { + goto failed; + } + + /* Username and password matched. Ask for a retype. */ + ssh_message_auth_interactive_request(message, + name, + instruction, + 1, + retype_prompt, + retype_echo); + + rc = MORE; + goto end; + case 2: + /* Get and compare password */ + expected_len = strlen(sdata->password); + if (expected_len <= 0) { + goto failed; + } + + answer = ssh_userauth_kbdint_getanswer(session, 0); + if (answer == NULL) { + goto failed; + } + + cmp = strncmp(answer, sdata->password, expected_len); + if (cmp != 0) { + goto failed; + } + + /* Password was correct, authenticated */ + rc = SUCCESS; + goto end; + default: + goto failed; + } + +failed: + if (step != NULL) { + *step = 0; + } + return FAILED; + +end: + if (step != NULL) { + (*step)++; + } + return rc; +} + +static int authenticate_callback(ssh_session session, + ssh_message message, + void *userdata) +{ + struct session_data_st *sdata = (struct session_data_st *)userdata; + int rc; + + if (sdata == NULL) { + fprintf(stderr, "Null userdata\n"); + goto denied; + } + + if (sdata->extra_data == NULL) { + sdata->extra_data = (void *)calloc(1, sizeof(int)); + } + + switch (ssh_message_type(message)) { + case SSH_REQUEST_AUTH: + switch (ssh_message_subtype(message)) { + case SSH_AUTH_METHOD_INTERACTIVE: + rc = authenticate_kbdint(session, message, (void *)sdata); + if (rc == SUCCESS) { + goto accept; + } + else if (rc == MORE) { + goto more; + } + ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_INTERACTIVE); + goto denied; + default: + ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_INTERACTIVE); + goto denied; + } + default: + ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_INTERACTIVE); + goto denied; + } + + ssh_message_free(message); + +accept: + if (sdata) { + if (sdata->extra_data) { + free(sdata->extra_data); + sdata->extra_data = NULL; + } + } + ssh_message_auth_reply_success (message, 0); +more: + return 0; +denied: + if (sdata) { + if (sdata->extra_data) { + free(sdata->extra_data); + sdata->extra_data = NULL; + } + } + return 1; +} + +static void handle_kbdint_session_cb(ssh_event event, + ssh_session session, + struct server_state_st *state) +{ + int n; + int rc = 0; + + /* Structure for storing the pty size. */ + struct winsize wsize = { + .ws_row = 0, + .ws_col = 0, + .ws_xpixel = 0, + .ws_ypixel = 0 + }; + + /* Our struct holding information about the channel. */ + struct channel_data_st cdata = { + .pid = 0, + .pty_master = -1, + .pty_slave = -1, + .child_stdin = -1, + .child_stdout = -1, + .child_stderr = -1, + .event = NULL, + .winsize = &wsize + }; + + /* Our struct holding information about the session. */ + struct session_data_st sdata = { + .channel = NULL, + .auth_attempts = 0, + .authenticated = 0, + .username = TORTURE_SSH_USER_BOB, + .password = TORTURE_SSH_USER_BOB_PASSWORD + }; + + struct ssh_channel_callbacks_struct *channel_cb = NULL; + struct ssh_server_callbacks_struct *server_cb = NULL; + + if (state == NULL) { + fprintf(stderr, "NULL server state provided\n"); + goto end; + } + + server_cb = get_default_server_cb(); + if (server_cb == NULL) { + goto end; + } + + server_cb->userdata = &sdata; + + /* This is a macro, it does not return a value */ + ssh_callbacks_init(server_cb); + + rc = ssh_set_server_callbacks(session, server_cb); + if (rc) { + goto end; + } + +#ifdef WITH_PCAP + set_pcap(&sdata, session, state->pcap_file); +#endif + + rc = ssh_handle_key_exchange(session); + if (rc != SSH_OK) { + fprintf(stderr, "%s\n", ssh_get_error(session)); + goto end; + } + + /* Set the supported authentication methods */ + ssh_set_auth_methods(session, SSH_AUTH_METHOD_INTERACTIVE); + + ssh_set_message_callback(session, authenticate_callback, &sdata); + + rc = ssh_event_add_session(event, session); + if (rc != 0) { + fprintf(stderr, "Error adding session to event\n"); + goto end; + } + + n = 0; + while (sdata.authenticated == 0 || sdata.channel == NULL) { + /* If the user has used up all attempts, or if he hasn't been able to + * authenticate in 10 seconds (n * 100ms), disconnect. */ + if (sdata.auth_attempts >= state->max_tries || n >= 100) { + goto end; + } + + if (ssh_event_dopoll(event, 100) == SSH_ERROR) { + fprintf(stderr, "do_poll error: %s\n", ssh_get_error(session)); + goto end; + } + n++; + } + + channel_cb = get_default_channel_cb(); + if (channel_cb == NULL) { + goto end; + } + + channel_cb->userdata = &cdata; + + ssh_callbacks_init(channel_cb); + rc = ssh_set_channel_callbacks(sdata.channel, channel_cb); + if (rc != 0) { + goto end; + } + + do { + /* Poll the main event which takes care of the session, the channel and + * even our child process's stdout/stderr (once it's started). */ + rc = ssh_event_dopoll(event, -1); + if (rc == SSH_ERROR) { + ssh_channel_close(sdata.channel); + } + + /* If child process's stdout/stderr has been registered with the event, + * or the child process hasn't started yet, continue. */ + if (cdata.event != NULL || cdata.pid == 0) { + continue; + } + /* Executed only once, once the child process starts. */ + cdata.event = event; + /* If stdout valid, add stdout to be monitored by the poll event. */ + if (cdata.child_stdout != -1) { + if (ssh_event_add_fd(event, cdata.child_stdout, POLLIN, process_stdout, + sdata.channel) != SSH_OK) { + fprintf(stderr, "Failed to register stdout to poll context\n"); + ssh_channel_close(sdata.channel); + } + } + + /* If stderr valid, add stderr to be monitored by the poll event. */ + if (cdata.child_stderr != -1){ + if (ssh_event_add_fd(event, cdata.child_stderr, POLLIN, process_stderr, + sdata.channel) != SSH_OK) { + fprintf(stderr, "Failed to register stderr to poll context\n"); + ssh_channel_close(sdata.channel); + } + } + } while(ssh_channel_is_open(sdata.channel) && + (cdata.pid == 0 || waitpid(cdata.pid, &rc, WNOHANG) == 0)); + + close(cdata.pty_master); + close(cdata.child_stdin); + close(cdata.child_stdout); + close(cdata.child_stderr); + + /* Remove the descriptors from the polling context, since they are now + * closed, they will always trigger during the poll calls. */ + ssh_event_remove_fd(event, cdata.child_stdout); + ssh_event_remove_fd(event, cdata.child_stderr); + + /* If the child process exited. */ + if (kill(cdata.pid, 0) < 0 && WIFEXITED(rc)) { + rc = WEXITSTATUS(rc); + ssh_channel_request_send_exit_status(sdata.channel, rc); + /* If client terminated the channel or the process did not exit nicely, + * but only if something has been forked. */ + } else if (cdata.pid > 0) { + kill(cdata.pid, SIGKILL); + } + + ssh_channel_send_eof(sdata.channel); + ssh_channel_close(sdata.channel); + + /* Wait up to 5 seconds for the client to terminate the session. */ + for (n = 0; n < 50 && (ssh_get_status(session) & SESSION_END) == 0; n++) { + ssh_event_dopoll(event, 100); + } + +end: +#ifdef WITH_PCAP + cleanup_pcap(&sdata); +#endif + if (channel_cb != NULL) { + free(channel_cb); + } + if (server_cb != NULL) { + free(server_cb); + } + return; +} + +static int setup_kbdint_server(void **state) +{ + struct torture_state *s; + struct server_state_st *ss; + struct test_server_st *tss; + + char ed25519_hostkey[1024] = {0}; + + char sshd_path[1024]; + + int rc; + + char pid_str[1024]; + + pid_t pid; + + assert_non_null(state); + + tss = (struct test_server_st*)calloc(1, sizeof(struct test_server_st)); + assert_non_null(tss); + + torture_setup_socket_dir((void **)&s); + assert_non_null(s->socket_dir); + + /* Set the default interface for the server */ + setenv("SOCKET_WRAPPER_DEFAULT_IFACE", "10", 1); + setenv("PAM_WRAPPER", "1", 1); + + snprintf(sshd_path, + sizeof(sshd_path), + "%s/sshd", + s->socket_dir); + + rc = mkdir(sshd_path, 0755); + assert_return_code(rc, errno); + + snprintf(ed25519_hostkey, + sizeof(ed25519_hostkey), + "%s/sshd/ssh_host_ed25519_key", + s->socket_dir); + torture_write_file(ed25519_hostkey, + torture_get_openssh_testkey(SSH_KEYTYPE_ED25519, 0, 0)); + + /* Create the server state */ + ss = (struct server_state_st *)calloc(1, sizeof(struct server_state_st)); + assert_non_null(ss); + + ss->address = strdup("127.0.0.10"); + assert_non_null(ss->address); + + ss->port = 22; + + ss->host_key = strdup(ed25519_hostkey); + assert_non_null(ed25519_hostkey); + + ss->verbosity = torture_libssh_verbosity(); + +#ifdef WITH_PCAP + ss->with_pcap = 1; + ss->pcap_file = strdup(s->pcap_file); + assert_non_null(ss->pcap_file); +#endif + + ss->max_tries = 3; + ss->error = 0; + + /* Set the session handling function */ + ss->handle_session = handle_kbdint_session_cb; + assert_non_null(ss->handle_session); + + /* Start the server */ + pid = fork_run_server(ss); + if (pid < 0) { + fail(); + } + + snprintf(pid_str, sizeof(pid_str), "%d", pid); + + torture_write_file(s->srv_pidfile, (const char *)pid_str); + + setenv("SOCKET_WRAPPER_DEFAULT_IFACE", "21", 1); + unsetenv("PAM_WRAPPER"); + + /* Wait 200ms */ + usleep(200 * 1000); + + tss->state = s; + tss->ss = ss; + + *state = tss; + + return 0; +} + +static int teardown_kbdint_server(void **state) +{ + struct torture_state *s; + struct server_state_st *ss; + struct test_server_st *tss; + + tss = *state; + assert_non_null(tss); + + s = tss->state; + assert_non_null(s); + + ss = tss->ss; + assert_non_null(ss); + + /* This function can be reused */ + torture_teardown_sshd_server((void **)&s); + + free_server_state(tss->ss); + SAFE_FREE(tss->ss); + SAFE_FREE(tss); + + return 0; +} + +static int session_setup(void **state) +{ + struct test_server_st *tss = *state; + struct torture_state *s; + int verbosity = torture_libssh_verbosity(); + struct passwd *pwd; + bool b = false; + int rc; + + assert_non_null(tss); + + s = tss->state; + assert_non_null(s); + + pwd = getpwnam("bob"); + assert_non_null(pwd); + + rc = setuid(pwd->pw_uid); + assert_return_code(rc, errno); + + s->ssh.session = ssh_new(); + assert_non_null(s->ssh.session); + + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity); + assert_return_code(s->ssh.session, rc); + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_HOST, TORTURE_SSH_SERVER); + assert_return_code(s->ssh.session, rc); + /* Make sure no other configuration options from system will get used */ + rc = ssh_options_set(s->ssh.session, SSH_OPTIONS_PROCESS_CONFIG, &b); + assert_ssh_return_code(s->ssh.session, rc); + + return 0; +} + +static int session_teardown(void **state) +{ + struct test_server_st *tss = *state; + struct torture_state *s; + + assert_non_null(tss); + + s = tss->state; + assert_non_null(s); + + ssh_disconnect(s->ssh.session); + ssh_free(s->ssh.session); + + return 0; +} + +static void torture_server_auth_kbdint(void **state) +{ + struct test_server_st *tss = *state; + struct torture_state *s; + ssh_session session; + int rc; + + assert_non_null(tss); + + s = tss->state; + assert_non_null(s); + + session = s->ssh.session; + assert_non_null(session); + + rc = ssh_options_set(session, SSH_OPTIONS_USER, TORTURE_SSH_USER_BOB); + assert_int_equal(rc, SSH_OK); + + rc = ssh_connect(session); + assert_int_equal(rc, SSH_OK); + + rc = ssh_userauth_none(session,NULL); + /* This request should return a SSH_REQUEST_DENIED error */ + if (rc == SSH_ERROR) { + assert_int_equal(ssh_get_error_code(session), SSH_REQUEST_DENIED); + } + rc = ssh_userauth_list(session, NULL); + assert_true(rc & SSH_AUTH_METHOD_INTERACTIVE); + + rc = ssh_userauth_kbdint(session, NULL, NULL); + assert_int_equal(rc, SSH_AUTH_INFO); + assert_int_equal(ssh_userauth_kbdint_getnprompts(session), 2); + + /* Reply the first 2 prompts using the username and password */ + rc = ssh_userauth_kbdint_setanswer(session, 0, + TORTURE_SSH_USER_BOB); + assert_false(rc < 0); + + rc = ssh_userauth_kbdint_setanswer(session, 1, + TORTURE_SSH_USER_BOB_PASSWORD); + assert_false(rc < 0); + + /* Resend the password */ + rc = ssh_userauth_kbdint(session, NULL, NULL); + assert_int_equal(rc, SSH_AUTH_INFO); + assert_int_equal(ssh_userauth_kbdint_getnprompts(session), 1); + + rc = ssh_userauth_kbdint_setanswer(session, 0, + TORTURE_SSH_USER_BOB_PASSWORD); + assert_false(rc < 0); + + rc = ssh_userauth_kbdint(session, NULL, NULL); + + /* Sometimes, SSH server send an empty query at the end of exchange */ + if(rc == SSH_AUTH_INFO) { + assert_int_equal(ssh_userauth_kbdint_getnprompts(session), 0); + rc = ssh_userauth_kbdint(session, NULL, NULL); + } + + assert_int_equal(rc, SSH_AUTH_SUCCESS); +} + +int torture_run_tests(void) +{ + int rc; + struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(torture_server_auth_kbdint, + session_setup, + session_teardown), + }; + + ssh_init(); + + torture_filter_tests(tests); + rc = cmocka_run_group_tests(tests, + setup_kbdint_server, + teardown_kbdint_server); + + ssh_finalize(); + + return rc; +} -- cgit v1.2.3