From 26895498fb677abc6ca26a45b450f15df1bb2009 Mon Sep 17 00:00:00 2001 From: Linus Kardell Date: Sat, 9 Jul 2022 21:54:47 +0200 Subject: [PATCH] Implement IdentitiesOnly Signed-off-by: Linus Kardell Reviewed-by: Jakub Jelen --- include/libssh/config.h | 1 + include/libssh/libssh.h | 1 + include/libssh/session.h | 1 + src/auth.c | 104 +++++++++++++++++++++++++++++-- src/config.c | 9 ++- src/options.c | 14 +++++ src/pki.c | 18 ++++++ src/session.c | 1 + tests/client/torture_auth.c | 120 ++++++++++++++++++++++++++++++++++++ 9 files changed, 262 insertions(+), 7 deletions(-) diff --git a/include/libssh/config.h b/include/libssh/config.h index a24d11a1..8b29207f 100644 --- a/include/libssh/config.h +++ b/include/libssh/config.h @@ -62,6 +62,7 @@ enum ssh_config_opcode_e { SOC_PUBKEYACCEPTEDKEYTYPES, SOC_REKEYLIMIT, SOC_IDENTITYAGENT, + SOC_IDENTITIESONLY, SOC_MAX /* Keep this one last in the list */ }; diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h index 7857a77b..0e421907 100644 --- a/include/libssh/libssh.h +++ b/include/libssh/libssh.h @@ -408,6 +408,7 @@ enum ssh_options_e { SSH_OPTIONS_REKEY_TIME, SSH_OPTIONS_RSA_MIN_SIZE, SSH_OPTIONS_IDENTITY_AGENT, + SSH_OPTIONS_IDENTITIES_ONLY, }; enum { diff --git a/include/libssh/session.h b/include/libssh/session.h index d3e5787c..9a58cc7e 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -237,6 +237,7 @@ struct ssh_session_struct { uint64_t rekey_data; uint32_t rekey_time; int rsa_min_size; + bool identities_only; } opts; /* counters */ ssh_counter socket_counter; diff --git a/src/auth.c b/src/auth.c index aa3aa965..9d8be990 100644 --- a/src/auth.c +++ b/src/auth.c @@ -906,6 +906,9 @@ int ssh_userauth_agent(ssh_session session, { int rc = SSH_AUTH_ERROR; struct ssh_agent_state_struct *state; + ssh_key *configKeys = NULL; + size_t configKeysCount = 0; + size_t i; if (session == NULL) { return SSH_AUTH_ERROR; @@ -934,10 +937,92 @@ int ssh_userauth_agent(ssh_session session, return SSH_AUTH_DENIED; } + if (session->opts.identities_only) { + /* + * Read keys mentioned in the config, so we can check if key from agent + * is in there. + */ + size_t identityLen = ssh_list_count(session->opts.identity); + struct ssh_iterator *it = ssh_list_get_iterator(session->opts.identity); + + configKeys = malloc(identityLen * sizeof(configKeys[0])); + if (!configKeys) { + ssh_set_error_oom(session); + return SSH_AUTH_ERROR; + } + + while (it != NULL && configKeysCount < identityLen) { + const char *privkeyFile = it->data; + + /* + * Read the private key file listed in the config, but we're only + * interested in the public key. Don't try to decrypt private key. + */ + ssh_key pubkey = NULL; + rc = ssh_pki_import_pubkey_file(privkeyFile, &pubkey); + if (rc == SSH_OK) { + configKeys[configKeysCount++] = pubkey; + } else { + char *pubkeyFile = NULL; + size_t pubkeyPathLen = strlen(privkeyFile) + sizeof(".pub"); + + if (pubkey) { + ssh_key_free(pubkey); + } + + /* + * If we couldn't get the public key from the private key file, + * try a .pub file instead. + */ + pubkeyFile = malloc(pubkeyPathLen); + if (!pubkeyFile) { + ssh_set_error_oom(session); + rc = SSH_AUTH_ERROR; + goto done; + } + snprintf(pubkeyFile, pubkeyPathLen, "%s.pub", privkeyFile); + rc = ssh_pki_import_pubkey_file(pubkeyFile, &pubkey); + if (rc == SSH_OK) { + configKeys[configKeysCount++] = pubkey; + } else if (pubkey) { + ssh_key_free(pubkey); + } + free(pubkeyFile); + } + it = it->next; + } + } + while (state->pubkey != NULL) { if (state->state == SSH_AGENT_STATE_NONE) { SSH_LOG(SSH_LOG_DEBUG, "Trying identity %s", state->comment); + if (session->opts.identities_only) { + /* Check if this key is one of the keys listed in the config */ + bool found_key = false; + for (i = 0; i < configKeysCount; i++) { + if (ssh_key_cmp(state->pubkey, configKeys[i], + SSH_KEY_CMP_PUBLIC) == 0) { + found_key = true; + break; + } + } + + if (!found_key) { + SSH_LOG(SSH_LOG_DEBUG, + "Identities only is enabled and identity %s was " + "not listed in config, skipping", state->comment); + SSH_STRING_FREE_CHAR(state->comment); + state->comment = NULL; + state->pubkey = ssh_agent_get_next_ident( + session, &state->comment); + + if (state->pubkey == NULL) { + rc = SSH_AUTH_DENIED; + } + continue; + } + } } if (state->state == SSH_AGENT_STATE_NONE || state->state == SSH_AGENT_STATE_PUBKEY) { @@ -945,10 +1030,10 @@ int ssh_userauth_agent(ssh_session session, if (rc == SSH_AUTH_ERROR) { ssh_agent_state_free (state); session->agent_state = NULL; - return rc; + goto done; } else if (rc == SSH_AUTH_AGAIN) { state->state = SSH_AGENT_STATE_PUBKEY; - return rc; + goto done; } else if (rc != SSH_AUTH_SUCCESS) { SSH_LOG(SSH_LOG_DEBUG, "Public key of %s refused by server", state->comment); @@ -966,14 +1051,15 @@ int ssh_userauth_agent(ssh_session session, } if (state->state == SSH_AGENT_STATE_AUTH) { rc = ssh_userauth_agent_publickey(session, username, state->pubkey); - if (rc == SSH_AUTH_AGAIN) - return rc; + if (rc == SSH_AUTH_AGAIN) { + goto done; + } SSH_STRING_FREE_CHAR(state->comment); state->comment = NULL; if (rc == SSH_AUTH_ERROR || rc == SSH_AUTH_PARTIAL) { ssh_agent_state_free (session->agent_state); session->agent_state = NULL; - return rc; + goto done; } else if (rc != SSH_AUTH_SUCCESS) { SSH_LOG(SSH_LOG_INFO, "Server accepted public key but refused the signature"); @@ -984,12 +1070,18 @@ int ssh_userauth_agent(ssh_session session, } ssh_agent_state_free (session->agent_state); session->agent_state = NULL; - return SSH_AUTH_SUCCESS; + rc = SSH_AUTH_SUCCESS; + goto done; } } ssh_agent_state_free (session->agent_state); session->agent_state = NULL; +done: + for (i = 0; i < configKeysCount; i++) { + ssh_key_free(configKeys[i]); + } + free(configKeys); return rc; } diff --git a/src/config.c b/src/config.c index 30892842..7ec0a971 100644 --- a/src/config.c +++ b/src/config.c @@ -103,7 +103,7 @@ static struct ssh_config_keyword_table_s ssh_config_keyword_table[] = { { "hostbasedauthentication", SOC_UNSUPPORTED}, { "hostbasedacceptedalgorithms", SOC_UNSUPPORTED}, { "hostkeyalias", SOC_UNSUPPORTED}, - { "identitiesonly", SOC_UNSUPPORTED}, + { "identitiesonly", SOC_IDENTITIESONLY}, { "identityagent", SOC_IDENTITYAGENT}, { "ipqos", SOC_UNSUPPORTED}, { "kbdinteractivedevices", SOC_UNSUPPORTED}, @@ -1179,6 +1179,13 @@ ssh_config_parse_line(ssh_session session, ssh_options_set(session, SSH_OPTIONS_IDENTITY_AGENT, p); } break; + case SOC_IDENTITIESONLY: + i = ssh_config_get_yesno(&s, -1); + if (i >= 0 && *parsing) { + bool b = i; + ssh_options_set(session, SSH_OPTIONS_IDENTITIES_ONLY, &b); + } + break; default: ssh_set_error(session, SSH_FATAL, "ERROR - unimplemented opcode: %d", opcode); diff --git a/src/options.c b/src/options.c index 49aaefa2..f58c7b3f 100644 --- a/src/options.c +++ b/src/options.c @@ -481,6 +481,11 @@ int ssh_options_set_algo(ssh_session session, * Set the path to the SSH agent socket. If unset, the * SSH_AUTH_SOCK environment is consulted. * (const char *) + + * - SSH_OPTIONS_IDENTITIES_ONLY + * Use only keys specified in the SSH config, even if agent + * offers more. + * (bool) * * @param value The value to set. This is a generic pointer and the * datatype which is used should be set according to the @@ -1077,6 +1082,15 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, } } break; + case SSH_OPTIONS_IDENTITIES_ONLY: + if (value == NULL) { + ssh_set_error_invalid(session); + return -1; + } else { + bool *x = (bool *)value; + session->opts.identities_only = *x; + } + break; default: ssh_set_error(session, SSH_REQUEST_DENIED, "Unknown ssh option %d", type); return -1; diff --git a/src/pki.c b/src/pki.c index 8f62ce2b..aa8cd297 100644 --- a/src/pki.c +++ b/src/pki.c @@ -1783,6 +1783,7 @@ int ssh_pki_import_pubkey_file(const char *filename, ssh_key *pkey) off_t size; int rc, cmp; char err_msg[SSH_ERRNO_MSG_MAX] = {0}; + ssh_key priv_key = NULL; if (pkey == NULL || filename == NULL || *filename == '\0') { return SSH_ERROR; @@ -1852,6 +1853,23 @@ int ssh_pki_import_pubkey_file(const char *filename, ssh_key *pkey) return SSH_OK; } + /* + * Try to parse key as PEM. Set empty passphrase, so user won't be prompted + * for passphrase. Don't try to decrypt encrypted private key. + */ + priv_key = pki_private_key_from_base64(key_buf, "", NULL, NULL); + if (priv_key) { + rc = ssh_pki_export_privkey_to_pubkey(priv_key, pkey); + ssh_key_free(priv_key); + SAFE_FREE(key_buf); + if (rc != SSH_OK) { + SSH_LOG(SSH_LOG_WARN, "Failed to import public key from PEM" + " private key file"); + return SSH_ERROR; + } + return SSH_OK; + } + /* This the old one-line public key format */ q = p = key_buf; for (i = 0; i < buflen; i++) { diff --git a/src/session.c b/src/session.c index 6025c133..7529aed4 100644 --- a/src/session.c +++ b/src/session.c @@ -108,6 +108,7 @@ ssh_session ssh_new(void) session->opts.fd = -1; session->opts.compressionlevel = 7; session->opts.nodelay = 0; + session->opts.identities_only = false; session->opts.flags = SSH_OPT_FLAG_PASSWORD_AUTH | SSH_OPT_FLAG_PUBKEY_AUTH | diff --git a/tests/client/torture_auth.c b/tests/client/torture_auth.c index 79dbd4a7..9148bfbf 100644 --- a/tests/client/torture_auth.c +++ b/tests/client/torture_auth.c @@ -686,6 +686,120 @@ static void torture_auth_agent_nonblocking(void **state) { assert_ssh_return_code(session, rc); } +static void torture_auth_agent_identities_only(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char bob_ssh_key[1024]; + struct passwd *pwd; + int rc; + int identities_only = 1; + char *id; + + pwd = getpwnam("bob"); + assert_non_null(pwd); + + snprintf(bob_ssh_key, + sizeof(bob_ssh_key), + "%s/.ssh/id_rsa", + pwd->pw_dir); + + if (!ssh_agent_is_running(session)){ + print_message("*** Agent not running. Test ignored\n"); + return; + } + rc = ssh_options_set(session, SSH_OPTIONS_USER, TORTURE_SSH_USER_ALICE); + assert_int_equal(rc, SSH_OK); + + rc = ssh_options_set(session, SSH_OPTIONS_IDENTITIES_ONLY, &identities_only); + assert_int_equal(rc, SSH_OK); + + /* Remove the default identities */ + while ((id = ssh_list_pop_head(char *, session->opts.identity)) != NULL) { + SAFE_FREE(id); + } + + 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_PUBLICKEY); + + /* Should fail as key is not in config */ + rc = ssh_userauth_agent(session, NULL); + assert_ssh_return_code_equal(session, rc, SSH_AUTH_DENIED); + + /* Re-add a key */ + rc = ssh_list_append(session->opts.identity, strdup(bob_ssh_key)); + assert_int_equal(rc, SSH_OK); + + /* Should succeed as key now in config */ + rc = ssh_userauth_agent(session, NULL); + assert_ssh_return_code(session, rc); +} + +static void torture_auth_agent_identities_only_protected(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char bob_ssh_key[1024]; + struct passwd *pwd; + int rc; + int identities_only = 1; + char *id; + + pwd = getpwnam("bob"); + assert_non_null(pwd); + + snprintf(bob_ssh_key, + sizeof(bob_ssh_key), + "%s/.ssh/id_rsa_protected", + pwd->pw_dir); + + if (!ssh_agent_is_running(session)){ + print_message("*** Agent not running. Test ignored\n"); + return; + } + rc = ssh_options_set(session, SSH_OPTIONS_USER, TORTURE_SSH_USER_ALICE); + assert_int_equal(rc, SSH_OK); + + rc = ssh_options_set(session, SSH_OPTIONS_IDENTITIES_ONLY, &identities_only); + assert_int_equal(rc, SSH_OK); + + /* Remove the default identities */ + while ((id = ssh_list_pop_head(char *, session->opts.identity)) != NULL) { + SAFE_FREE(id); + } + + 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_PUBLICKEY); + + /* Should fail as key is not in config */ + rc = ssh_userauth_agent(session, NULL); + assert_ssh_return_code_equal(session, rc, SSH_AUTH_DENIED); + + /* Re-add a key */ + rc = ssh_list_append(session->opts.identity, strdup(bob_ssh_key)); + assert_int_equal(rc, SSH_OK); + + /* Should succeed as key now in config */ + rc = ssh_userauth_agent(session, NULL); + assert_ssh_return_code(session, rc); +} + static void torture_auth_cert(void **state) { struct torture_state *s = *state; ssh_session session = s->ssh.session; @@ -1242,6 +1356,12 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_auth_agent_nonblocking, agent_setup, agent_teardown), + cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only, + agent_setup, + agent_teardown), + cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only_protected, + agent_setup, + agent_teardown), cmocka_unit_test_setup_teardown(torture_auth_cert, pubkey_setup, session_teardown),