diff --git a/include/libssh/session.h b/include/libssh/session.h index f926b223..b604b008 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -311,6 +311,30 @@ struct ssh_session_struct { struct ssh_pki_ctx_struct *pki_context; }; +/** + * @internal + * + * @brief Apply the reduced-authentication behavior for an unsafe host key + * continuation. + * + * When `StrictHostKeyChecking` is `off` and host key verification reports a + * changed or other key, OpenSSH allows the connection to continue only + * "subject to some restrictions"; see `ssh_config(5)` for + * `StrictHostKeyChecking` and `check_host_key()` in OpenSSH's `sshconnect.c` + * under the `continue_unsafe` label. For libssh's supported client-side auth + * methods, this means disabling password and keyboard-interactive + * authentication for the rest of the session. + */ +static inline void ssh_known_hosts_continue_unsafe(ssh_session session) +{ + SSH_LOG(SSH_LOG_WARN, + "Continuing despite an unsafe host key with " + "StrictHostKeyChecking=off; password and keyboard-interactive " + "authentication have been disabled for this session"); + session->opts.flags &= + ~(SSH_OPT_FLAG_PASSWORD_AUTH | SSH_OPT_FLAG_KBDINT_AUTH); +} + /** @internal * @brief a termination function evaluates the status of an object * @param user[in] object to evaluate diff --git a/src/known_hosts.c b/src/known_hosts.c index 4375a5dd..82578199 100644 --- a/src/known_hosts.c +++ b/src/known_hosts.c @@ -419,10 +419,21 @@ int ssh_is_server_known(ssh_session session) } } while (1); - if ((ret == SSH_SERVER_NOT_KNOWN) && - (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF || - session->opts.StrictHostKeyChecking == - SSH_STRICT_HOSTKEY_ACCEPT_NEW)) { + if (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF) { + if (ret == SSH_SERVER_KNOWN_CHANGED || ret == SSH_SERVER_FOUND_OTHER) { + ssh_known_hosts_continue_unsafe(session); + ret = SSH_SERVER_KNOWN_OK; + } else if (ret == SSH_SERVER_NOT_KNOWN) { + int rv = ssh_session_update_known_hosts(session); + if (rv != SSH_OK) { + ret = SSH_SERVER_ERROR; + } else { + ret = SSH_SERVER_KNOWN_OK; + } + } + } else if ((ret == SSH_SERVER_NOT_KNOWN) && + session->opts.StrictHostKeyChecking == + SSH_STRICT_HOSTKEY_ACCEPT_NEW) { int rv = ssh_session_update_known_hosts(session); if (rv != SSH_OK) { ret = SSH_SERVER_ERROR; diff --git a/src/knownhosts.c b/src/knownhosts.c index df057f1b..63345103 100644 --- a/src/knownhosts.c +++ b/src/knownhosts.c @@ -386,6 +386,7 @@ static void ssh_knownhosts_entries_free(struct ssh_list *entry_list) } ssh_list_free(entry_list); } + /** * @internal * @@ -1152,6 +1153,13 @@ ssh_known_hosts_check_server_key(const char *hosts_entry, return found; } +static bool ssh_known_hosts_should_continue_unsafe(ssh_session session, + enum ssh_known_hosts_e rv) +{ + return session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF && + (rv == SSH_KNOWN_HOSTS_CHANGED || rv == SSH_KNOWN_HOSTS_OTHER); +} + /** * @brief Get the known_hosts entry for the currently connected session. * @@ -1210,8 +1218,18 @@ ssh_session_get_known_hosts_entry(ssh_session session, session->opts.global_knownhosts, pentry); + if (ssh_known_hosts_should_continue_unsafe(session, rv)) { + ssh_known_hosts_continue_unsafe(session); + return SSH_KNOWN_HOSTS_OK; + } + /* If the global file did not help, report the result from the user file. */ if (rv == SSH_KNOWN_HOSTS_UNKNOWN || rv == SSH_KNOWN_HOSTS_NOT_FOUND) { + if (ssh_known_hosts_should_continue_unsafe(session, old_rv)) { + ssh_known_hosts_continue_unsafe(session); + return SSH_KNOWN_HOSTS_OK; + } + if ((old_rv == SSH_KNOWN_HOSTS_UNKNOWN || old_rv == SSH_KNOWN_HOSTS_NOT_FOUND) && (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF || @@ -1222,10 +1240,16 @@ ssh_session_get_known_hosts_entry(ssh_session session, return SSH_KNOWN_HOSTS_ERROR; } - return ssh_session_get_known_hosts_entry_file( - session, - session->opts.knownhosts, - pentry); + rv = + ssh_session_get_known_hosts_entry_file(session, + session->opts.knownhosts, + pentry); + if (rv == SSH_KNOWN_HOSTS_UNKNOWN || + rv == SSH_KNOWN_HOSTS_NOT_FOUND) { + return SSH_KNOWN_HOSTS_OK; + } + + return rv; } return old_rv; diff --git a/tests/client/torture_knownhosts.c b/tests/client/torture_knownhosts.c index 55aee217..a0e82384 100644 --- a/tests/client/torture_knownhosts.c +++ b/tests/client/torture_knownhosts.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "session.c" #include "known_hosts.c" @@ -474,6 +475,135 @@ static void torture_knownhosts_no_hostkeychecking(void **state) assert_int_equal(found, SSH_KNOWN_HOSTS_OK); } +static void torture_knownhosts_no_hostkeychecking_legacy(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct stat sb; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + + rc = stat(known_hosts_file, &sb); + assert_return_code(rc, errno); + assert_true(sb.st_size > 0); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_changed(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BADRSA "\n"); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_other(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BADRSA "\n"); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + int torture_run_tests(void) { int rc; struct CMUnitTest tests[] = { @@ -501,6 +631,18 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_knownhosts_no_hostkeychecking, session_setup, session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_legacy, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_other, + session_setup, + session_teardown), }; ssh_init(); diff --git a/tests/client/torture_knownhosts_verify.c b/tests/client/torture_knownhosts_verify.c index 25ed9920..98c58b5b 100644 --- a/tests/client/torture_knownhosts_verify.c +++ b/tests/client/torture_knownhosts_verify.c @@ -424,18 +424,207 @@ static void torture_knownhosts_accept_new_persists(void **state) free(known_hosts_file); } -static void torture_knownhosts_accept_new_rejects_changed(void **state) +static void torture_knownhosts_no_hostkeychecking_dev_null(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, "/dev/null"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_GLOBAL_KNOWNHOSTS, "/dev/null"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); +} + +static void torture_knownhosts_no_hostkeychecking_changed(void **state) { struct torture_state *s = *state; ssh_session session = s->ssh.session; - struct ssh_knownhosts_entry *entry = NULL; - struct stat sb_before; - struct stat sb_after; char tmp_file[1024] = {0}; char *known_hosts_file = NULL; enum ssh_known_hosts_e found; - FILE *file = NULL; - int strict_host_key_checking = SSH_STRICT_HOSTKEY_NEW; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_other(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void +torture_knownhosts_no_hostkeychecking_changed_global_ok(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct ssh_knownhosts_entry *entry = NULL; + char global_tmp_file[1024] = {0}; + char user_tmp_file[1024] = {0}; + char *global_known_hosts_file = NULL; + char *known_hosts_entry = NULL; + char *user_known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(user_tmp_file, + sizeof(user_tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + user_known_hosts_file = torture_create_temp_file(user_tmp_file); + assert_non_null(user_known_hosts_file); + + snprintf(global_tmp_file, + sizeof(global_tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + global_known_hosts_file = torture_create_temp_file(global_tmp_file); + assert_non_null(global_known_hosts_file); + + torture_write_file(user_known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = + ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, user_known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_GLOBAL_KNOWNHOSTS, + global_known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_session_export_known_hosts_entry(session, &known_hosts_entry); + assert_ssh_return_code(session, rc); + torture_write_file(global_known_hosts_file, known_hosts_entry); + SAFE_FREE(known_hosts_entry); + + assert_true(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_true(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + found = ssh_session_get_known_hosts_entry(session, &entry); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_non_null(entry); + assert_true(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_true(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + ssh_knownhosts_entry_free(entry); + free(global_known_hosts_file); + free(user_known_hosts_file); +} + +static void torture_knownhosts_accept_new_rejects_changed(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct ssh_knownhosts_entry *entry = NULL; + struct stat sb_before; + struct stat sb_after; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_ACCEPT_NEW; int rc; snprintf(tmp_file, @@ -447,14 +636,9 @@ static void torture_knownhosts_accept_new_rejects_changed(void **state) known_hosts_file = torture_create_temp_file(tmp_file); assert_non_null(known_hosts_file); - file = fopen(known_hosts_file, "w"); - if (file == NULL) { - free(known_hosts_file); - fail(); - } /* BAD_RSA is a fixed fixture key that must not match the test server. */ - fprintf(file, "127.0.0.10 %s %s\n", "ssh-rsa", BAD_RSA); - fclose(file); + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); rc = stat(known_hosts_file, &sb_before); assert_return_code(rc, errno); @@ -615,6 +799,22 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_knownhosts_accept_new_persists, session_setup, session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_dev_null, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_other, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed_global_ok, + session_setup, + session_teardown), cmocka_unit_test_setup_teardown( torture_knownhosts_accept_new_rejects_changed, session_setup,