From a818cb1cae3f6bb0dd6f4e02c03d7fd24dc09ba0 Mon Sep 17 00:00:00 2001 From: Petr Hanzlik Date: Tue, 21 Jul 2026 13:21:43 +0200 Subject: [PATCH] session server ssh UPDATE migrate to callback-based auth for libssh 0.12+ --- CMakeLists.txt | 6 + src/session.c | 18 + src/session_p.h | 15 +- src/session_server.c | 59 +- src/session_server.h | 34 +- src/session_server_ssh.c | 1165 ++++-------------------- src/session_server_ssh_auth_callback.c | 1016 +++++++++++++++++++++ src/session_server_ssh_auth_message.c | 907 ++++++++++++++++++ src/session_server_ssh_wrapper.h | 316 +++++++ 9 files changed, 2531 insertions(+), 1005 deletions(-) create mode 100644 src/session_server_ssh_auth_callback.c create mode 100644 src/session_server_ssh_auth_message.c create mode 100644 src/session_server_ssh_wrapper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ca5954b..f5642e0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -273,6 +273,12 @@ if(ENABLE_SSH_TLS) list(APPEND CMAKE_REQUIRED_LIBRARIES ${LIBSSH_LIBRARIES}) include_directories(${LIBSSH_INCLUDE_DIRS}) + if(LIBSSH_VERSION VERSION_GREATER_EQUAL "0.12.0") + list(APPEND libsrc src/session_server_ssh_auth_callback.c) + else () + list(APPEND libsrc src/session_server_ssh_auth_message.c) + endif() + # dependencies - libcurl find_package(CURL 7.30.0 REQUIRED) if(TARGET CURL::libcurl) diff --git a/src/session.c b/src/session.c index e71c668e..528597c1 100644 --- a/src/session.c +++ b/src/session.c @@ -38,6 +38,7 @@ #ifdef NC_ENABLED_SSH_TLS +#include "session_server_ssh_wrapper.h" #include "session_wrapper.h" #include @@ -944,6 +945,7 @@ nc_session_free_transport(struct nc_session *session, int *multisession) } } ssh_channel_free(session->ti.libssh.channel); + free(session->ti.libssh.channel_cb); } if (session->ti.libssh.next) { @@ -965,6 +967,7 @@ nc_session_free_transport(struct nc_session *session, int *multisession) /* free starting SSH NETCONF session (channel will be freed in ssh_free()) */ free(siter->username); free(siter->host); + free(siter->ti.libssh.channel_cb); if (!(siter->flags & NC_SESSION_SHAREDCTX)) { ly_ctx_destroy((struct ly_ctx *)siter->ctx); } @@ -982,6 +985,12 @@ nc_session_free_transport(struct nc_session *session, int *multisession) sock = -1; #endif + /* free heap-allocated callback data and ssh event (libssh >= 0.12) */ +#if LIBSSH_0_12 + nc_server_ssh_cb_data_free(session->ti.libssh.cb_data); + ssh_event_free(session->ti.libssh.event); +#endif + /* closes sock if set */ ssh_free(session->ti.libssh.session); } else { @@ -994,6 +1003,15 @@ nc_session_free_transport(struct nc_session *session, int *multisession) /* there are still multiple sessions, keep the ring list */ siter->ti.libssh.next = session->ti.libssh.next; } + /* transfer cb_data to a remaining session so it's freed when the SSH session is freed */ + if (session->ti.libssh.cb_data) { + siter->ti.libssh.cb_data = session->ti.libssh.cb_data; + session->ti.libssh.cb_data = NULL; + } + if (session->ti.libssh.event) { + siter->ti.libssh.event = session->ti.libssh.event; + session->ti.libssh.event = NULL; + } } /* SESSION IO UNLOCK */ diff --git a/src/session_p.h b/src/session_p.h index d5482854..c05ce22f 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -896,9 +896,13 @@ struct nc_session { struct { ssh_channel channel; ssh_session session; + struct ssh_channel_callbacks_struct *channel_cb; /**< channel callbacks used in the + callback-based auth (libssh >= 0.12) */ + void *cb_data; /**< heap-allocated nc_server_ssh_cb_data (libssh >= 0.12) */ struct nc_session *next; /**< pointer to the next NETCONF session on the same SSH session, but different SSH channel. If no such session exists, it is NULL. otherwise there is a ring list of the NETCONF sessions */ + ssh_event event; /**< libssh event structure used for the callback-based auth (libssh >= 0.12) */ } libssh; struct { @@ -1435,17 +1439,6 @@ struct nc_session *nc_accept_callhome_ssh_sock(int sock, const char *host, uint1 */ int nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opts, int sock); -/** - * @brief Process a SSH message. - * - * @param[in] session Session structure of the connection. - * @param[in] opts Endpoint SSH options on which the session was created. - * @param[in] msg SSH message itself. - * @param[in] auth_state State of the authentication. - * @return 0 if the message was handled, 1 if it is left up to libssh. - */ -int nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, struct nc_auth_state *auth_state); - void nc_client_ssh_destroy_opts(void); void _nc_client_ssh_destroy_opts(struct nc_client_ssh_opts *opts); diff --git a/src/session_server.c b/src/session_server.c index e7d832ab..f23b812d 100644 --- a/src/session_server.c +++ b/src/session_server.c @@ -42,6 +42,7 @@ #include "session_p.h" #include "session_server.h" #include "session_server_ch.h" +#include "session_server_ssh_wrapper.h" #ifdef NC_ENABLED_SSH_TLS @@ -2294,6 +2295,34 @@ nc_server_send_reply_io(struct nc_session *session, int io_timeout, const struct return ret; } +#ifdef NC_ENABLED_SSH_TLS +/** + * @brief Scan the session ring for a newly established NETCONF SSH channel. + * + * @param[in] session Session whose SSH channel ring to scan. + * @return Pointer to the new starting NETCONF session if found, NULL otherwise. + */ +static struct nc_session * +nc_ps_ssh_find_new_channel(struct nc_session *session) +{ + struct nc_session *new; + + if (!session->ti.libssh.next) { + return NULL; + } + + for (new = session->ti.libssh.next; new != session; new = new->ti.libssh.next) { + if ((new->status == NC_STATUS_STARTING) && new->ti.libssh.channel && + (new->flags & NC_SESSION_SSH_SUBSYS_NETCONF)) { + return new; + } + } + + return NULL; +} + +#endif /* NC_ENABLED_SSH_TLS */ + /** * @brief Poll a session from pspoll acquiring IO lock as needed. * Session must be running and session RPC lock held! @@ -2317,8 +2346,9 @@ nc_ps_poll_session_io(struct nc_session *session, int io_timeout, time_t now_mon uint16_t idle_timeout; #ifdef NC_ENABLED_SSH_TLS +#if !LIBSSH_0_12 ssh_message ssh_msg; - struct nc_session *new; +#endif #endif /* NC_ENABLED_SSH_TLS */ /* check timeout first */ @@ -2341,36 +2371,33 @@ nc_ps_poll_session_io(struct nc_session *session, int io_timeout, time_t now_mon switch (session->ti_type) { #ifdef NC_ENABLED_SSH_TLS case NC_TI_SSH: +#if LIBSSH_0_12 + if (nc_ps_ssh_find_new_channel(session)) { + ret = NC_PSPOLL_SSH_CHANNEL; + break; + } +#else ssh_msg = ssh_message_get(session->ti.libssh.session); if (ssh_msg) { if (nc_session_ssh_msg(session, NULL, ssh_msg, NULL)) { ssh_message_reply_default(ssh_msg); } - if (session->ti.libssh.next) { - for (new = session->ti.libssh.next; new != session; new = new->ti.libssh.next) { - if ((new->status == NC_STATUS_STARTING) && new->ti.libssh.channel && - (new->flags & NC_SESSION_SSH_SUBSYS_NETCONF)) { - /* new NETCONF SSH channel */ - ret = NC_PSPOLL_SSH_CHANNEL; - break; - } - } - if (new != session) { - ssh_message_free(ssh_msg); - break; - } + if (nc_ps_ssh_find_new_channel(session)) { + ret = NC_PSPOLL_SSH_CHANNEL; + ssh_message_free(ssh_msg); + break; } if (!ret) { /* just some SSH message */ ret = NC_PSPOLL_SSH_MSG; } ssh_message_free(ssh_msg); - /* break because 1) we don't want to return anything here ORred with NC_PSPOLL_RPC - * and 2) we don't want to delay openning a new channel by waiting for a RPC to get processed + * and 2) we don't want to delay opening a new channel by waiting for a RPC to get processed */ break; } +#endif r = ssh_channel_poll_timeout(session->ti.libssh.channel, 0, 0); if (r == SSH_EOF) { diff --git a/src/session_server.h b/src/session_server.h index ff538f5c..2da71ec0 100644 --- a/src/session_server.h +++ b/src/session_server.h @@ -525,17 +525,33 @@ int nc_server_ssh_set_authkey_path_format(const char *path); * @brief Keyboard interactive authentication callback. * * The callback has to handle sending interactive challenges and receiving responses by itself. - * An example callback may fit the following description: - * Prepare all prompts for the user and send them via `ssh_message_auth_interactive_request()`. - * Get the answers either by calling `ssh_message_get()` or `nc_server_ssh_kbdint_get_nanswers()`. - * Return value based on your authentication logic and user answers retrieved by - * calling `ssh_userauth_kbdint_getanswer()`. + * The exact workflow depends on the libssh version the library was compiled with. + * + * **libssh older than 0.12 (message-based workflow):** + * The callback is invoked exactly once per authentication attempt, with the initial + * keyboard-interactive request message. Prepare all prompts for the user and send them via + * `ssh_message_auth_interactive_request()`. Get the answers either by calling `ssh_message_get()` + * or `nc_server_ssh_kbdint_get_nanswers()`, and then `ssh_userauth_kbdint_getanswer()` for each + * of them. Multiple challenge-response rounds can be performed within this single invocation. + * + * **libssh 0.12 and newer (callback-based workflow):** + * Authentication is driven by libssh server callbacks, so this callback is invoked separately + * for every stage of the keyboard-interactive exchange and each invocation must return promptly + * (blocking helpers such as `ssh_message_get()` or `nc_server_ssh_kbdint_get_nanswers()` must + * not be used). Determine the current stage with `ssh_message_auth_kbdint_is_response()`: + * - not a response: send a challenge via `ssh_message_auth_interactive_request()` and return + * `SSH_AUTH_INFO`; + * - a response: retrieve the answers with `ssh_userauth_kbdint_getnanswers()` and + * `ssh_userauth_kbdint_getanswer()` and return the authentication result, or send another + * challenge and return `SSH_AUTH_INFO` to start the next round. * * @param[in] session NETCONF session. * @param[in] ssh_sess libssh session. - * @param[in] msg SSH message that contains the interactive request and which expects a reply with prompts. + * @param[in] msg SSH message with the interactive request (a response message with libssh 0.12+). * @param[in] user_data Arbitrary user data. - * @return 0 for successful authentication, non-zero to deny the user. + * @return 0 for successful authentication, non-zero to deny the user; with libssh 0.12+ + * `SSH_AUTH_INFO` may be returned when a challenge was sent and the client's response + * is expected (the callback is then invoked again once it arrives). */ typedef int (*nc_server_ssh_interactive_auth_clb)(const struct nc_session *session, ssh_session ssh_sess, ssh_message msg, void *user_data); @@ -543,8 +559,8 @@ typedef int (*nc_server_ssh_interactive_auth_clb)(const struct nc_session *sessi /** * @brief Set the callback for SSH interactive authentication. * - * @param[in] auth_clb Keyboard interactive authentication callback. This callback is only called once per authentication. - * @param[in] user_data Optional arbitrary user data that will be passed to @p interactive_auth_clb. + * @param[in] auth_clb Keyboard interactive authentication callback. Called once per authentication (libssh < 0.12) or once per stage (libssh >= 0.12). + * @param[in] user_data Optional arbitrary user data that will be passed to @p auth_clb. * @param[in] free_user_data Optional callback that will be called during cleanup to free any @p user_data. */ void nc_server_ssh_set_interactive_auth_clb(nc_server_ssh_interactive_auth_clb auth_clb, void *user_data, void (*free_user_data)(void *user_data)); diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index bc55850d..5285a8cd 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -45,8 +45,108 @@ #include "nc_version.h" #include "session.h" #include "session_p.h" +#include "session_server_ssh_wrapper.h" #include "session_wrapper.h" +int +nc_ssh_check_local_user_support(struct nc_session *session) +{ + const struct ly_ctx *ctx; + struct lys_module *mod; + + ctx = nc_session_get_ctx(session); + mod = ly_ctx_get_module_latest(ctx, "ietf-ssh-server"); + if (!mod) { + ERRINT; + return -1; + } + + if (lys_feature_value(mod, "local-users-supported") == 0) { + return 1; + } + + return 0; +} + +struct nc_auth_client * +nc_ssh_find_auth_client(struct nc_server_ssh_opts *opts, const char *user) +{ + struct nc_endpt *referenced_endpt; + + if (!user) { + return NULL; + } + + for (uint32_t u = 0; u < LY_ARRAY_COUNT(opts->auth_clients); u++) { + if (!strcmp(opts->auth_clients[u].username, user)) { + return &opts->auth_clients[u]; + } + } + + /* client not known by the endpt, but it references another one so try it */ + if (opts->referenced_endpt_name) { + if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) { + return NULL; + } + return nc_ssh_find_auth_client(referenced_endpt->opts.ssh, user); + } + return NULL; +} + +void +nc_ssh_auth_state_init(struct nc_session *session, struct nc_auth_state *auth_state, + int local_users_supported, struct nc_auth_client *auth_client) +{ + if (auth_state->method_count) { + return; + } + + if (local_users_supported) { + if (auth_client->pubkey_store != NC_STORE_UNKNOWN) { + auth_state->methods |= SSH_AUTH_METHOD_PUBLICKEY; + auth_state->method_count++; + } + if (auth_client->password) { + auth_state->methods |= SSH_AUTH_METHOD_PASSWORD; + auth_state->method_count++; + } + if (auth_client->kbdint_method != NC_KBDINT_AUTH_METHOD_NONE) { + auth_state->methods |= SSH_AUTH_METHOD_INTERACTIVE; + auth_state->method_count++; + } + if (auth_client->none_enabled) { + auth_state->methods |= SSH_AUTH_METHOD_NONE; + auth_state->method_count++; + } + } else { + /* no local users meaning pw, pubkey and kbdint methods are supported, method count is set to 1, + * because only one method is needed for successful auth */ + auth_state->methods = SSH_AUTH_METHOD_PUBLICKEY | SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE; + auth_state->method_count = 1; + } + + ssh_set_auth_methods(session->ti.libssh.session, auth_state->methods); +} + +int +nc_ssh_auth_success(struct nc_session *session, struct nc_auth_state *auth_state, int method) +{ + auth_state->success_methods |= method; + auth_state->success_count++; + + if (auth_state->success_count < auth_state->method_count) { + /* success, but he needs to do another method */ + VRB(session, "User \"%s\" partially authenticated, but still needs to authenticate via the rest of his configured methods.", + session->username); + ssh_set_auth_methods(session->ti.libssh.session, auth_state->methods & ~auth_state->success_methods); + return SSH_AUTH_PARTIAL; + } + + /* authenticated */ + session->flags |= NC_SESSION_SSH_AUTHENTICATED; + return SSH_AUTH_SUCCESS; +} + /** * @brief Stores the private key data as a temporary file. * @@ -154,15 +254,7 @@ nc_server_ssh_ks_ref_get_key(const char *referenced_name, struct nc_asymmetric_k return 0; } -/** - * @brief Get public keys from the truststore. - * - * @param[in] referenced_name Name of the public key bag in the truststore. - * @param[out] pubkeys Referenced public keys. - * @param[out] pubkey_count Referenced public key count. - * @return 0 on success, 1 on error. - */ -static int +int nc_server_ssh_ts_ref_get_keys(const char *referenced_name, struct nc_public_key **pubkeys, uint32_t *pubkey_count) { LY_ARRAY_COUNT_TYPE i; @@ -435,15 +527,7 @@ nc_server_ssh_read_authorized_keys_file(const char *path, struct nc_public_key * return ret; } -/** - * @brief Get user's public keys from the system. - * - * @param[in] username Username. - * @param[out] pubkeys User's public keys. - * @param[out] pubkey_count Public key count. - * @return 0 on success, non-zero on error. - */ -static int +int nc_server_ssh_get_system_keys(const char *username, struct nc_public_key **pubkeys, uint32_t *pubkey_count) { int ret = 0; @@ -551,13 +635,7 @@ nc_server_ssh_getspnam(const char *username, struct spwd *spwd_buf, char **buf, return spwd; } -/** - * @brief Get the user's hashed password from the system. - * - * @param[in] username Username. - * @return User's hashed password or NULL on error. - */ -static char * +char * nc_server_ssh_get_pwd_hash(const char *username) { struct passwd *pwd, pwd_buf; @@ -615,14 +693,7 @@ nc_server_ssh_get_pwd_hash(const char *username) #endif -/** - * @brief Compare stored hashed password with a cleartext received password. - * - * @param[in] stored_pw Hashed stored password. - * @param[in] received_pw Cleartext received password. - * @return 0 on match, non-zero otherwise. - */ -static int +int nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw) { char *received_pw_hash = NULL; @@ -705,306 +776,6 @@ nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_session return ret; } -#ifdef HAVE_LIBPAM - -/** - * @brief PAM conversation function, which serves as a callback for exchanging messages between the client and a PAM module. - * - * @param[in] n_messages Number of messages. - * @param[in] msg PAM module's messages. - * @param[out] resp User responses. - * @param[in] appdata_ptr Callback's data. - * @return PAM_SUCCESS on success, PAM_BUF_ERR on memory allocation error, PAM_CONV_ERR otherwise. - */ -static int -nc_pam_conv_clb(int n_messages, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) -{ - int i, j, t, r = PAM_SUCCESS, n_answers, n_requests = n_messages; - const char **prompts = NULL; - char *echo = NULL; - const char *name = "Keyboard-Interactive Authentication"; - const char *instruction = "Please enter your authentication token"; - ssh_message reply = NULL; - struct nc_pam_thread_arg *clb_data = appdata_ptr; - ssh_session libssh_session; - - libssh_session = clb_data->session->ti.libssh.session; - - /* PAM_MAX_NUM_MSG == 32 by default */ - if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) { - ERR(clb_data->session, "Bad number of PAM messages (#%d).", n_messages); - r = PAM_CONV_ERR; - goto cleanup; - } - - /* only accepting these 4 types of messages */ - for (i = 0; i < n_messages; i++) { - t = msg[i]->msg_style; - if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) { - ERR(clb_data->session, "PAM conversation callback received an unexpected type of message."); - r = PAM_CONV_ERR; - goto cleanup; - } - } - - /* display messages with errors and/or some information and count the amount of actual authentication challenges */ - for (i = 0; i < n_messages; i++) { - if (msg[i]->msg_style == PAM_TEXT_INFO) { - VRB(clb_data->session, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg); - n_requests--; - } - if (msg[i]->msg_style == PAM_ERROR_MSG) { - ERR(clb_data->session, "PAM conversation callback received an error message (%s).", msg[i]->msg); - r = PAM_CONV_ERR; - goto cleanup; - } - } - - /* there are no requests left for the user, only messages with some information for the client were sent */ - if (n_requests <= 0) { - r = PAM_SUCCESS; - goto cleanup; - } - - /* it is the PAM module's responsibility to release both, this array and the responses themselves */ - *resp = calloc(n_requests, sizeof **resp); - prompts = calloc(n_requests, sizeof *prompts); - echo = calloc(n_requests, sizeof *echo); - NC_CHECK_ERRMEM_GOTO(!(*resp) || !prompts || !echo, r = PAM_BUF_ERR, cleanup); - - /* set the prompts for the user */ - j = 0; - for (i = 0; i < n_messages; i++) { - if ((msg[i]->msg_style == PAM_PROMPT_ECHO_ON) || (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF)) { - prompts[j++] = msg[i]->msg; - } - } - - /* iterate over all the messages and adjust the echo array accordingly */ - j = 0; - for (i = 0; i < n_messages; i++) { - if (msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { - echo[j++] = 1; - } - if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) { - /* no need to set to 0 because of calloc */ - j++; - } - } - - /* print all the keyboard-interactive challenges to the user */ - r = ssh_message_auth_interactive_request(clb_data->msg, name, instruction, n_requests, prompts, echo); - if (r != SSH_OK) { - ERR(clb_data->session, "Failed to send an authentication request."); - r = PAM_CONV_ERR; - goto cleanup; - } - - n_answers = nc_server_ssh_kbdint_get_nanswers(clb_data->session, libssh_session); - if (n_answers < 0) { - /* timeout or dc */ - r = PAM_CONV_ERR; - goto cleanup; - } else if (n_answers != n_requests) { - /* check if the number of answers and requests matches */ - ERR(clb_data->session, "Expected %d response(s), got %d.", n_requests, n_answers); - r = PAM_CONV_ERR; - goto cleanup; - } - - /* give the replies to a PAM module */ - for (i = 0; i < n_answers; i++) { - (*resp)[i].resp = strdup(ssh_userauth_kbdint_getanswer(libssh_session, i)); - /* it should be the caller's responsibility to free this, however if mem alloc fails, - * it is safer to free the responses here and set them to NULL */ - if ((*resp)[i].resp == NULL) { - for (j = 0; j < i; j++) { - free((*resp)[j].resp); - (*resp)[j].resp = NULL; - } - ERRMEM; - r = PAM_BUF_ERR; - goto cleanup; - } - } - -cleanup: - ssh_message_free(reply); - free(prompts); - free(echo); - return r; -} - -/** - * @brief Handles authentication via Linux PAM. - * - * @param[in] session NETCONF session. - * @param[in] username Username of the client to auhtenticate. - * @param[in] ssh_msg SSH message with a keyboard-interactive authentication request. - * @return PAM_SUCCESS on success; - * @return PAM error otherwise. - */ -static int -nc_server_ssh_auth_kbdint_pam(struct nc_session *session, const char *username, ssh_message ssh_msg) -{ - pam_handle_t *pam_h = NULL; - int ret; - struct nc_pam_thread_arg clb_data; - struct pam_conv conv; - - /* structure holding callback's data */ - clb_data.msg = ssh_msg; - clb_data.session = session; - - /* PAM conversation structure holding the callback and it's data */ - conv.conv = nc_pam_conv_clb; - conv.appdata_ptr = &clb_data; - - if (!server_opts.pam_config_name) { - ERR(session, "PAM configuration filename not set."); - ret = 1; - goto cleanup; - } - - /* initialize PAM and see if the given configuration file exists */ - ret = pam_start(server_opts.pam_config_name, username, &conv, &pam_h); - if (ret != PAM_SUCCESS) { - ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - goto cleanup; - } - - /* authentication based on the modules listed in the configuration file */ - ret = pam_authenticate(pam_h, 0); - if (ret != PAM_SUCCESS) { - if (ret == PAM_ABORT) { - ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - goto cleanup; - } else { - VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - goto cleanup; - } - } - - /* correct token entered, check other requirements(the time of the day, expired token, ...) */ - ret = pam_acct_mgmt(pam_h, 0); - if ((ret != PAM_SUCCESS) && (ret != PAM_NEW_AUTHTOK_REQD)) { - VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - goto cleanup; - } - - /* if a token has expired a new one will be generated */ - if (ret == PAM_NEW_AUTHTOK_REQD) { - VRB(session, "PAM warning occurred (%s).", pam_strerror(pam_h, ret)); - ret = pam_chauthtok(pam_h, PAM_CHANGE_EXPIRED_AUTHTOK); - if (ret == PAM_SUCCESS) { - VRB(session, "The authentication token of user \"%s\" updated successfully.", username); - } else { - ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - goto cleanup; - } - } - -cleanup: - /* destroy the PAM context */ - if (pam_h && (pam_end(pam_h, ret) != PAM_SUCCESS)) { - ERR(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); - } - return ret; -} - -#elif defined (HAVE_SHADOW) - -/** - * @brief Authenticate using credentials stored in the system. - * - * @param[in] session Session to authenticate on. - * @param[in] username Username of the client to authenticate. - * @param[in] msg SSH message that originally requested kbdint authentication. - * - * @return 0 on success, non-zero otherwise. - */ -static int -nc_server_ssh_auth_kbdint_passwd(struct nc_session *session, const char *username, ssh_message msg) -{ - int ret = 0, n_answers; - const char *name = "Keyboard-Interactive Authentication"; - const char *instruction = "Please enter your authentication token"; - char *prompt = NULL, *pw = NULL, *received_pw = NULL; - char echo[] = {0}; - - /* try to get the client's pw hash from the system */ - pw = nc_server_ssh_get_pwd_hash(username); - if (!pw) { - ret = 1; - goto cleanup; - } - - ret = asprintf(&prompt, "%s's password:", username); - NC_CHECK_ERRMEM_GOTO(ret == -1, prompt = NULL; ret = 1, cleanup); - - /* send the password prompt to the client */ - ret = ssh_message_auth_interactive_request(msg, name, instruction, 1, (const char **) &prompt, echo); - if (ret) { - ERR(session, "Failed to send an authentication request to client \"%s\".", username); - goto cleanup; - } - - /* get the reply */ - n_answers = nc_server_ssh_kbdint_get_nanswers(session, session->ti.libssh.session); - if (n_answers < 0) { - /* timeout or dc */ - ret = 1; - goto cleanup; - } else if (n_answers != 1) { - /* only expecting a single answer */ - ERR(session, "Unexpected amount of answers in system auth. Expected 1, got \"%d\".", n_answers); - ret = 1; - goto cleanup; - } - received_pw = strdup(ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0)); - NC_CHECK_ERRMEM_GOTO(!received_pw, ret = 1, cleanup); - - /* cmp the passwords */ - ret = nc_server_ssh_compare_password(pw, received_pw); - -cleanup: - free(pw); - free(received_pw); - free(prompt); - return ret; -} - -#endif - -/** - * @brief Keyboard-interactive authentication method using the system's authentication methods. - * - * @param[in] session NETCONF session. - * @param[in] msg SSH message with a keyboard-interactive authentication request. - * @return 0 on success, non-zero otherwise. - */ -static int -nc_server_ssh_auth_kbdint_system(struct nc_session *session, ssh_message msg) -{ - int rc; - -#ifdef HAVE_LIBPAM - /* authenticate using PAM */ - rc = nc_server_ssh_auth_kbdint_pam(session, session->username, msg); -#elif defined (HAVE_SHADOW) - /* authenticate using /etc/passwd and /etc/shadow */ - rc = nc_server_ssh_auth_kbdint_passwd(session, session->username, msg); -#else - (void)session; - (void)msg; - - ERR(NULL, "Keyboard-interactive method not supported."); - rc = 1; -#endif - - return rc; -} - API void nc_server_ssh_set_interactive_auth_clb(int (*interactive_auth_clb)(const struct nc_session *session, ssh_session ssh_sess, ssh_message msg, void *user_data), void *user_data, void (*free_user_data)(void *user_data)) @@ -1228,13 +999,7 @@ nc_server_ssh_create_ssh_pubkey(const char *base64, ssh_key *key) return ret; } -/** - * @brief Compare SSH key with configured authorized keys and return the username of the matching one, if any. - * - * @param[in] key Presented SSH key to compare. - * @return Authorized key username, NULL if no match was found. - */ -static int +int nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys, uint16_t pubkey_count) { uint16_t i; @@ -1265,13 +1030,7 @@ nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys return ret; } -/** - * @brief Send the SSH issue banner if configured. - * - * @param[in] session NETCONF session. - * @param[in] opts SSH server options. - */ -static void +void nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts *opts) { if (!opts->banner) { @@ -1293,647 +1052,52 @@ nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts #endif } -/** - * @brief Handle authentication request for the None method. - * - * @param[in] local_users_supported Whether the server supports local users. - * @param[in] auth_client Configured client's authentication data. - * @param[in] msg libssh message. - * @return 0 if the authentication was successful, -1 if not (@p msg already replied to). - */ -static int -nc_server_ssh_auth_none(int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) -{ - assert(!local_users_supported || auth_client); - - if (local_users_supported && auth_client->none_enabled) { - return 0; - } - - ssh_message_reply_default(msg); - return -1; -} - -/** - * @brief Handle authentication request for the Password method. - * - * @param[in] session NETCONF session. - * @param[in] local_users_supported Whether the server supports local users. - * @param[in] auth_client Configured client's authentication data. - * @param[in] msg libssh message. - * @return 0 if the authentication was successful, 1 if not (@p msg not yet replied to). - */ +/* ret 1 on success, 0 on timeout, -1 on error */ static int -nc_server_ssh_auth_password(struct nc_session *session, int local_users_supported, - struct nc_auth_client *auth_client, ssh_message msg) +nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc_server_ssh_opts *opts) { - int rc; - char *password = NULL; - - assert(!local_users_supported || auth_client); + struct timespec ts_timeout; - if (local_users_supported) { - /* obtain pw from config */ - password = auth_client->password; - if (!password) { - /* client requested password auth, but it is not configured for this user, so just deny */ - DBG(session, - "User \"%s\" does not have password method configured, but a request was received.", session->username); - return 1; - } - } else { -#ifdef HAVE_SHADOW - /* obtain pw from system, this one needs to be free'd */ - password = nc_server_ssh_get_pwd_hash(session->username); - if (!password) { - return 1; - } +#if LIBSSH_0_12 + int32_t time_diff; + int ret; #else - ERR(session, "Obtaining password from system not supported."); - return 1; + ssh_message msg; #endif - } - - /* compare the passwords */ - rc = nc_server_ssh_compare_password(password, ssh_message_auth_password(msg)); - - if (!local_users_supported) { - free(password); - } - - return rc ? 1 : 0; -} - -/** - * @brief Handle authentication request for the Publickey method. - * - * @param[in] session NETCONF session. - * @param[in] local_users_supported Whether the server supports local users. - * @param[in] auth_client Configured client's authentication data. - * @param[in] msg libssh message. - * @return 0 if the authentication was successful, 1 if not and the @p msg not yet replied to, -1 if not and @p msg was replied to. - */ -static int -nc_server_ssh_auth_pubkey(struct nc_session *session, int local_users_supported, - struct nc_auth_client *auth_client, ssh_message msg) -{ - int signature_state, ret = 0; - struct nc_public_key *pubkeys = NULL; - uint32_t pubkey_count = 0, i; - - assert(!local_users_supported || auth_client); - - /* get the public keys */ - if (!local_users_supported) { - /* system user, get the keys from the system (these need to be free'd as they're not in the config) */ - ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); - if (ret) { - goto cleanup; - } - } else { - if (auth_client->pubkey_store == NC_STORE_UNKNOWN) { - /* client requested pubkey auth, but it is not configured for this user, so just deny */ - DBG(session, - "User \"%s\" does not have public key method configured, but a request was received.", session->username); - return 1; - } - - if (auth_client->pubkey_store == NC_STORE_SYSTEM) { - /* get the keys from the system (these need to be free'd as they're not in the config) */ - ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); - if (ret) { - goto cleanup; - } - } else if (auth_client->pubkey_store == NC_STORE_LOCAL) { - /* saved directly in the user's config */ - pubkeys = auth_client->pubkeys; - pubkey_count = LY_ARRAY_COUNT(auth_client->pubkeys); - } else if (auth_client->pubkey_store == NC_STORE_TRUSTSTORE) { - /* need to fetch from the truststore */ - ret = nc_server_ssh_ts_ref_get_keys(auth_client->ts_ref, &pubkeys, &pubkey_count); - if (ret) { - goto cleanup; - } - } else { - ERRINT; - return 1; - } - } - - /* compare the received pubkey with the authorized ones */ - if (nc_server_ssh_auth_pubkey_compare_key(ssh_message_auth_pubkey(msg), pubkeys, pubkey_count)) { - VRB(session, "User \"%s\" tried to use an unknown (unauthorized) public key.", session->username); - ret = 1; - goto cleanup; - } - - signature_state = ssh_message_auth_publickey_state(msg); - if (signature_state == SSH_PUBLICKEY_STATE_NONE) { - /* accepting only the use of a public key */ - ssh_message_auth_reply_pk_ok_simple(msg); - ret = -1; - } - -cleanup: - if (!local_users_supported || (auth_client->pubkey_store == NC_STORE_SYSTEM)) { - for (i = 0; i < pubkey_count; i++) { - free(pubkeys[i].name); - free(pubkeys[i].data); - } - free(pubkeys); - } - - return ret; -} - -/** - * @brief Handle authentication request for the Keyboard-interactive method. - * - * @param[in] session NETCONF session. - * @param[in] local_users_supported Whether the server supports local users. - * @param[in] auth_client Configured client's authentication data. - * @param[in] msg libssh message. - * @return 0 if the authentication was successful, 1 if not. - */ -static int -nc_server_ssh_auth_kbdint(struct nc_session *session, int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) -{ - int r = 0; - - assert(!local_users_supported || auth_client); - - if (!local_users_supported) { - /* no local users supported, use the system method */ - r = nc_server_ssh_auth_kbdint_system(session, msg); - } else { - if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_NONE) { - /* client requested kbdint auth, but it is not configured for this user, so just deny */ - DBG(session, - "User \"%s\" does not have kbdint method configured, but a request was received.", session->username); - return 1; - } - - if (server_opts.interactive_auth_clb) { - /* custom callback has higher priority */ - r = server_opts.interactive_auth_clb(session, - session->ti.libssh.session, msg, server_opts.interactive_auth_data); - } else { - /* perform the authentication based on the configured method */ - if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_SYSTEM) { - r = nc_server_ssh_auth_kbdint_system(session, msg); - } else { - /* add future methods here */ - ERR(session, "Keyboard-interactive authentication method not supported."); - return 1; - } - } - } - - return r ? 1 : 0; -} - -/** - * @brief Handle SSH channel open request. - * - * @param[in] session NETCONF session. - * @param[in] msg libssh message. - * @return 0 on success, -1 on failure. - */ -static int -nc_server_ssh_channel_open(struct nc_session *session, ssh_message msg) -{ - ssh_channel chan; - - /* first channel request */ - if (!session->ti.libssh.channel) { - if (session->status != NC_STATUS_STARTING) { - ERRINT; - return -1; - } - chan = ssh_message_channel_request_open_reply_accept(msg); - if (!chan) { - ERR(session, "Failed to create a new SSH channel."); - return -1; - } - session->ti.libssh.channel = chan; - - /* additional channel request */ - } else { - chan = ssh_message_channel_request_open_reply_accept(msg); - if (!chan) { - ERR(session, "Session %u: failed to create a new SSH channel.", session->id); - return -1; - } - /* channel was created and libssh stored it internally in the ssh_session structure, good enough */ - } - return 0; -} + DBG(session, "Waiting for \"netconf\" SSH subsystem request..."); -/** - * @brief Handle SSH channel request subsystem request. - * - * @param[in] session NETCONF session. - * @param[in] channel Requested SSH channel. - * @param[in] subsystem Name of the requested subsystem. - * @return 0 on success, -1 on failure. - */ -static int -nc_server_ssh_channel_subsystem(struct nc_session *session, ssh_channel channel, const char *subsystem) -{ - struct nc_session *new_session; + nc_timeouttime_get(&ts_timeout, NC_TRANSPORT_MSG_TIMEOUT); - if (strcmp(subsystem, "netconf")) { - WRN(session, "Received an unknown subsystem \"%s\" request.", subsystem); - return -1; - } +#if LIBSSH_0_12 + (void) opts; - if (session->ti.libssh.channel == channel) { - /* first channel requested */ - if (session->ti.libssh.next || (session->status != NC_STATUS_STARTING)) { - ERRINT; - return -1; - } - if (session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { - ERR(session, "Subsystem \"netconf\" requested for the second time."); + /* Run the event loop instead of ssh_message_get() */ + while (!(session->flags & NC_SESSION_SSH_SUBSYS_NETCONF)) { + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(session, "Communication SSH socket unexpectedly closed."); return -1; } - session->flags |= NC_SESSION_SSH_SUBSYS_NETCONF; - } else { - /* additional channel subsystem request, new session is ready as far as SSH is concerned */ - new_session = nc_new_session(NC_SERVER, 1); - NC_CHECK_ERRMEM_RET(!new_session, -1); - - /* insert the new session */ - if (!session->ti.libssh.next) { - new_session->ti.libssh.next = session; - } else { - new_session->ti.libssh.next = session->ti.libssh.next; - } - session->ti.libssh.next = new_session; - - new_session->status = NC_STATUS_STARTING; - new_session->ti_type = NC_TI_SSH; - new_session->io_lock = session->io_lock; - new_session->ti.libssh.channel = channel; - new_session->ti.libssh.session = session->ti.libssh.session; - new_session->username = strdup(session->username); - new_session->host = strdup(session->host); - new_session->port = session->port; - new_session->ctx = (struct ly_ctx *)session->ctx; - new_session->flags = NC_SESSION_SSH_AUTHENTICATED | NC_SESSION_SSH_SUBSYS_NETCONF | NC_SESSION_SHAREDCTX; - } - - return 0; -} - -/** - * @brief Handle NETCONF SSH authentication. - * - * @param[in] session NETCONF session. - * @param[in] opts SSH server options. - * @param[in] msg libssh message. - * @param[in] method Type of the authentication method. - * @param[in] str_method String representation of the authentication method. - * @param[in] local_users_supported Whether the server supports local users. - * @param[in,out] auth_state Authentication state. - * @return 1 in case of a fatal error, 0 otherwise. - */ -static int -nc_server_ssh_auth(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, - int method, const char *str_method, int local_users_supported, struct nc_auth_state *auth_state) -{ - const char *username; - int ret = 0; - LY_ARRAY_COUNT_TYPE u; - struct nc_auth_client *auth_client = NULL; - struct nc_endpt *referenced_endpt; - - /* save the username, do not let the client change it */ - username = ssh_message_auth_user(msg); - assert(username); - - if (local_users_supported) { - /* get the locally configured user */ - LY_ARRAY_FOR(opts->auth_clients, u) { - if (!strcmp(opts->auth_clients[u].username, username)) { - auth_client = &opts->auth_clients[u]; - break; - } - } - - if (!auth_client) { - if (opts->referenced_endpt_name) { - /* client not known by the endpt, but it references another one so try it */ - if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) { - ERRINT; - return 1; - } - - return nc_server_ssh_auth(session, referenced_endpt->opts.ssh, msg, method, - str_method, local_users_supported, auth_state); - } - - /* user not known, set his authentication methods to public key only so that - * there is no interaction and it will simply be denied */ - ERR(NULL, "User \"%s\" not known by the server.", username); - ssh_set_auth_methods(session->ti.libssh.session, SSH_AUTH_METHOD_PUBLICKEY); - ssh_message_reply_default(msg); - return 0; - } - } - - if (!session->username) { - session->username = strdup(username); - NC_CHECK_ERRMEM_RET(!session->username, 1); - - /* send the SSH issue banner on the first userauth request */ - nc_server_ssh_send_banner(session, opts); - - /* configure and count accepted auth methods */ - if (local_users_supported) { - if ((auth_client->pubkey_store == NC_STORE_LOCAL) || - (auth_client->pubkey_store == NC_STORE_TRUSTSTORE) || (auth_client->pubkey_store == NC_STORE_SYSTEM)) { - /* either locally configured pubkeys, or truststore or system */ - auth_state->methods |= SSH_AUTH_METHOD_PUBLICKEY; - auth_state->method_count++; - } - if (auth_client->password) { - auth_state->methods |= SSH_AUTH_METHOD_PASSWORD; - auth_state->method_count++; - } - if (auth_client->kbdint_method) { - auth_state->methods |= SSH_AUTH_METHOD_INTERACTIVE; - auth_state->method_count++; - } - if (auth_client->none_enabled) { - auth_state->methods |= SSH_AUTH_METHOD_NONE; - auth_state->method_count++; - } - } else { - /* no local users meaning pw, pubkey and kbdint methods are supported, method count is set to 1, - * because only one method is needed for successful auth */ - auth_state->methods = SSH_AUTH_METHOD_PUBLICKEY | SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE; - auth_state->method_count = 1; - } - - ssh_set_auth_methods(session->ti.libssh.session, auth_state->methods); - } else { - if (strcmp(username, session->username)) { - /* changing username not allowed */ - ERR(session, "User \"%s\" changed its username to \"%s\".", session->username, username); - session->status = NC_STATUS_INVALID; - session->term_reason = NC_SESSION_TERM_OTHER; - return 1; - } - } - - /* try authenticating, if local users are supported, then the configured user must authenticate via all of his - * configured auth methods, otherwise for system users just one is needed, - * 0 return indicates success, 1 fail (msg not yet replied to), -1 fail (msg was replied to) */ - if (method == SSH_AUTH_METHOD_NONE) { - ret = nc_server_ssh_auth_none(local_users_supported, auth_client, msg); - } else if (method == SSH_AUTH_METHOD_PASSWORD) { - ret = nc_server_ssh_auth_password(session, local_users_supported, auth_client, msg); - } else if (method == SSH_AUTH_METHOD_PUBLICKEY) { - ret = nc_server_ssh_auth_pubkey(session, local_users_supported, auth_client, msg); - } else if (method == SSH_AUTH_METHOD_INTERACTIVE) { - ret = nc_server_ssh_auth_kbdint(session, local_users_supported, auth_client, msg); - } else { - ++session->opts.server.ssh_auth_attempts; - VRB(session, "Authentication method \"%s\" not supported.", str_method); - ssh_message_reply_default(msg); - return 0; - } - - if (!ret) { - auth_state->success_methods |= method; - auth_state->success_count++; - - if (auth_state->success_count < auth_state->method_count) { - /* success, but he needs to do another method */ - VRB(session, "User \"%s\" partially authenticated, but still needs to authenticate via the rest of his configured methods.", username); - ssh_set_auth_methods(session->ti.libssh.session, auth_state->methods & ~auth_state->success_methods); - ssh_message_auth_reply_success(msg, 1); - } else { - /* authenticated */ - ssh_message_auth_reply_success(msg, 0); - session->flags |= NC_SESSION_SSH_AUTHENTICATED; - VRB(session, "User \"%s\" authenticated.", username); - } - } else if (ret == 1) { - /* failed attempt, msg wasnt yet replied to */ - ++session->opts.server.ssh_auth_attempts; - VRB(session, "Failed user \"%s\" authentication attempt (#%d).", session->username, - session->opts.server.ssh_auth_attempts); - ssh_message_reply_default(msg); - } - - return 0; -} - -int -nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, struct nc_auth_state *auth_state) -{ - const char *str_type, *str_subtype = NULL; - int subtype, type, rc, local_users_supported; - const struct ly_ctx *ctx; - struct lys_module *mod; - - type = ssh_message_type(msg); - subtype = ssh_message_subtype(msg); - - switch (type) { - case SSH_REQUEST_AUTH: - str_type = "request-auth"; - switch (subtype) { - case SSH_AUTH_METHOD_NONE: - str_subtype = "none"; - break; - case SSH_AUTH_METHOD_PASSWORD: - str_subtype = "password"; - break; - case SSH_AUTH_METHOD_PUBLICKEY: - str_subtype = "publickey"; - break; - case SSH_AUTH_METHOD_HOSTBASED: - str_subtype = "hostbased"; - break; - case SSH_AUTH_METHOD_INTERACTIVE: - str_subtype = "interactive"; - break; - case SSH_AUTH_METHOD_GSSAPI_MIC: - str_subtype = "gssapi-mic"; - break; - } - break; - - case SSH_REQUEST_CHANNEL_OPEN: - str_type = "request-channel-open"; - switch (subtype) { - case SSH_CHANNEL_SESSION: - str_subtype = "session"; - break; - case SSH_CHANNEL_DIRECT_TCPIP: - str_subtype = "direct-tcpip"; - break; - case SSH_CHANNEL_FORWARDED_TCPIP: - str_subtype = "forwarded-tcpip"; - break; - case (int)SSH_CHANNEL_X11: - str_subtype = "channel-x11"; - break; - case SSH_CHANNEL_UNKNOWN: - /* fallthrough */ - default: - str_subtype = "unknown"; + time_diff = nc_timeouttime_cur_diff(&ts_timeout); + if (time_diff < 1) { + /* timeout */ + ERR(session, "Failed to start \"netconf\" SSH subsystem for too long, disconnecting."); break; } - break; - case SSH_REQUEST_CHANNEL: - str_type = "request-channel"; - switch (subtype) { - case SSH_CHANNEL_REQUEST_PTY: - str_subtype = "pty"; - break; - case SSH_CHANNEL_REQUEST_EXEC: - str_subtype = "exec"; - break; - case SSH_CHANNEL_REQUEST_SHELL: - str_subtype = "shell"; - break; - case SSH_CHANNEL_REQUEST_ENV: - str_subtype = "env"; - break; - case SSH_CHANNEL_REQUEST_SUBSYSTEM: - str_subtype = "subsystem"; - break; - case SSH_CHANNEL_REQUEST_WINDOW_CHANGE: - str_subtype = "window-change"; - break; - case SSH_CHANNEL_REQUEST_X11: - str_subtype = "x11"; - break; - case SSH_CHANNEL_REQUEST_UNKNOWN: - /* fallthrough */ - default: - str_subtype = "unknown"; - break; - } - break; - - case SSH_REQUEST_SERVICE: - str_type = "request-service"; - str_subtype = ssh_message_service_service(msg); - break; - - case SSH_REQUEST_GLOBAL: - str_type = "request-global"; - switch (subtype) { - case SSH_GLOBAL_REQUEST_TCPIP_FORWARD: - str_subtype = "tcpip-forward"; - break; - case SSH_GLOBAL_REQUEST_CANCEL_TCPIP_FORWARD: - str_subtype = "cancel-tcpip-forward"; - break; - case SSH_GLOBAL_REQUEST_UNKNOWN: - /* fallthrough */ - default: - str_subtype = "unknown"; + /* This function listens to the network and automatically calls callback funcitons. */ + ret = ssh_event_dopoll(session->ti.libssh.event, time_diff); + if ((ret == SSH_ERROR) || (ret == SSH_AGAIN)) { break; } - break; - - default: - str_type = "unknown"; - str_subtype = "unknown"; - break; - } - - VRB(session, "Received an SSH message \"%s\" of subtype \"%s\".", str_type, str_subtype); - if (!session || (session->status == NC_STATUS_CLOSING) || (session->status == NC_STATUS_INVALID)) { - /* "valid" situation if, for example, receiving some auth or channel request timeouted, - * but we got it now, during session free */ - VRB(session, "SSH message arrived on a %s session, the request will be denied.", - (session && session->status == NC_STATUS_CLOSING ? "closing" : "invalid")); - ssh_message_reply_default(msg); - return 0; } - /* - * process known messages - */ - if (type == SSH_REQUEST_AUTH) { - if (session->flags & NC_SESSION_SSH_AUTHENTICATED) { - ERR(session, "User \"%s\" authenticated, but requested another authentication.", session->username); - ssh_message_reply_default(msg); - return 0; - } else if (!auth_state || !opts) { - /* these two parameters should always be set during an authentication, - * however do a check just in case something goes really wrong, since they - * are not needed for other types of messages - */ - ERRINT; - return 1; - } - - /* get libyang ctx from session and ietf-ssh-server yang model from the ctx */ - ctx = nc_session_get_ctx(session); - mod = ly_ctx_get_module_latest(ctx, "ietf-ssh-server"); - if (!mod) { - ERRINT; - return 1; - } - - /* check if local-users-supported feature is enabled */ - rc = lys_feature_value(mod, "local-users-supported"); - if (!rc) { - /* using users from the YANG data */ - local_users_supported = 1; - } else if (rc == LY_ENOT) { - /* using users from the system */ - local_users_supported = 0; - } else { - ERRINT; - return 1; - } - - /* authenticate */ - return nc_server_ssh_auth(session, opts, msg, subtype, str_subtype, local_users_supported, auth_state); - } else if (session->flags & NC_SESSION_SSH_AUTHENTICATED) { - if ((type == SSH_REQUEST_CHANNEL_OPEN) && ((enum ssh_channel_type_e)subtype == SSH_CHANNEL_SESSION)) { - if (nc_server_ssh_channel_open(session, msg)) { - ssh_message_reply_default(msg); - } - return 0; - - } else if ((type == SSH_REQUEST_CHANNEL) && ((enum ssh_channel_requests_e)subtype == SSH_CHANNEL_REQUEST_SUBSYSTEM)) { - if (nc_server_ssh_channel_subsystem(session, ssh_message_channel_request_channel(msg), - ssh_message_channel_request_subsystem(msg))) { - ssh_message_reply_default(msg); - } else { - ssh_message_channel_request_reply_success(msg); - } - return 0; - } + if (session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { + VRB(session, "NETCONF subsystem successfully opened."); + return 1; } - - /* we did not process it */ - return 1; -} - -/* ret 1 on success, 0 on timeout, -1 on error */ -static int -nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc_server_ssh_opts *opts) -{ - struct timespec ts_timeout; - ssh_message msg; - - DBG(session, "Waiting for \"netconf\" SSH subsystem request..."); - - nc_timeouttime_get(&ts_timeout, NC_TRANSPORT_MSG_TIMEOUT); +#else while (1) { if (!ssh_is_connected(session->ti.libssh.session)) { ERR(session, "Communication SSH socket unexpectedly closed while waiting for \"netconf\" subsystem request."); @@ -1959,7 +1123,7 @@ nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc break; } } - +#endif return 0; } @@ -2008,8 +1172,15 @@ static int nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts *opts) { struct timespec ts_timeout; + +#if LIBSSH_0_12 + ssh_event event; + int32_t time_diff; + int ret; +#else ssh_message msg; struct nc_auth_state auth_state = {0}; +#endif DBG(session, "SSH authentication..."); @@ -2017,6 +1188,32 @@ nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts if (opts->auth_timeout) { nc_timeouttime_get(&ts_timeout, opts->auth_timeout * 1000); } +#if LIBSSH_0_12 + /* Create an event loop */ + event = ssh_event_new(); + ssh_event_add_session(event, session->ti.libssh.session); + session->ti.libssh.event = event; + + /* Run the event loop instead of ssh_message_get() */ + while (!(session->flags & NC_SESSION_SSH_AUTHENTICATED)) { + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(session, "Communication SSH socket unexpectedly closed."); + return -1; + } + + time_diff = nc_timeouttime_cur_diff(&ts_timeout); + if (time_diff < 1) { + /* timeout */ + break; + } + + /* This function listens to the network and automatically calls callback funcitons. */ + ret = ssh_event_dopoll(event, time_diff); + if ((ret == SSH_ERROR) || (ret == SSH_AGAIN)) { + break; + } + } +#else while (1) { if (!ssh_is_connected(session->ti.libssh.session)) { ERR(session, "Communication SSH socket unexpectedly closed while waiting for authentication."); @@ -2041,6 +1238,7 @@ nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts break; } } +#endif if (!(session->flags & NC_SESSION_SSH_AUTHENTICATED)) { /* timeout */ @@ -2064,6 +1262,10 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt const char *err_msg; char *proto_str = NULL, *proto_str_dyn = NULL; +#if LIBSSH_0_12 + struct nc_server_ssh_cb_data *cb_data = NULL; +#endif + /* other transport-specific data */ session->ti_type = NC_TI_SSH; session->ti.libssh.session = ssh_new(); @@ -2073,6 +1275,24 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt goto cleanup; } +#if LIBSSH_0_12 + cb_data = calloc(1, sizeof(*cb_data)); + NC_CHECK_ERRMEM_GOTO(!cb_data, rc = -1, cleanup); + cb_data->session = session; + cb_data->opts = opts; + + cb_data->server_cb.userdata = cb_data; + cb_data->server_cb.auth_password_function = nc_server_ssh_cb_auth_password; + cb_data->server_cb.auth_pubkey_function = nc_server_ssh_cb_auth_pubkey; + cb_data->server_cb.auth_none_function = nc_server_ssh_cb_auth_none; + cb_data->server_cb.auth_kbdint_function = nc_server_ssh_cb_auth_kbdint; + cb_data->server_cb.channel_open_request_session_function = nc_server_ssh_cb_channel_open_request_session; + + ssh_callbacks_init(&cb_data->server_cb); + ssh_set_server_callbacks(session->ti.libssh.session, &cb_data->server_cb); + session->ti.libssh.cb_data = cb_data; +#endif /* LIBSSH_0_12 */ + sbind = ssh_bind_new(); if (!sbind) { ERR(session, "Failed to create an SSH bind."); @@ -2190,6 +1410,13 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt rc = nc_accept_ssh_session_auth(session, opts); session->data = NULL; if (rc != 1) { +#if LIBSSH_0_12 && defined (HAVE_LIBPAM) + /* if PAM thread is still running, cancel and clean it up */ + if (cb_data) { + nc_server_ssh_cb_pam_cancel(cb_data->pam_kbdint); + cb_data->pam_kbdint = NULL; + } +#endif goto cleanup; } diff --git a/src/session_server_ssh_auth_callback.c b/src/session_server_ssh_auth_callback.c new file mode 100644 index 00000000..1e90a6f7 --- /dev/null +++ b/src/session_server_ssh_auth_callback.c @@ -0,0 +1,1016 @@ +/** + * @file session_server_ssh_auth_callback.c + * @author Petr Hanzlik + * @brief libnetconf2 SSH authentication with callbacks (Libssh 0.12 and newer). + * + * @copyright + * Copyright (c) 2026 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 "config.h" /* Expose HAVE_LIBPAM and HAVE_SHADOW */ + +#ifdef HAVE_LIBPAM +# include +#endif + +#include +#include +#include +#include + +#include "compat.h" +#include "log_p.h" +#include "session_server_ssh_wrapper.h" + +#ifdef HAVE_LIBPAM + +/** Fixed prompt name sent with every interactive request. */ +#define NC_PAM_KBDINT_NAME "Keyboard-Interactive Authentication" + +/** Fixed prompt instruction sent with every interactive request. */ +#define NC_PAM_KBDINT_INSTRUCTION "Please enter your authentication token" + +/** + * @brief PAM conversation callback for the callback-based kbdint path. + * + * @param[in] n_messages Number of PAM messages. + * @param[in] msg PAM module's messages. + * @param[out] resp User responses (allocated here, freed by PAM). + * @param[in] appdata_ptr Pointer to nc_server_ssh_cb_pam_data. + * @return PAM_SUCCESS on success. + * @return PAM_BUF_ERR on OOM. + * @return PAM_CONV_ERR otherwise. + */ +static int +nc_server_ssh_cb_pam_conv(int n_messages, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + struct nc_server_ssh_cb_pam_data *data = appdata_ptr; + int i, j, t, n_requests = n_messages; + const char **prompts = NULL; + char *echo = NULL; + + if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) { + ERR(data->session, "Bad number of PAM messages (#%d).", n_messages); + return PAM_CONV_ERR; + } + + /* only accepting these 4 types of messages */ + for (i = 0; i < n_messages; i++) { + t = msg[i]->msg_style; + if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && + (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) { + ERR(data->session, "PAM conversation callback received an unexpected type of message."); + return PAM_CONV_ERR; + } + } + + /* handle info/error messages, count actual prompts */ + for (i = 0; i < n_messages; i++) { + if (msg[i]->msg_style == PAM_TEXT_INFO) { + VRB(data->session, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg); + n_requests--; + } + if (msg[i]->msg_style == PAM_ERROR_MSG) { + ERR(data->session, "PAM conversation callback received an error message (%s).", msg[i]->msg); + return PAM_CONV_ERR; + } + } + + /* no actual prompts */ + if (n_requests <= 0) { + return PAM_SUCCESS; + } + + /* build prompts and echo arrays */ + *resp = calloc(n_requests, sizeof **resp); + prompts = calloc(n_requests, sizeof *prompts); + echo = calloc(n_requests, sizeof *echo); + if (!(*resp) || !prompts || !echo) { + free(*resp); + *resp = NULL; + free(prompts); + free(echo); + return PAM_BUF_ERR; + } + + j = 0; + for (i = 0; i < n_messages; i++) { + if ((msg[i]->msg_style == PAM_PROMPT_ECHO_ON) || (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF)) { + prompts[j] = msg[i]->msg; + if (msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { + echo[j] = 1; + } + j++; + } + } + + /* signal main thread that prompts are ready */ + pthread_mutex_lock(&data->lock); + if (data->state == NC_PAM_CANCELLED) { + pthread_mutex_unlock(&data->lock); + free(prompts); + free(echo); + free(*resp); + *resp = NULL; + return PAM_CONV_ERR; + } + data->n_prompts = n_requests; + data->prompts = prompts; + data->echo = echo; + data->state = NC_PAM_PROMPTS_READY; + pthread_cond_signal(&data->changed); + + /* wait for answers from main thread */ + while (data->state == NC_PAM_PROMPTS_READY) { + pthread_cond_wait(&data->changed, &data->lock); + } + + if (data->state == NC_PAM_CANCELLED) { + pthread_mutex_unlock(&data->lock); + free(prompts); + free(echo); + free(*resp); + *resp = NULL; + return PAM_CONV_ERR; + } + + /* state == NC_PAM_ANSWERS_READY: copy answers */ + if (data->n_answers > n_requests) { + ERR(data->session, "Client sent more answers (%d) than prompts (%d).", data->n_answers, n_requests); + pthread_mutex_unlock(&data->lock); + free(prompts); + free(echo); + free(*resp); + *resp = NULL; + return PAM_CONV_ERR; + } + + for (i = 0; i < data->n_answers; i++) { + (*resp)[i].resp = strdup(data->answers[i]); + if (!(*resp)[i].resp) { + for (j = 0; j < i; j++) { + free((*resp)[j].resp); + (*resp)[j].resp = NULL; + } + free(prompts); + free(echo); + pthread_mutex_unlock(&data->lock); + return PAM_BUF_ERR; + } + } + + pthread_mutex_unlock(&data->lock); + + /* free prompts/echo arrays */ + free(prompts); + free(echo); + return PAM_SUCCESS; +} + +/** + * @brief PAM thread function — runs pam_authenticate/pam_acct_mgmt/pam_chauthtok. + * + * Never calls libssh. Communicates with the main thread via condvars. + * + * @param[in] arg Pointer to nc_server_ssh_cb_pam_data. + * @return NULL. + */ +static void * +nc_server_ssh_cb_pam_thread(void *arg) +{ + struct nc_server_ssh_cb_pam_data *data = arg; + struct pam_conv conv = {0}; + int ret; + + conv.conv = nc_server_ssh_cb_pam_conv; + conv.appdata_ptr = data; + + if (!server_opts.pam_config_name) { + ERR(data->session, "PAM configuration filename not set."); + ret = 1; + goto done; + } + + ret = pam_start(server_opts.pam_config_name, data->username, &conv, &data->pam_h); + if (ret != PAM_SUCCESS) { + ERR(data->session, "PAM error occurred (%s).", pam_strerror(data->pam_h, ret)); + goto done; + } + + ret = pam_authenticate(data->pam_h, 0); + if (ret != PAM_SUCCESS) { + if (ret == PAM_ABORT) { + ERR(data->session, "PAM error occurred (%s).", pam_strerror(data->pam_h, ret)); + } else { + VRB(data->session, "PAM error occurred (%s).", pam_strerror(data->pam_h, ret)); + } + goto done; + } + + ret = pam_acct_mgmt(data->pam_h, 0); + if ((ret != PAM_SUCCESS) && (ret != PAM_NEW_AUTHTOK_REQD)) { + VRB(data->session, "PAM error occurred (%s).", pam_strerror(data->pam_h, ret)); + goto done; + } + + if (ret == PAM_NEW_AUTHTOK_REQD) { + VRB(data->session, "PAM warning occurred (%s).", pam_strerror(data->pam_h, ret)); + ret = pam_chauthtok(data->pam_h, PAM_CHANGE_EXPIRED_AUTHTOK); + if (ret == PAM_SUCCESS) { + VRB(data->session, "The authentication token of user \"%s\" updated successfully.", data->username); + } else { + ERR(data->session, "PAM error occurred (%s).", pam_strerror(data->pam_h, ret)); + } + } + +done: + pthread_mutex_lock(&data->lock); + data->pam_ret = ret; + data->state = NC_PAM_DONE; + pthread_cond_signal(&data->changed); + pthread_mutex_unlock(&data->lock); + + if (data->pam_h) { + pam_end(data->pam_h, ret); + data->pam_h = NULL; + } + return NULL; +} + +/** + * @brief Free PAM bridge data (must be called after thread is joined). + * + * @param[in] data PAM data to free. + */ +static void +nc_server_ssh_cb_pam_data_free(struct nc_server_ssh_cb_pam_data *data) +{ + if (!data) { + return; + } + pthread_mutex_destroy(&data->lock); + pthread_cond_destroy(&data->changed); + free(data); +} + +void +nc_server_ssh_cb_pam_cancel(struct nc_server_ssh_cb_pam_data *data) +{ + if (!data) { + return; + } + + /* signal PAM thread to cancel */ + pthread_mutex_lock(&data->lock); + if (data->state == NC_PAM_PROMPTS_READY) { + /* PAM thread is waiting for answers — tell it to abort */ + data->state = NC_PAM_CANCELLED; + pthread_cond_signal(&data->changed); + } else if (data->state == NC_PAM_RUNNING) { + /* PAM thread is processing — mark as cancelled so it stops at next conv */ + data->state = NC_PAM_CANCELLED; + } + pthread_mutex_unlock(&data->lock); + + pthread_join(data->thread, NULL); + nc_server_ssh_cb_pam_data_free(data); +} + +/** + * @brief Phase 1 of callback-based PAM kbdint: start PAM thread, wait for prompts, send to client. + * + * @param[in] cb_data Callback data (stores pam_kbdint pointer for Phase 2). + * @param[in] message SSH message for sending prompts. + * @param[in] user Username. + * @return SSH_AUTH_INFO if prompts sent. + * @return SSH_AUTH_SUCCESS/SSH_AUTH_DENIED on PAM completion. + */ +static int +nc_server_ssh_cb_kbdint_pam_request(struct nc_server_ssh_cb_data *cb_data, ssh_message message, const char *UNUSED(user)) +{ + struct nc_server_ssh_cb_pam_data *pam_data; + int rc, n_prompts; + const char **prompts; + char *echo; + + /* allocate and initialize PAM bridge data */ + pam_data = calloc(1, sizeof *pam_data); + NC_CHECK_ERRMEM_RET(!pam_data, SSH_AUTH_DENIED); + + pthread_mutex_init(&pam_data->lock, NULL); + pthread_cond_init(&pam_data->changed, NULL); + pam_data->username = cb_data->session->username; + pam_data->session = cb_data->session; + pam_data->state = NC_PAM_RUNNING; + cb_data->pam_kbdint = pam_data; + + /* start PAM thread */ + rc = pthread_create(&pam_data->thread, NULL, nc_server_ssh_cb_pam_thread, pam_data); + if (rc) { + ERR(cb_data->session, "Failed to create PAM thread (%s).", strerror(rc)); + pthread_mutex_destroy(&pam_data->lock); + pthread_cond_destroy(&pam_data->changed); + free(pam_data); + cb_data->pam_kbdint = NULL; + return SSH_AUTH_DENIED; + } + + /* wait for PAM thread to produce prompts or finish */ + pthread_mutex_lock(&pam_data->lock); + while (pam_data->state == NC_PAM_RUNNING) { + pthread_cond_wait(&pam_data->changed, &pam_data->lock); + } + + if (pam_data->state == NC_PAM_DONE) { + /* PAM finished without needing prompts (error or immediate success) */ + rc = pam_data->pam_ret; + pthread_mutex_unlock(&pam_data->lock); + pthread_join(pam_data->thread, NULL); + nc_server_ssh_cb_pam_data_free(pam_data); + cb_data->pam_kbdint = NULL; + return (rc == PAM_SUCCESS) ? SSH_AUTH_SUCCESS : SSH_AUTH_DENIED; + } + + /* state == NC_PAM_PROMPTS_READY: send prompts to client */ + n_prompts = pam_data->n_prompts; + prompts = pam_data->prompts; + echo = pam_data->echo; + + pthread_mutex_unlock(&pam_data->lock); + rc = ssh_message_auth_interactive_request(message, NC_PAM_KBDINT_NAME, NC_PAM_KBDINT_INSTRUCTION, + n_prompts, prompts, echo); + + if (rc != SSH_OK) { + ERR(cb_data->session, "Failed to send an authentication request."); + nc_server_ssh_cb_pam_cancel(pam_data); + cb_data->pam_kbdint = NULL; + return SSH_AUTH_DENIED; + } + + return SSH_AUTH_INFO; +} + +/** + * @brief Phase 2 of callback-based PAM kbdint: read answers, pass to PAM thread, wait for result. + * + * @param[in] cb_data Callback data (contains pam_kbdint from Phase 1). + * @param[in] message SSH message for sending additional prompts if needed. + * @return SSH_AUTH_INFO if more prompts needed. + * @return SSH_AUTH_SUCCESS/SSH_AUTH_DENIED on completion. + */ +static int +nc_server_ssh_cb_kbdint_pam_response(struct nc_server_ssh_cb_data *cb_data, ssh_message message) +{ + struct nc_server_ssh_cb_pam_data *pam_data = cb_data->pam_kbdint; + struct nc_session *session = cb_data->session; + int n_answers, i, rc, n_prompts; + char **answers = NULL, *echo; + const char **prompts, *answer; + + if (!pam_data) { + ERR(session, "Keyboard-interactive response received without prior request."); + return SSH_AUTH_DENIED; + } + + /* read answers from libssh kbdint structure */ + n_answers = ssh_userauth_kbdint_getnanswers(session->ti.libssh.session); + if (n_answers < 0) { + ERR(session, "Failed to get number of kbdint answers."); + nc_server_ssh_cb_pam_cancel(pam_data); + cb_data->pam_kbdint = NULL; + return SSH_AUTH_DENIED; + } + + answers = calloc(n_answers, sizeof *answers); + NC_CHECK_ERRMEM_GOTO(!answers, rc = SSH_AUTH_DENIED, cleanup); + + for (i = 0; i < n_answers; i++) { + answer = ssh_userauth_kbdint_getanswer(session->ti.libssh.session, i); + + if (!answer) { + ERR(session, "Failed to get keyboard-interactive answer %d.", i); + rc = SSH_AUTH_DENIED; + goto cleanup; + } + answers[i] = strdup(answer); + NC_CHECK_ERRMEM_GOTO(!answers[i], rc = SSH_AUTH_DENIED, cleanup); + } + + /* pass answers to PAM thread */ + pthread_mutex_lock(&pam_data->lock); + pam_data->n_answers = n_answers; + pam_data->answers = answers; + pam_data->state = NC_PAM_ANSWERS_READY; + pthread_cond_signal(&pam_data->changed); + + /* wait for PAM thread to process: either new prompts or completion */ + while (pam_data->state == NC_PAM_ANSWERS_READY) { + pthread_cond_wait(&pam_data->changed, &pam_data->lock); + } + + /* answers have been consumed by PAM thread, free them */ + for (i = 0; i < n_answers; i++) { + free(answers[i]); + } + free(answers); + pam_data->answers = NULL; + answers = NULL; + + if (pam_data->state == NC_PAM_DONE) { + rc = pam_data->pam_ret; + pthread_mutex_unlock(&pam_data->lock); + pthread_join(pam_data->thread, NULL); + nc_server_ssh_cb_pam_data_free(pam_data); + cb_data->pam_kbdint = NULL; + return (rc == PAM_SUCCESS) ? SSH_AUTH_SUCCESS : SSH_AUTH_DENIED; + } + + /* state == NC_PAM_PROMPTS_READY: send new prompts to client */ + n_prompts = pam_data->n_prompts; + prompts = pam_data->prompts; + echo = pam_data->echo; + + pthread_mutex_unlock(&pam_data->lock); + rc = ssh_message_auth_interactive_request(message, NC_PAM_KBDINT_NAME, NC_PAM_KBDINT_INSTRUCTION, + n_prompts, prompts, echo); + + if (rc != SSH_OK) { + ERR(session, "Failed to send an authentication request."); + nc_server_ssh_cb_pam_cancel(pam_data); + cb_data->pam_kbdint = NULL; + return SSH_AUTH_DENIED; + } + + return SSH_AUTH_INFO; + +cleanup: + for (i = 0; i < n_answers; i++) { + free(answers[i]); + } + free(answers); + nc_server_ssh_cb_pam_cancel(pam_data); + cb_data->pam_kbdint = NULL; + return rc; +} + +#elif defined (HAVE_SHADOW) + +/** + * @brief Send a password prompt to the client (Phase 1 of callback-based shadow kbdint). + * + * @param[in] session NETCONF session. + * @param[in] username Username of the client to authenticate. + * @param[in] msg SSH message with the keyboard-interactive authentication request. + * @return SSH_AUTH_INFO if the prompt was sent successfully. + * @return SSH_AUTH_DENIED on error. + */ +static int +nc_server_ssh_cb_kbdint_shadow_request(struct nc_session *session, const char *username, ssh_message msg) +{ + const char *name = "Keyboard-Interactive Authentication"; + const char *instruction = "Please enter your authentication token"; + char *prompt = NULL; + char echo[] = {0}; + int rc; + + rc = asprintf(&prompt, "%s's password:", username); + NC_CHECK_ERRMEM_RET(rc == -1, SSH_AUTH_DENIED); + + rc = ssh_message_auth_interactive_request(msg, name, instruction, 1, (const char **)&prompt, echo); + free(prompt); + if (rc) { + ERR(session, "Failed to send an authentication request to client \"%s\".", username); + return SSH_AUTH_DENIED; + } + + return SSH_AUTH_INFO; +} + +/** + * @brief Check the client's password answer against the shadow hash (Phase 2 of callback-based shadow kbdint). + * + * @param[in] session NETCONF session. + * @param[in] username Username of the client to authenticate. + * @return SSH_AUTH_SUCCESS if the password matches. + * @return SSH_AUTH_DENIED otherwise. + */ +static int +nc_server_ssh_cb_kbdint_shadow_response(struct nc_session *session, const char *username) +{ + char *pw = NULL, *received_pw = NULL; + const char *answer; + int n_answers, rc; + + n_answers = ssh_userauth_kbdint_getnanswers(session->ti.libssh.session); + if (n_answers != 1) { + ERR(session, "Unexpected amount of answers in system auth. Expected 1, got \"%d\".", n_answers); + return SSH_AUTH_DENIED; + } + + pw = nc_server_ssh_get_pwd_hash(username); + if (!pw) { + return SSH_AUTH_DENIED; + } + + answer = ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0); + + if (!answer) { + ERR(session, "Failed to get keyboard-interactive password answer."); + free(pw); + return SSH_AUTH_DENIED; + } + received_pw = strdup(answer); + if (!received_pw) { + ERRMEM; + free(pw); + return SSH_AUTH_DENIED; + } + + rc = nc_server_ssh_compare_password(pw, received_pw); + free(pw); + free(received_pw); + + return rc == 0 ? SSH_AUTH_SUCCESS : SSH_AUTH_DENIED; +} + +#endif + +/** + * @brief Common setup for all callback auth methods: save username, send banner, + * validate username consistency, check local-users support, find auth client, + * and initialize auth state. + * + * @param[in] cb_data Callback data. + * @param[in] user Username. + * @param[out] local_users_supported Set to 1 if local users are supported, 0 otherwise. + * @param[out] auth_client Set to the found auth_client (may be NULL for system users). + * @return 0 on success, + * @return -1 on failure (caller should return SSH_AUTH_DENIED). + */ +static int +nc_server_ssh_cb_auth_common_setup(struct nc_server_ssh_cb_data *cb_data, const char *user, + int *local_users_supported, struct nc_auth_client **auth_client) +{ + struct nc_session *session = cb_data->session; + struct nc_server_ssh_opts *opts = cb_data->opts; + + *local_users_supported = 0; + *auth_client = NULL; + + if (!user) { + session->opts.server.ssh_auth_attempts++; + return -1; + } + + /* Save the username if this is the first attempt */ + if (!session->username) { + session->username = strdup(user); + NC_CHECK_ERRMEM_RET(!session->username, -1); + + /* send the SSH issue banner on the first userauth request */ + nc_server_ssh_send_banner(session, opts); + } else if (strcmp(user, session->username)) { + /* changing username not allowed */ + ERR(session, "User \"%s\" changed its username to \"%s\".", session->username, user); + session->status = NC_STATUS_INVALID; + session->term_reason = NC_SESSION_TERM_OTHER; + session->opts.server.ssh_auth_attempts++; + return -1; + } + + /* Check if local users are supported via the YANG model */ + *local_users_supported = nc_ssh_check_local_user_support(session); + if (*local_users_supported < 0) { + /* fatal error checking local users support */ + session->opts.server.ssh_auth_attempts++; + return -1; + } + + /* Find the auth client if local users are supported */ + if (*local_users_supported) { + *auth_client = nc_ssh_find_auth_client(opts, user); + + if (!*auth_client) { + ERR(session, "User \"%s\" not known by the server.", user); + /* advertise only publickey so there is no interaction and it is simply denied */ + ssh_set_auth_methods(session->ti.libssh.session, SSH_AUTH_METHOD_PUBLICKEY); + session->opts.server.ssh_auth_attempts++; + return -1; + } + } + + assert(!*local_users_supported || *auth_client); + + nc_ssh_auth_state_init(session, &cb_data->auth_state, *local_users_supported, *auth_client); + + return 0; +} + +int +nc_server_ssh_cb_auth_none(ssh_session UNUSED(libssh_sess), const char *user, void *userdata) +{ + struct nc_server_ssh_cb_data *cb_data = (struct nc_server_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_auth_client *auth_client = NULL; + int local_users_supported = 0; + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + if (local_users_supported && !auth_client) { + ssh_set_auth_methods(session->ti.libssh.session, SSH_AUTH_METHOD_PUBLICKEY); + } + return SSH_AUTH_DENIED; + } + + if (local_users_supported && auth_client->none_enabled) { + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_NONE); + } + + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; +} + +int +nc_server_ssh_cb_auth_password(ssh_session UNUSED(libssh_sess), const char *user, const char *password, void *userdata) +{ + struct nc_server_ssh_cb_data *cb_data = (struct nc_server_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + int local_users_supported = 0; + struct nc_auth_client *auth_client = NULL; + int rc = 1; + char *stored_password = NULL; + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + return SSH_AUTH_DENIED; + } + + /* Get the stored password */ + if (local_users_supported) { + stored_password = auth_client->password; + if (!stored_password) { + DBG(session, "User \"%s\" does not have password method configured.", user); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + } else { + #ifdef HAVE_SHADOW + stored_password = nc_server_ssh_get_pwd_hash(user); + if (!stored_password) { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + #else + ERR(session, "Obtaining password from system not supported."); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + #endif + } + + /* Compare the passwords */ + rc = nc_server_ssh_compare_password(stored_password, password); + + if (!local_users_supported) { + free(stored_password); + } + + if (rc == 0) { + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_PASSWORD); + } else { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } +} + +int +nc_server_ssh_cb_auth_pubkey(ssh_session UNUSED(libssh_sess), const char *user, struct ssh_key_struct *pubkey, char signature_state, void *userdata) +{ + struct nc_server_ssh_cb_data *cb_data = (struct nc_server_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_auth_client *auth_client = NULL; + int local_users_supported = 0; + int ret = 0; + struct nc_public_key *pubkeys = NULL; + uint32_t pubkey_count = 0, i; + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + return SSH_AUTH_DENIED; + } + + /* get the public keys */ + if (!local_users_supported) { + /* system user, get the keys from the system (these need to be free'd as they're not in the config) */ + ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else { + if (auth_client->pubkey_store == NC_STORE_UNKNOWN) { + /* client requested pubkey auth, but it is not configured for this user, so just deny */ + DBG(session, + "User \"%s\" does not have public key method configured, but a request was received.", session->username); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + if (auth_client->pubkey_store == NC_STORE_SYSTEM) { + /* get the keys from the system (these need to be free'd as they're not in the config) */ + ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else if (auth_client->pubkey_store == NC_STORE_LOCAL) { + /* saved directly in the user's config */ + pubkeys = auth_client->pubkeys; + pubkey_count = LY_ARRAY_COUNT(auth_client->pubkeys); + } else if (auth_client->pubkey_store == NC_STORE_TRUSTSTORE) { + /* need to fetch from the truststore */ + ret = nc_server_ssh_ts_ref_get_keys(auth_client->ts_ref, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else { + ERRINT; + return SSH_AUTH_DENIED; + } + } + + /* compare the received pubkey with the authorized ones */ + if (nc_server_ssh_auth_pubkey_compare_key(pubkey, pubkeys, pubkey_count)) { + VRB(session, "User \"%s\" tried to use an unknown (unauthorized) public key.", session->username); + ret = 1; + goto cleanup; + } + +cleanup: + if (!local_users_supported || (auth_client->pubkey_store == NC_STORE_SYSTEM)) { + for (i = 0; i < pubkey_count; i++) { + free(pubkeys[i].name); + free(pubkeys[i].data); + } + free(pubkeys); + } + + if (ret == 0) { + if (signature_state == SSH_PUBLICKEY_STATE_NONE) { + /* just checking if the public key would be accepted */ + return SSH_AUTH_SUCCESS; + } + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_PUBLICKEY); + } else { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } +} + +/** + * @brief Dispatch callback-based system keyboard-interactive authentication. + * + * @param[in] session NETCONF session. + * @param[in] message SSH message. + * @param[in] user Username. + * @return SSH_AUTH_INFO if prompts sent. + * @return SSH_AUTH_SUCCESS if authenticated. + * @return SSH_AUTH_DENIED on failure. + */ +static int +nc_server_ssh_cb_kbdint_system(struct nc_server_ssh_cb_data *cb_data, ssh_message message, const char *user) +{ + int is_response = ssh_message_auth_kbdint_is_response(message); + +#ifdef HAVE_LIBPAM + if (is_response) { + return nc_server_ssh_cb_kbdint_pam_response(cb_data, message); + } else { + return nc_server_ssh_cb_kbdint_pam_request(cb_data, message, user); + } +#elif defined (HAVE_SHADOW) + if (is_response) { + return nc_server_ssh_cb_kbdint_shadow_response(cb_data->session, user); + } else { + return nc_server_ssh_cb_kbdint_shadow_request(cb_data->session, user, message); + } +#else + (void)is_response; + (void)user; + ERR(cb_data->session, "Keyboard-interactive method not supported."); + return SSH_AUTH_DENIED; +#endif +} + +int +nc_server_ssh_cb_auth_kbdint(ssh_message message, ssh_session UNUSED(libssh_sess), void *userdata) +{ + struct nc_server_ssh_cb_data *cb_data = (struct nc_server_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_auth_client *auth_client = NULL; + int local_users_supported = 0; + int ret = SSH_AUTH_DENIED; + const char *user; + + /* Extract the username from the message. */ + if (ssh_message_auth_kbdint_is_response(message) && session->username) { + user = session->username; + } else { + user = ssh_message_auth_user(message); + } + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + return SSH_AUTH_DENIED; + } + + /* determine kbdint path and execute the appropriate phase */ + if (!local_users_supported) { + /* system kbdint (PAM or shadow) */ + ret = nc_server_ssh_cb_kbdint_system(cb_data, message, user); + } else { + if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_NONE) { + /* client requested kbdint auth, but it is not configured for this user, so just deny */ + DBG(session, + "User \"%s\" does not have kbdint method configured, but a request was received.", session->username); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + if (server_opts.interactive_auth_clb) { + /* custom interactive auth callback */ + ret = server_opts.interactive_auth_clb(session, + session->ti.libssh.session, message, server_opts.interactive_auth_data); + } else if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_SYSTEM) { + ret = nc_server_ssh_cb_kbdint_system(cb_data, message, user); + } else { + /* add future methods here */ + ERR(session, "Keyboard-interactive authentication method not supported."); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + } + + /* handle the result from the kbdint system dispatch */ + if (ret == SSH_AUTH_INFO) { + /* prompts sent, waiting for client response — libssh sends no reply */ + return SSH_AUTH_INFO; + } else if (ret == SSH_AUTH_SUCCESS) { + VRB(session, "User \"%s\" authenticated via keyboard-interactive.", user); + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_INTERACTIVE); + } else { + VRB(session, "User \"%s\" authentication denied via keyboard-interactive.", user); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } +} + +/** + * @brief Callback function for SSH channel subsystem request. + * + * @param[in] libssh_sess SSH session object. + * @param[in] channel SSH channel the subsystem was requested on. + * @param[in] subsystem Requested subsystem name (expected "netconf"). + * @param[in] userdata Pointer to user data (struct nc_ssh_channel_cb_data). + * @return 0 on success. + * @return 1 on error (unknown subsystem, duplicate request, or memory error). + */ +static int +nc_server_ssh_cb_channel_subsystem(ssh_session UNUSED(libssh_sess), ssh_channel channel, const char *subsystem, void *userdata) +{ + struct nc_session *new_session; + struct nc_ssh_channel_cb_data *channel_data = userdata; + struct nc_server_ssh_cb_data *cb_data = channel_data->cb_data; + struct nc_session *session = cb_data->session; + + if (strcmp(subsystem, "netconf")) { + WRN(session, "Received an unknown subsystem \"%s\" request.", subsystem); + return 1; + } + + if (session->ti.libssh.channel == channel) { + /* first channel requested */ + if (session->ti.libssh.next || (session->status != NC_STATUS_STARTING)) { + ERRINT; + return 1; + } + if (session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { + ERR(session, "Subsystem \"netconf\" requested for the second time."); + return 1; + } + + session->flags |= NC_SESSION_SSH_SUBSYS_NETCONF; + } else { + /* additional channel subsystem request, new session is ready as far as SSH is concerned */ + new_session = nc_new_session(NC_SERVER, 1); + NC_CHECK_ERRMEM_RET(!new_session, 1); + + /* insert the new session */ + if (!session->ti.libssh.next) { + new_session->ti.libssh.next = session; + } else { + new_session->ti.libssh.next = session->ti.libssh.next; + } + session->ti.libssh.next = new_session; + + new_session->status = NC_STATUS_STARTING; + new_session->ti_type = NC_TI_SSH; + new_session->io_lock = session->io_lock; + new_session->ti.libssh.channel = channel; + new_session->ti.libssh.session = session->ti.libssh.session; + new_session->username = strdup(session->username); + new_session->host = strdup(session->host); + new_session->port = session->port; + new_session->ctx = (struct ly_ctx *)session->ctx; + new_session->flags = NC_SESSION_SSH_AUTHENTICATED | NC_SESSION_SSH_SUBSYS_NETCONF | NC_SESSION_SHAREDCTX; + + /* take over the ownership of the channel callbacks struct, freed with the new session */ + /* remove from the tracked list so it is not freed again when cb_data is freed */ + { + struct nc_ssh_channel_cb_data **p = &cb_data->channels; + + while (*p) { + if (*p == channel_data) { + *p = channel_data->next; + channel_data->next = NULL; + break; + } + p = &(*p)->next; + } + } + new_session->ti.libssh.channel_cb = &channel_data->channel_cb; + } + + return 0; +} + +ssh_channel +nc_server_ssh_cb_channel_open_request_session(ssh_session libssh_sess, void *userdata) +{ + struct nc_server_ssh_cb_data *cb_data = (struct nc_server_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + ssh_channel chan; + struct nc_ssh_channel_cb_data *channel_data; + + /* first channel request */ + if (!session->ti.libssh.channel && (session->status != NC_STATUS_STARTING)) { + ERRINT; + return NULL; + } + + /* create the new channel */ + chan = ssh_channel_new(libssh_sess); + if (!chan) { + ERR(session, "Session %u: failed to create a new SSH channel.", session->id); + return NULL; + } + + channel_data = calloc(1, sizeof(*channel_data)); + if (!channel_data) { + ssh_channel_free(chan); + return NULL; + } + /* userdata is the whole channel_data so the subsystem callback recovers both cb_data and, + * for additional channels, this structure itself to hand its ownership to the new session */ + channel_data->cb_data = cb_data; + channel_data->channel_cb.userdata = channel_data; + channel_data->channel_cb.channel_subsystem_request_function = nc_server_ssh_cb_channel_subsystem; + ssh_callbacks_init(&channel_data->channel_cb); + + /* Bind the subsystem callback to this specific channel */ + ssh_set_channel_callbacks(chan, &channel_data->channel_cb); + + if (!session->ti.libssh.channel) { + /* first channel - owned by the session, freed when the session is freed */ + session->ti.libssh.channel_cb = &channel_data->channel_cb; + session->ti.libssh.channel = chan; + } else { + /* additional channel - track on cb_data so it can be freed if it is never claimed + * by a netconf subsystem request (ownership transfers to a new session then) */ + channel_data->next = cb_data->channels; + cb_data->channels = channel_data; + } + + return chan; +} + +void +nc_server_ssh_cb_data_free(void *cb_data) +{ + struct nc_server_ssh_cb_data *data = (struct nc_server_ssh_cb_data *)cb_data; + struct nc_ssh_channel_cb_data *cur, *next; + + if (!data) { + return; + } + + /* free any channel callback data that was never claimed by a netconf subsystem request */ + for (cur = data->channels; cur; cur = next) { + next = cur->next; + free(cur); + } + + free(data); +} diff --git a/src/session_server_ssh_auth_message.c b/src/session_server_ssh_auth_message.c new file mode 100644 index 00000000..c9f41459 --- /dev/null +++ b/src/session_server_ssh_auth_message.c @@ -0,0 +1,907 @@ +/** + * @file session_server_ssh_auth_message.c + * @author Michal Vasko + * @brief libnetconf2 SSH authentication with messages + * + * @copyright + * Copyright (c) 2017 - 2021 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 "config.h" /* Expose HAVE_LIBPAM and HAVE_SHADOW */ + +#ifdef HAVE_LIBPAM +# include +#endif + +#include +#include +#include +#include +#include +#include + +#include "compat.h" +#include "log_p.h" +#include "session.h" +#include "session_p.h" +#include "session_server_ssh_wrapper.h" + +#ifdef HAVE_LIBPAM + +/** + * @brief PAM conversation function, which serves as a callback for exchanging messages between the client and a PAM module. + * + * @param[in] n_messages Number of messages. + * @param[in] msg PAM module's messages. + * @param[out] resp User responses. + * @param[in] appdata_ptr Callback's data. + * @return PAM_SUCCESS on success, PAM_BUF_ERR on memory allocation error, PAM_CONV_ERR otherwise. + */ +static int +nc_pam_conv_clb(int n_messages, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) +{ + int i, j, t, r = PAM_SUCCESS, n_answers, n_requests = n_messages; + const char **prompts = NULL; + char *echo = NULL; + const char *name = "Keyboard-Interactive Authentication"; + const char *instruction = "Please enter your authentication token"; + const char *answer; + struct nc_pam_thread_arg *clb_data = appdata_ptr; + ssh_session libssh_session; + + libssh_session = clb_data->session->ti.libssh.session; + + /* PAM_MAX_NUM_MSG == 32 by default */ + if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) { + ERR(clb_data->session, "Bad number of PAM messages (#%d).", n_messages); + r = PAM_CONV_ERR; + goto cleanup; + } + + /* only accepting these 4 types of messages */ + for (i = 0; i < n_messages; i++) { + t = msg[i]->msg_style; + if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) { + ERR(clb_data->session, "PAM conversation callback received an unexpected type of message."); + r = PAM_CONV_ERR; + goto cleanup; + } + } + + /* display messages with errors and/or some information and count the amount of actual authentication challenges */ + for (i = 0; i < n_messages; i++) { + if (msg[i]->msg_style == PAM_TEXT_INFO) { + VRB(clb_data->session, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg); + n_requests--; + } + if (msg[i]->msg_style == PAM_ERROR_MSG) { + ERR(clb_data->session, "PAM conversation callback received an error message (%s).", msg[i]->msg); + r = PAM_CONV_ERR; + goto cleanup; + } + } + + /* there are no requests left for the user, only messages with some information for the client were sent */ + if (n_requests <= 0) { + r = PAM_SUCCESS; + goto cleanup; + } + + /* it is the PAM module's responsibility to release both, this array and the responses themselves */ + *resp = calloc(n_requests, sizeof **resp); + prompts = calloc(n_requests, sizeof *prompts); + echo = calloc(n_requests, sizeof *echo); + NC_CHECK_ERRMEM_GOTO(!(*resp) || !prompts || !echo, r = PAM_BUF_ERR, cleanup); + + /* set the prompts for the user */ + j = 0; + for (i = 0; i < n_messages; i++) { + if ((msg[i]->msg_style == PAM_PROMPT_ECHO_ON) || (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF)) { + prompts[j++] = msg[i]->msg; + } + } + + /* iterate over all the messages and adjust the echo array accordingly */ + j = 0; + for (i = 0; i < n_messages; i++) { + if (msg[i]->msg_style == PAM_PROMPT_ECHO_ON) { + echo[j++] = 1; + } + if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) { + /* no need to set to 0 because of calloc */ + j++; + } + } + + /* print all the keyboard-interactive challenges to the user */ + r = ssh_message_auth_interactive_request(clb_data->msg, name, instruction, n_requests, prompts, echo); + if (r != SSH_OK) { + ERR(clb_data->session, "Failed to send an authentication request."); + r = PAM_CONV_ERR; + goto cleanup; + } + + n_answers = nc_server_ssh_kbdint_get_nanswers(clb_data->session, libssh_session); + if (n_answers < 0) { + /* timeout or dc */ + r = PAM_CONV_ERR; + goto cleanup; + } else if (n_answers != n_requests) { + /* check if the number of answers and requests matches */ + ERR(clb_data->session, "Expected %d response(s), got %d.", n_requests, n_answers); + r = PAM_CONV_ERR; + goto cleanup; + } + + /* give the replies to a PAM module */ + for (i = 0; i < n_answers; i++) { + answer = ssh_userauth_kbdint_getanswer(libssh_session, i); + if (!answer) { + ERR(clb_data->session, "Failed to get keyboard-interactive answer %d.", i); + for (j = 0; j < i; j++) { + free((*resp)[j].resp); + (*resp)[j].resp = NULL; + } + r = PAM_CONV_ERR; + goto cleanup; + } + (*resp)[i].resp = strdup(answer); + /* it should be the caller's responsibility to free this, however if mem alloc fails, + * it is safer to free the responses here and set them to NULL */ + if ((*resp)[i].resp == NULL) { + for (j = 0; j < i; j++) { + free((*resp)[j].resp); + (*resp)[j].resp = NULL; + } + ERRMEM; + r = PAM_BUF_ERR; + goto cleanup; + } + } + +cleanup: + free(prompts); + free(echo); + return r; +} + +/** + * @brief Handles authentication via Linux PAM. + * + * @param[in] session NETCONF session. + * @param[in] username Username of the client to auhtenticate. + * @param[in] ssh_msg SSH message with a keyboard-interactive authentication request. + * @return PAM_SUCCESS on success; + * @return PAM error otherwise. + */ +static int +nc_server_ssh_msg_auth_kbdint_pam(struct nc_session *session, const char *username, ssh_message ssh_msg) +{ + pam_handle_t *pam_h = NULL; + int ret; + struct nc_pam_thread_arg clb_data; + struct pam_conv conv; + + /* structure holding callback's data */ + clb_data.msg = ssh_msg; + clb_data.session = session; + + /* PAM conversation structure holding the callback and it's data */ + conv.conv = nc_pam_conv_clb; + conv.appdata_ptr = &clb_data; + + if (!server_opts.pam_config_name) { + ERR(session, "PAM configuration filename not set."); + ret = 1; + goto cleanup; + } + + /* initialize PAM and see if the given configuration file exists */ + ret = pam_start(server_opts.pam_config_name, username, &conv, &pam_h); + if (ret != PAM_SUCCESS) { + ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + goto cleanup; + } + + /* authentication based on the modules listed in the configuration file */ + ret = pam_authenticate(pam_h, 0); + if (ret != PAM_SUCCESS) { + if (ret == PAM_ABORT) { + ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + goto cleanup; + } else { + VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + goto cleanup; + } + } + + /* correct token entered, check other requirements(the time of the day, expired token, ...) */ + ret = pam_acct_mgmt(pam_h, 0); + if ((ret != PAM_SUCCESS) && (ret != PAM_NEW_AUTHTOK_REQD)) { + VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + goto cleanup; + } + + /* if a token has expired a new one will be generated */ + if (ret == PAM_NEW_AUTHTOK_REQD) { + VRB(session, "PAM warning occurred (%s).", pam_strerror(pam_h, ret)); + ret = pam_chauthtok(pam_h, PAM_CHANGE_EXPIRED_AUTHTOK); + if (ret == PAM_SUCCESS) { + VRB(session, "The authentication token of user \"%s\" updated successfully.", username); + } else { + ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + goto cleanup; + } + } + +cleanup: + /* destroy the PAM context */ + if (pam_h && (pam_end(pam_h, ret) != PAM_SUCCESS)) { + ERR(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret)); + } + return ret; +} + +#elif defined (HAVE_SHADOW) + +/** + * @brief Authenticate using credentials stored in the system. + * + * @param[in] session Session to authenticate on. + * @param[in] username Username of the client to authenticate. + * @param[in] msg SSH message that originally requested kbdint authentication. + * + * @return 0 on success, non-zero otherwise. + */ +static int +nc_server_ssh_msg_auth_kbdint_passwd(struct nc_session *session, const char *username, ssh_message msg) +{ + int ret = 0, n_answers; + const char *name = "Keyboard-Interactive Authentication"; + const char *instruction = "Please enter your authentication token"; + char *prompt = NULL, *pw = NULL, *received_pw = NULL; + char echo[] = {0}; + + /* try to get the client's pw hash from the system */ + pw = nc_server_ssh_get_pwd_hash(username); + if (!pw) { + ret = 1; + goto cleanup; + } + + ret = asprintf(&prompt, "%s's password:", username); + NC_CHECK_ERRMEM_GOTO(ret == -1, prompt = NULL; ret = 1, cleanup); + + /* send the password prompt to the client */ + ret = ssh_message_auth_interactive_request(msg, name, instruction, 1, (const char **) &prompt, echo); + if (ret) { + ERR(session, "Failed to send an authentication request to client \"%s\".", username); + goto cleanup; + } + + /* get the reply */ + n_answers = nc_server_ssh_kbdint_get_nanswers(session, session->ti.libssh.session); + if (n_answers < 0) { + /* timeout or dc */ + ret = 1; + goto cleanup; + } else if (n_answers != 1) { + /* only expecting a single answer */ + ERR(session, "Unexpected amount of answers in system auth. Expected 1, got \"%d\".", n_answers); + ret = 1; + goto cleanup; + } + received_pw = strdup(ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0)); + NC_CHECK_ERRMEM_GOTO(!received_pw, ret = 1, cleanup); + + /* cmp the passwords */ + ret = nc_server_ssh_compare_password(pw, received_pw); + +cleanup: + free(pw); + free(received_pw); + free(prompt); + return ret; +} + +#endif /* HAVE_SHADOW */ + +/** + * @brief Keyboard-interactive authentication method using the system's authentication methods. + * + * @param[in] session NETCONF session. + * @param[in] msg SSH message with a keyboard-interactive authentication request. + * @return 0 on success, non-zero otherwise. + */ +static int +nc_server_ssh_msg_auth_kbdint_system(struct nc_session *session, ssh_message msg) +{ + int rc; + +#ifdef HAVE_LIBPAM + /* authenticate using PAM */ + rc = nc_server_ssh_msg_auth_kbdint_pam(session, session->username, msg); +#elif defined (HAVE_SHADOW) + /* authenticate using /etc/passwd and /etc/shadow */ + rc = nc_server_ssh_msg_auth_kbdint_passwd(session, session->username, msg); +#else + (void)session; + (void)msg; + + ERR(NULL, "Keyboard-interactive method not supported."); + rc = 1; +#endif + + return rc; +} + +/** + * @brief Handle authentication request for the None method. + * + * @param[in] local_users_supported Whether the server supports local users. + * @param[in] auth_client Configured client's authentication data. + * @param[in] msg libssh message. + * @return 0 if the authentication was successful, -1 if not (@p msg already replied to). + */ +static int +nc_server_ssh_msg_auth_none(int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) +{ + assert(!local_users_supported || auth_client); + + if (local_users_supported && auth_client->none_enabled) { + return 0; + } + + ssh_message_reply_default(msg); + return -1; +} + +/** + * @brief Handle authentication request for the Password method. + * + * @param[in] session NETCONF session. + * @param[in] local_users_supported Whether the server supports local users. + * @param[in] auth_client Configured client's authentication data. + * @param[in] msg libssh message. + * @return 0 if the authentication was successful, 1 if not (@p msg not yet replied to). + */ +static int +nc_server_ssh_msg_auth_password(struct nc_session *session, int local_users_supported, + struct nc_auth_client *auth_client, ssh_message msg) +{ + int rc; + char *password = NULL; + + assert(!local_users_supported || auth_client); + + if (local_users_supported) { + /* obtain pw from config */ + password = auth_client->password; + if (!password) { + /* client requested password auth, but it is not configured for this user, so just deny */ + DBG(session, + "User \"%s\" does not have password method configured, but a request was received.", session->username); + return 1; + } + } else { +#ifdef HAVE_SHADOW + /* obtain pw from system, this one needs to be free'd */ + password = nc_server_ssh_get_pwd_hash(session->username); + if (!password) { + return 1; + } +#else + ERR(session, "Obtaining password from system not supported."); + return 1; +#endif + } + + /* compare the passwords */ + rc = nc_server_ssh_compare_password(password, ssh_message_auth_password(msg)); + + if (!local_users_supported) { + free(password); + } + + return rc ? 1 : 0; +} + +/** + * @brief Handle authentication request for the Publickey method. + * + * @param[in] session NETCONF session. + * @param[in] local_users_supported Whether the server supports local users. + * @param[in] auth_client Configured client's authentication data. + * @param[in] msg libssh message. + * @return 0 if the authentication was successful, 1 if not and the @p msg not yet replied to, -1 if not and @p msg was replied to. + */ +static int +nc_server_ssh_msg_auth_pubkey(struct nc_session *session, int local_users_supported, + struct nc_auth_client *auth_client, ssh_message msg) +{ + int signature_state, ret = 0; + struct nc_public_key *pubkeys = NULL; + uint32_t pubkey_count = 0, i; + + assert(!local_users_supported || auth_client); + + /* get the public keys */ + if (!local_users_supported) { + /* system user, get the keys from the system (these need to be free'd as they're not in the config) */ + ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else { + if (auth_client->pubkey_store == NC_STORE_UNKNOWN) { + /* client requested pubkey auth, but it is not configured for this user, so just deny */ + DBG(session, + "User \"%s\" does not have public key method configured, but a request was received.", session->username); + return 1; + } + + if (auth_client->pubkey_store == NC_STORE_SYSTEM) { + /* get the keys from the system (these need to be free'd as they're not in the config) */ + ret = nc_server_ssh_get_system_keys(session->username, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else if (auth_client->pubkey_store == NC_STORE_LOCAL) { + /* saved directly in the user's config */ + pubkeys = auth_client->pubkeys; + pubkey_count = LY_ARRAY_COUNT(auth_client->pubkeys); + } else if (auth_client->pubkey_store == NC_STORE_TRUSTSTORE) { + /* need to fetch from the truststore */ + ret = nc_server_ssh_ts_ref_get_keys(auth_client->ts_ref, &pubkeys, &pubkey_count); + if (ret) { + goto cleanup; + } + } else { + ERRINT; + return 1; + } + } + + /* compare the received pubkey with the authorized ones */ + if (nc_server_ssh_auth_pubkey_compare_key(ssh_message_auth_pubkey(msg), pubkeys, pubkey_count)) { + VRB(session, "User \"%s\" tried to use an unknown (unauthorized) public key.", session->username); + ret = 1; + goto cleanup; + } + + signature_state = ssh_message_auth_publickey_state(msg); + if (signature_state == SSH_PUBLICKEY_STATE_NONE) { + /* accepting only the use of a public key */ + ssh_message_auth_reply_pk_ok_simple(msg); + ret = -1; + } + +cleanup: + if (!local_users_supported || (auth_client->pubkey_store == NC_STORE_SYSTEM)) { + for (i = 0; i < pubkey_count; i++) { + free(pubkeys[i].name); + free(pubkeys[i].data); + } + free(pubkeys); + } + + return ret; +} + +/** + * @brief Handle authentication request for the Keyboard-interactive method. + * + * @param[in] session NETCONF session. + * @param[in] local_users_supported Whether the server supports local users. + * @param[in] auth_client Configured client's authentication data. + * @param[in] msg libssh message. + * @return 0 if the authentication was successful, 1 if not. + */ +static int +nc_server_ssh_msg_auth_kbdint(struct nc_session *session, int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) +{ + int r = 0; + + assert(!local_users_supported || auth_client); + + if (!local_users_supported) { + /* no local users supported, use the system method */ + r = nc_server_ssh_msg_auth_kbdint_system(session, msg); + } else { + if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_NONE) { + /* client requested kbdint auth, but it is not configured for this user, so just deny */ + DBG(session, + "User \"%s\" does not have kbdint method configured, but a request was received.", session->username); + return 1; + } + + if (server_opts.interactive_auth_clb) { + /* custom callback has higher priority */ + r = server_opts.interactive_auth_clb(session, + session->ti.libssh.session, msg, server_opts.interactive_auth_data); + } else { + /* perform the authentication based on the configured method */ + if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_SYSTEM) { + r = nc_server_ssh_msg_auth_kbdint_system(session, msg); + } else { + /* add future methods here */ + ERR(session, "Keyboard-interactive authentication method not supported."); + return 1; + } + } + } + + return r ? 1 : 0; +} + +/** + * @brief Handle SSH channel open request. + * + * @param[in] session NETCONF session. + * @param[in] msg libssh message. + * @return 0 on success, -1 on failure. + */ +static int +nc_server_ssh_msg_channel_open(struct nc_session *session, ssh_message msg) +{ + ssh_channel chan; + + /* first channel request */ + if (!session->ti.libssh.channel) { + if (session->status != NC_STATUS_STARTING) { + ERRINT; + return -1; + } + chan = ssh_message_channel_request_open_reply_accept(msg); + if (!chan) { + ERR(session, "Failed to create a new SSH channel."); + return -1; + } + session->ti.libssh.channel = chan; + + /* additional channel request */ + } else { + chan = ssh_message_channel_request_open_reply_accept(msg); + if (!chan) { + ERR(session, "Session %u: failed to create a new SSH channel.", session->id); + return -1; + } + /* channel was created and libssh stored it internally in the ssh_session structure, good enough */ + } + + return 0; +} + +/** + * @brief Handle SSH channel request subsystem request. + * + * @param[in] session NETCONF session. + * @param[in] channel Requested SSH channel. + * @param[in] subsystem Name of the requested subsystem. + * @return 0 on success, -1 on failure. + */ +static int +nc_server_ssh_msg_channel_subsystem(struct nc_session *session, ssh_channel channel, const char *subsystem) +{ + struct nc_session *new_session; + + if (strcmp(subsystem, "netconf")) { + WRN(session, "Received an unknown subsystem \"%s\" request.", subsystem); + return -1; + } + + if (session->ti.libssh.channel == channel) { + /* first channel requested */ + if (session->ti.libssh.next || (session->status != NC_STATUS_STARTING)) { + ERRINT; + return -1; + } + if (session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { + ERR(session, "Subsystem \"netconf\" requested for the second time."); + return -1; + } + + session->flags |= NC_SESSION_SSH_SUBSYS_NETCONF; + } else { + /* additional channel subsystem request, new session is ready as far as SSH is concerned */ + new_session = nc_new_session(NC_SERVER, 1); + NC_CHECK_ERRMEM_RET(!new_session, -1); + + /* insert the new session */ + if (!session->ti.libssh.next) { + new_session->ti.libssh.next = session; + } else { + new_session->ti.libssh.next = session->ti.libssh.next; + } + session->ti.libssh.next = new_session; + + new_session->status = NC_STATUS_STARTING; + new_session->ti_type = NC_TI_SSH; + new_session->io_lock = session->io_lock; + new_session->ti.libssh.channel = channel; + new_session->ti.libssh.session = session->ti.libssh.session; + new_session->username = strdup(session->username); + new_session->host = strdup(session->host); + new_session->port = session->port; + new_session->ctx = (struct ly_ctx *)session->ctx; + new_session->flags = NC_SESSION_SSH_AUTHENTICATED | NC_SESSION_SSH_SUBSYS_NETCONF | NC_SESSION_SHAREDCTX; + } + + return 0; +} + +/** + * @brief Handle NETCONF SSH authentication. + * + * @param[in] session NETCONF session. + * @param[in] opts SSH server options. + * @param[in] msg libssh message. + * @param[in] method Type of the authentication method. + * @param[in] str_method String representation of the authentication method. + * @param[in] local_users_supported Whether the server supports local users. + * @param[in,out] auth_state Authentication state. + * @return 1 in case of a fatal error, 0 otherwise. + */ +static int +nc_server_ssh_msg_auth(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, + int method, const char *str_method, int local_users_supported, struct nc_auth_state *auth_state) +{ + const char *username; + int ret = 0; + struct nc_auth_client *auth_client = NULL; + + /* save the username, do not let the client change it */ + username = ssh_message_auth_user(msg); + assert(username); + + if (local_users_supported) { + auth_client = nc_ssh_find_auth_client(opts, username); + + if (!auth_client) { + /* user not known, set his authentication methods to public key only so that + * there is no interaction and it will simply be denied */ + ERR(NULL, "User \"%s\" not known by the server.", username); + ssh_set_auth_methods(session->ti.libssh.session, SSH_AUTH_METHOD_PUBLICKEY); + ssh_message_reply_default(msg); + return 0; + } + } + + if (!session->username) { + session->username = strdup(username); + NC_CHECK_ERRMEM_RET(!session->username, 1); + + /* send the SSH issue banner on the first userauth request */ + nc_server_ssh_send_banner(session, opts); + + /* configure and count accepted auth methods */ + nc_ssh_auth_state_init(session, auth_state, local_users_supported, auth_client); + } else { + if (strcmp(username, session->username)) { + /* changing username not allowed */ + ERR(session, "User \"%s\" changed its username to \"%s\".", session->username, username); + session->status = NC_STATUS_INVALID; + session->term_reason = NC_SESSION_TERM_OTHER; + return 1; + } + } + + /* try authenticating, if local users are supported, then the configured user must authenticate via all of his + * configured auth methods, otherwise for system users just one is needed, + * 0 return indicates success, 1 fail (msg not yet replied to), -1 fail (msg was replied to) */ + if (method == SSH_AUTH_METHOD_NONE) { + ret = nc_server_ssh_msg_auth_none(local_users_supported, auth_client, msg); + } else if (method == SSH_AUTH_METHOD_PASSWORD) { + ret = nc_server_ssh_msg_auth_password(session, local_users_supported, auth_client, msg); + } else if (method == SSH_AUTH_METHOD_PUBLICKEY) { + ret = nc_server_ssh_msg_auth_pubkey(session, local_users_supported, auth_client, msg); + } else if (method == SSH_AUTH_METHOD_INTERACTIVE) { + ret = nc_server_ssh_msg_auth_kbdint(session, local_users_supported, auth_client, msg); + } else { + ++session->opts.server.ssh_auth_attempts; + VRB(session, "Authentication method \"%s\" not supported.", str_method); + ssh_message_reply_default(msg); + return 0; + } + + if (!ret) { + int success = nc_ssh_auth_success(session, auth_state, method); + + if (success == SSH_AUTH_PARTIAL) { + ssh_message_auth_reply_success(msg, 1); + } else { + ssh_message_auth_reply_success(msg, 0); + VRB(session, "User \"%s\" authenticated.", username); + } + } else if (ret == 1) { + /* failed attempt, msg wasnt yet replied to */ + ++session->opts.server.ssh_auth_attempts; + VRB(session, "Failed user \"%s\" authentication attempt (#%d).", session->username, + session->opts.server.ssh_auth_attempts); + ssh_message_reply_default(msg); + } + + return 0; +} + +int +nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, struct nc_auth_state *auth_state) +{ + const char *str_type, *str_subtype = NULL; + int subtype, type, local_users_supported; + + type = ssh_message_type(msg); + subtype = ssh_message_subtype(msg); + + switch (type) { + case SSH_REQUEST_AUTH: + str_type = "request-auth"; + switch (subtype) { + case SSH_AUTH_METHOD_NONE: + str_subtype = "none"; + break; + case SSH_AUTH_METHOD_PASSWORD: + str_subtype = "password"; + break; + case SSH_AUTH_METHOD_PUBLICKEY: + str_subtype = "publickey"; + break; + case SSH_AUTH_METHOD_HOSTBASED: + str_subtype = "hostbased"; + break; + case SSH_AUTH_METHOD_INTERACTIVE: + str_subtype = "interactive"; + break; + case SSH_AUTH_METHOD_GSSAPI_MIC: + str_subtype = "gssapi-mic"; + break; + } + break; + + case SSH_REQUEST_CHANNEL_OPEN: + str_type = "request-channel-open"; + switch (subtype) { + case SSH_CHANNEL_SESSION: + str_subtype = "session"; + break; + case SSH_CHANNEL_DIRECT_TCPIP: + str_subtype = "direct-tcpip"; + break; + case SSH_CHANNEL_FORWARDED_TCPIP: + str_subtype = "forwarded-tcpip"; + break; + case (int)SSH_CHANNEL_X11: + str_subtype = "channel-x11"; + break; + case SSH_CHANNEL_UNKNOWN: + /* fallthrough */ + default: + str_subtype = "unknown"; + break; + } + break; + + case SSH_REQUEST_CHANNEL: + str_type = "request-channel"; + switch (subtype) { + case SSH_CHANNEL_REQUEST_PTY: + str_subtype = "pty"; + break; + case SSH_CHANNEL_REQUEST_EXEC: + str_subtype = "exec"; + break; + case SSH_CHANNEL_REQUEST_SHELL: + str_subtype = "shell"; + break; + case SSH_CHANNEL_REQUEST_ENV: + str_subtype = "env"; + break; + case SSH_CHANNEL_REQUEST_SUBSYSTEM: + str_subtype = "subsystem"; + break; + case SSH_CHANNEL_REQUEST_WINDOW_CHANGE: + str_subtype = "window-change"; + break; + case SSH_CHANNEL_REQUEST_X11: + str_subtype = "x11"; + break; + case SSH_CHANNEL_REQUEST_UNKNOWN: + /* fallthrough */ + default: + str_subtype = "unknown"; + break; + } + break; + + case SSH_REQUEST_SERVICE: + str_type = "request-service"; + str_subtype = ssh_message_service_service(msg); + break; + + case SSH_REQUEST_GLOBAL: + str_type = "request-global"; + switch (subtype) { + case SSH_GLOBAL_REQUEST_TCPIP_FORWARD: + str_subtype = "tcpip-forward"; + break; + case SSH_GLOBAL_REQUEST_CANCEL_TCPIP_FORWARD: + str_subtype = "cancel-tcpip-forward"; + break; + case SSH_GLOBAL_REQUEST_UNKNOWN: + /* fallthrough */ + default: + str_subtype = "unknown"; + break; + } + break; + + default: + str_type = "unknown"; + str_subtype = "unknown"; + break; + } + + VRB(session, "Received an SSH message \"%s\" of subtype \"%s\".", str_type, str_subtype); + if (!session || (session->status == NC_STATUS_CLOSING) || (session->status == NC_STATUS_INVALID)) { + /* "valid" situation if, for example, receiving some auth or channel request timeouted, + * but we got it now, during session free */ + VRB(session, "SSH message arrived on a %s session, the request will be denied.", + (session && session->status == NC_STATUS_CLOSING ? "closing" : "invalid")); + ssh_message_reply_default(msg); + return 0; + } + + /* + * process known messages + */ + if (type == SSH_REQUEST_AUTH) { + if (session->flags & NC_SESSION_SSH_AUTHENTICATED) { + ERR(session, "User \"%s\" authenticated, but requested another authentication.", session->username); + ssh_message_reply_default(msg); + return 0; + } else if (!auth_state || !opts) { + /* these two parameters should always be set during an authentication, + * however do a check just in case something goes really wrong, since they + * are not needed for other types of messages + */ + ERRINT; + return 1; + } + + /* check if local-users-supported feature is enabled */ + local_users_supported = nc_ssh_check_local_user_support(session); + if (local_users_supported < 0) { + return 1; + } + + /* authenticate */ + return nc_server_ssh_msg_auth(session, opts, msg, subtype, str_subtype, local_users_supported, auth_state); + } else if (session->flags & NC_SESSION_SSH_AUTHENTICATED) { + if ((type == SSH_REQUEST_CHANNEL_OPEN) && ((enum ssh_channel_type_e)subtype == SSH_CHANNEL_SESSION)) { + if (nc_server_ssh_msg_channel_open(session, msg)) { + ssh_message_reply_default(msg); + } + return 0; + + } else if ((type == SSH_REQUEST_CHANNEL) && ((enum ssh_channel_requests_e)subtype == SSH_CHANNEL_REQUEST_SUBSYSTEM)) { + if (nc_server_ssh_msg_channel_subsystem(session, ssh_message_channel_request_channel(msg), + ssh_message_channel_request_subsystem(msg))) { + ssh_message_reply_default(msg); + } else { + ssh_message_channel_request_reply_success(msg); + } + return 0; + } + } + + /* we did not process it */ + return 1; +} diff --git a/src/session_server_ssh_wrapper.h b/src/session_server_ssh_wrapper.h new file mode 100644 index 00000000..264df6c6 --- /dev/null +++ b/src/session_server_ssh_wrapper.h @@ -0,0 +1,316 @@ +/** + * @file session_server_ssh_wrapper.h + * @author Petr Hanzlik + * @brief libnetconf2 - header for wrapped SSH server library function calls + * + * @copyright + * Copyright (c) 2026 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 + */ + +#ifndef _SESSION_SERVER_SSH_WRAPPER_H_ +#define _SESSION_SERVER_SSH_WRAPPER_H_ + +#include "config.h" /* Expose HAVE_LIBPAM, HAVE_SHADOW */ + +#ifdef HAVE_LIBPAM +# include +#endif +#ifdef HAVE_SHADOW +# include +#endif + +#include + +#include "session_p.h" + +#define LIBSSH_0_12 (LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 12, 0)) + +#if LIBSSH_0_12 + +#ifdef HAVE_LIBPAM + +#include + +/** + * @brief PAM thread bridge state for callback-based keyboard-interactive auth. + * + * PAM's pam_authenticate() is synchronous, but the libssh callback-based kbdint + * protocol is asynchronous (callback must return SSH_AUTH_INFO and be called + * again with the response). This structure bridges the two by running + * pam_authenticate in a separate thread, with condvar-based communication. + * + * State machine (all transitions protected by @p lock, waiters re-check + * @p state in a while loop): + * + * RUNNING --(PAM thread: prompts)--> PROMPTS_READY --(main thread: answers)--> ANSWERS_READY + * ^ | + * | (PAM thread: consumed answers, new prompts) | + * +---------------------------------------------------------------------------------+ + * RUNNING/PROMPTS_READY/ANSWERS_READY --(PAM thread: finished)--> DONE + * RUNNING/PROMPTS_READY --(main thread: timeout/disconnect)--> CANCELLED --> DONE + * + * The main thread owns transitions out of NC_PAM_ANSWERS_READY and the PAM + * thread owns transitions out of NC_PAM_RUNNING/NC_PAM_PROMPTS_READY, so at + * any moment at most one thread waits on @p changed and the other may signal + * it. + * + * Invariants: + * - While state == NC_PAM_PROMPTS_READY the PAM thread is parked on + * @p changed, so the prompt strings/arrays it handed to the main thread + * stay valid until the main thread leaves that state. + * - NC_PAM_ANSWERS_READY is never observed by nc_server_ssh_cb_pam_cancel(), because + * cancel runs only in the main thread, which owns that state and leaves it + * itself (to PROMPTS_READY or DONE) before any cancel can happen. + */ +struct nc_server_ssh_cb_pam_data { + pthread_t thread; /**< PAM thread handle. */ + pthread_mutex_t lock; /**< Protects all fields below. */ + pthread_cond_t changed; /**< Signalled on every state change. */ + + /** PAM bridge state. */ + enum { + NC_PAM_RUNNING, /**< PAM thread is processing, no prompts yet. */ + NC_PAM_PROMPTS_READY, /**< PAM thread has prompts, waiting for answers. */ + NC_PAM_ANSWERS_READY, /**< Main thread has answers, PAM should consume. */ + NC_PAM_DONE, /**< PAM authentication complete (check pam_ret). */ + NC_PAM_CANCELLED /**< Cancelled by main thread (disconnect/timeout). */ + } state; + + /* Prompts (PAM thread sets, main thread reads, while holding lock) */ + int n_prompts; /**< Number of prompts. */ + const char **prompts; /**< Prompt strings. */ + char *echo; /**< Echo flags. */ + + /* Answers (main thread sets, PAM thread reads, while holding lock) */ + int n_answers; /**< Number of answers. */ + char **answers; /**< Answer strings. */ + + int pam_ret; /**< PAM return code (valid when state == DONE). */ + pam_handle_t *pam_h; /**< PAM handle (owned and freed by PAM thread). */ + + const char *username; /**< Username for pam_start. */ + struct nc_session *session; /**< NETCONF session for logging. */ +}; + +/** + * @brief Cancel the PAM thread and clean up (called on auth failure/timeout/disconnect). + * + * @param[in] data PAM data (may be NULL). + */ +void nc_server_ssh_cb_pam_cancel(struct nc_server_ssh_cb_pam_data *data); + +#endif /* HAVE_LIBPAM */ + +/** @brief Data structure passed to SSH callback functions. */ +struct nc_server_ssh_cb_data { + struct ssh_server_callbacks_struct server_cb; /**< libssh server callbacks. */ + struct nc_session *session; /**< The current session. */ + struct nc_server_ssh_opts *opts; /**< SSH server options. */ + struct nc_auth_state auth_state; /**< Tracks multi-method authentication state. */ + struct nc_ssh_channel_cb_data *channels; /**< List of additional channel callback data, + tracked so non-netconf channels can be freed. */ +#ifdef HAVE_LIBPAM + struct nc_server_ssh_cb_pam_data *pam_kbdint; /**< PAM thread bridge state. */ +#endif +}; + +/** + * @brief libssh channel callbacks struct together with its owner. + * + * @remark channel_cb MUST stay the first member, so that a plain free() of + * nc_session->ti.libssh.channel_cb frees the whole structure. + */ +struct nc_ssh_channel_cb_data { + struct ssh_channel_callbacks_struct channel_cb; /**< libssh channel callbacks (MUST be first). */ + struct nc_server_ssh_cb_data *cb_data; /**< Shared SSH-session callback data. */ + struct nc_ssh_channel_cb_data *next; /**< Next in the cb_data->channels list. */ +}; + +/** +* @brief Callback function for SSH authentication with none method. +* +* @param[in] libssh_sess SSH session object. +* @param[in] user Username attempting to authenticate. +* @param[in] userdata Pointer to user data (struct nc_server_ssh_cb_data). +* @return SSH_AUTH_SUCCESS if authentication is successful. +* @return SSH_AUTH_DENIED otherwise. +*/ +int nc_server_ssh_cb_auth_none(ssh_session libssh_sess, const char *user, void *userdata); + +/** +* @brief Callback function for SSH authentication with password method. +* +* @param[in] libssh_sess SSH session object. +* @param[in] user Username attempting to authenticate. +* @param[in] password Password provided by the user. +* @param[in] userdata Pointer to user data (struct nc_server_ssh_cb_data). +* @return SSH_AUTH_SUCCESS if authentication is successful. +* @return SSH_AUTH_DENIED otherwise. +*/ +int nc_server_ssh_cb_auth_password(ssh_session libssh_sess, const char *user, const char *password, void *userdata); + +/** + * @brief Callback function for SSH public key authentication. + * + * @param[in] libssh_sess SSH session object. + * @param[in] user Username attempting to authenticate. + * @param[in] pubkey Public key provided by the user. + * @param[in] signature_state Whether this is a probe (NONE) or signed auth (VALID). + * @param[in] userdata Pointer to user data (struct nc_server_ssh_cb_data). + * @return SSH_AUTH_SUCCESS if authentication is successful (or probe accepted). + * @return SSH_AUTH_DENIED otherwise. + */ +int nc_server_ssh_cb_auth_pubkey(ssh_session libssh_sess, const char *user, struct ssh_key_struct *pubkey, char signature_state, void *userdata); + +/** + * @brief Callback function for SSH keyboard-interactive authentication. + * + * @param[in] message SSH message containing the auth request or response. + * @param[in] libssh_sess SSH session object. + * @param[in] userdata Pointer to user data (struct nc_server_ssh_cb_data). + * @return SSH_AUTH_INFO if prompts were sent (waiting for client response). + * @return SSH_AUTH_SUCCESS if authentication is successful. + * @return SSH_AUTH_DENIED otherwise. + */ +int nc_server_ssh_cb_auth_kbdint(ssh_message message, ssh_session libssh_sess, void *userdata); + +/** + * @brief Callback function for SSH channel open request. + * + * @param[in] libssh_sess SSH session object. + * @param[in] userdata Pointer to user data (struct nc_server_ssh_cb_data). + * @return The new SSH channel on success. + * @return NULL on failure. + */ +ssh_channel nc_server_ssh_cb_channel_open_request_session(ssh_session libssh_sess, void *userdata); + +#else + +/** + * @brief Process a SSH message. + * + * @param[in] session Session structure of the connection. + * @param[in] opts Endpoint SSH options on which the session was created. + * @param[in] msg SSH message itself. + * @param[in] auth_state State of the authentication. + * @return 0 if the message was handled, 1 if it is left up to libssh. + */ +int nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, struct nc_auth_state *auth_state); + +#endif + +/** + * @brief Free SSH callback data, reclaiming any channel callback data for channels + * that were never claimed by a NETCONF subsystem request. + * + * @param[in] cb_data Callback data to free, may be NULL. + */ +void nc_server_ssh_cb_data_free(void *cb_data); + +/** + * @brief Check if local users are supported via the ietf-ssh-server YANG model. + * + * @param[in] session NETCONF session. + * @return 1 if local users are supported. + * @return 0 if local users are not supported. + * @return -1 on fatal error. + */ +int nc_ssh_check_local_user_support(struct nc_session *session); + +/** + * @brief Find an authentication client for a given username. + * + * @param[in] opts SSH server options. + * @param[in] user Username to search for. + * @return Pointer to the authentication client if found. + * @return NULL otherwise. + */ +struct nc_auth_client *nc_ssh_find_auth_client(struct nc_server_ssh_opts *opts, const char *user); + +/** + * @brief Initialize the authentication state for multi-method authentication. + * + * @param[in] session NETCONF session. + * @param[in,out] auth_state Authentication state to initialize. + * @param[in] local_users_supported Whether local users are supported. + * @param[in] auth_client The authenticated client configuration (may be NULL if !local_users_supported). + */ +void nc_ssh_auth_state_init(struct nc_session *session, struct nc_auth_state *auth_state, + int local_users_supported, struct nc_auth_client *auth_client); + +/** + * @brief Handle a successful authentication attempt, tracking partial/multi-method auth. + * + * @param[in] session NETCONF session. + * @param[in,out] auth_state Authentication state. + * @param[in] method The SSH auth method that succeeded. + * @return SSH_AUTH_SUCCESS if fully authenticated. + * @return SSH_AUTH_PARTIAL if more methods are needed. + */ +int nc_ssh_auth_success(struct nc_session *session, struct nc_auth_state *auth_state, int method); + +/** + * @brief Send the SSH issue banner if configured. + * + * @param[in] session NETCONF session. + * @param[in] opts SSH server options. + */ +void nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts *opts); + +/** + * @brief Compare SSH key with configured authorized keys. + * + * @param[in] key Presented SSH key to compare. + * @param[in] pubkeys Configured public keys to compare against. + * @param[in] pubkey_count Number of @p pubkeys. + * @return 0 if a match was found. + * @return 1 if no match was found. + */ +int nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys, uint16_t pubkey_count); + +/** + * @brief Get public keys from the truststore. + * + * @param[in] referenced_name Name of the public key bag in the truststore. + * @param[out] pubkeys Referenced public keys. + * @param[out] pubkey_count Referenced public key count. + * @return 0 on success, 1 on error. + */ +int nc_server_ssh_ts_ref_get_keys(const char *referenced_name, struct nc_public_key **pubkeys, uint32_t *pubkey_count); + +/** + * @brief Get user's public keys from the system. + * + * @param[in] username Username. + * @param[out] pubkeys User's public keys. + * @param[out] pubkey_count Public key count. + * @return 0 on success, non-zero on error. + */ +int nc_server_ssh_get_system_keys(const char *username, struct nc_public_key **pubkeys, uint32_t *pubkey_count); + +/** + * @brief Compare stored hashed password with a cleartext received password. + * + * @param[in] stored_pw Hashed stored password. + * @param[in] received_pw Cleartext received password. + * @return 0 on match, non-zero otherwise. + */ +int nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw); + +#ifdef HAVE_SHADOW +/** + * @brief Get the user's hashed password from the system. + * + * @param[in] username Username. + * @return User's hashed password or NULL on error. + */ +char * nc_server_ssh_get_pwd_hash(const char *username); +#endif /* HAVE_SHADOW */ + +#endif /* _SESSION_SERVER_SSH_WRAPPER_H_ */