diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ca5954b..29a38914 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -273,6 +273,11 @@ if(ENABLE_SSH_TLS) list(APPEND CMAKE_REQUIRED_LIBRARIES ${LIBSSH_LIBRARIES}) include_directories(${LIBSSH_INCLUDE_DIRS}) + # check if libssh version is >= 0.12.0 + if(LIBSSH_VERSION VERSION_GREATER_EQUAL "0.12.0") + set(HAVE_LIBSSH_0_12 TRUE) + endif() + # dependencies - libcurl find_package(CURL 7.30.0 REQUIRED) if(TARGET CURL::libcurl) diff --git a/src/config.h.in b/src/config.h.in index 557114e2..7c08f429 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -95,4 +95,7 @@ /* Comply with the O-RAN WG11 security spec (client cert Extended Key Usage and Key Usage checks). */ #cmakedefine NC_COMPLY_WITH_ORAN_WG11 +/* Support for libssh >= 0.12.0 callback API */ +#cmakedefine HAVE_LIBSSH_0_12 + #endif /* NC_CONFIG_H_ */ diff --git a/src/session.c b/src/session.c index e71c668e..962236ad 100644 --- a/src/session.c +++ b/src/session.c @@ -944,6 +944,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 +966,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 +984,9 @@ nc_session_free_transport(struct nc_session *session, int *multisession) sock = -1; #endif + /* free heap-allocated callback data (libssh >= 0.12) */ + free(session->ti.libssh.cb_data); + /* closes sock if set */ ssh_free(session->ti.libssh.session); } else { @@ -994,6 +999,11 @@ 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; + } } /* SESSION IO UNLOCK */ diff --git a/src/session_p.h b/src/session_p.h index d5482854..921d9e31 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -801,6 +801,10 @@ struct nc_server_opts { void *interactive_auth_data; void (*interactive_auth_data_free)(void *data); + int (*interactive_auth_clb_v2)(const struct nc_session *session, ssh_message msg, int is_response, void *user_data); + void *interactive_auth_v2_data; + void (*interactive_auth_v2_data_free)(void *data); + int (*user_verify_clb)(const struct nc_session *session); /** @@ -896,6 +900,9 @@ 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_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 */ diff --git a/src/session_server.c b/src/session_server.c index e7d832ab..43614cc1 100644 --- a/src/session_server.c +++ b/src/session_server.c @@ -1379,6 +1379,11 @@ nc_server_destroy(void) } server_opts.interactive_auth_data = NULL; server_opts.interactive_auth_data_free = NULL; + if (server_opts.interactive_auth_v2_data && server_opts.interactive_auth_v2_data_free) { + server_opts.interactive_auth_v2_data_free(server_opts.interactive_auth_v2_data); + } + server_opts.interactive_auth_v2_data = NULL; + server_opts.interactive_auth_v2_data_free = NULL; #endif /* NC_ENABLED_SSH_TLS */ /* hidden UNIX socket paths */ @@ -2317,7 +2322,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 +#ifndef HAVE_LIBSSH_0_12 ssh_message ssh_msg; +#endif /* !HAVE_LIBSSH_0_12 */ struct nc_session *new; #endif /* NC_ENABLED_SSH_TLS */ @@ -2341,36 +2348,41 @@ 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: +#ifndef HAVE_LIBSSH_0_12 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); + ssh_message_free(ssh_msg); + } +#endif /* !HAVE_LIBSSH_0_12 */ + + /* check for new SSH channels (in callback mode, ssh_message_get returns NULL) */ + 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 (!ret) { - /* just some SSH message */ - ret = NC_PSPOLL_SSH_MSG; + if (new != session) { + break; } - ssh_message_free(ssh_msg); + } +#ifndef HAVE_LIBSSH_0_12 + if (ssh_msg) { + /* just some SSH message */ + ret = NC_PSPOLL_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 */ break; } +#endif /* !HAVE_LIBSSH_0_12 */ 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..e42a16e4 100644 --- a/src/session_server.h +++ b/src/session_server.h @@ -549,6 +549,42 @@ typedef int (*nc_server_ssh_interactive_auth_clb)(const struct nc_session *sessi */ 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)); +/** + * @brief Keyboard interactive authentication callback (v2, for libssh >= 0.12). + * + * This callback is invoked by the callback-based auth path (libssh >= 0.12) and + * is called multiple times: once for the initial request (is_response == 0) and + * once for each client response (is_response == 1). It must NOT call + * `ssh_message_get()` — the answers are already available in the libssh session + * via `ssh_userauth_kbdint_getanswer()`. + * + * An example callback may fit the following description: + * - If is_response == 0: prepare prompts, send them via + * `ssh_message_auth_interactive_request()`, return SSH_AUTH_INFO. + * - If is_response == 1: read answers via `ssh_userauth_kbdint_getanswer()`, + * validate them, return SSH_AUTH_SUCCESS or SSH_AUTH_DENIED. + * + * @param[in] session NETCONF session. + * @param[in] msg SSH message that contains the interactive request or response. + * @param[in] is_response 0 for the initial request, 1 for the client's response. + * @param[in] user_data Arbitrary user data. + * @return SSH_AUTH_INFO if prompts were sent (waiting for client response), + * SSH_AUTH_SUCCESS for successful authentication, + * SSH_AUTH_DENIED to deny the user. + */ +typedef int (*nc_server_ssh_interactive_auth_clb_v2)(const struct nc_session *session, + ssh_message msg, int is_response, void *user_data); + +/** + * @brief Set the v2 callback for SSH interactive authentication (libssh >= 0.12). + * + * @param[in] auth_clb Keyboard interactive authentication callback (v2). + * @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_v2(nc_server_ssh_interactive_auth_clb_v2 auth_clb, void *user_data, + void (*free_user_data)(void *user_data)); + /** * @brief Get the number of answers to Keyboard interactive authentication prompts. * diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c index bc55850d..6795f02f 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -15,7 +15,7 @@ #define _GNU_SOURCE -#include "config.h" /* Expose HAVE_LIBPAM and HAVE_SHADOW */ +#include "config.h" /* Expose HAVE_LIBPAM, HAVE_SHADOW and HAVE_LIBSSH_0_12 */ #ifdef HAVE_LIBPAM # include @@ -23,6 +23,9 @@ #ifdef HAVE_SHADOW # include #endif +#ifdef HAVE_LIBSSH_0_12 + #include +#endif #include #include @@ -705,7 +708,20 @@ nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_session return ret; } +/** @brief Data structure passed to SSH callback functions. */ +struct nc_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 ssh_channel_callbacks_struct *pending_channel_cb; /**< channel_cb for additional channels, moved to new session by subsystem callback. */ +#ifdef HAVE_LIBPAM + struct nc_ssh_cb_pam_data *pam_kbdint; /**< PAM thread bridge state. */ +#endif +}; + #ifdef HAVE_LIBPAM +#ifndef HAVE_LIBSSH_0_12 /** * @brief PAM conversation function, which serves as a callback for exchanging messages between the client and a PAM module. @@ -912,7 +928,485 @@ nc_server_ssh_auth_kbdint_pam(struct nc_session *session, const char *username, return ret; } +#else + +/** + * @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. + */ +struct nc_ssh_cb_pam_data { + pthread_t thread; /**< PAM thread handle. */ + pthread_mutex_t lock; /**< Protects all fields below. */ + pthread_cond_t prompts_ready; /**< PAM thread signals: prompts available or done. */ + pthread_cond_t answers_ready; /**< Main thread signals: answers available. */ + + /** 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) */ + const char *name; /**< Prompt name. */ + const char *instruction; /**< Prompt instruction. */ + 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 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_ssh_cb_pam_data. + * @return PAM_SUCCESS on success, PAM_BUF_ERR on OOM, PAM_CONV_ERR otherwise. + */ +static int +nc_ssh_cb_pam_conv(int n_messages, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + struct nc_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->name = "Keyboard-Interactive Authentication"; + data->instruction = "Please enter your authentication token"; + data->n_prompts = n_requests; + data->prompts = prompts; + data->echo = echo; + data->state = NC_PAM_PROMPTS_READY; + pthread_cond_signal(&data->prompts_ready); + + /* wait for answers from main thread */ + while (data->state == NC_PAM_PROMPTS_READY) { + pthread_cond_wait(&data->answers_ready, &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_ssh_cb_pam_data. + * @return NULL. + */ +static void * +nc_ssh_cb_pam_thread(void *arg) +{ + struct nc_ssh_cb_pam_data *data = arg; + struct pam_conv conv = {0}; + int ret; + + conv.conv = nc_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->prompts_ready); + 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_ssh_cb_pam_data_free(struct nc_ssh_cb_pam_data *data) +{ + if (!data) { + return; + } + pthread_mutex_destroy(&data->lock); + pthread_cond_destroy(&data->prompts_ready); + pthread_cond_destroy(&data->answers_ready); + free(data); +} + +/** + * @brief Cancel the PAM thread and clean up (called on auth failure/timeout/disconnect). + * + * @param[in] data PAM data (may be NULL). + */ +static void +nc_ssh_cb_pam_cancel(struct nc_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->answers_ready); + } 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_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, SSH_AUTH_SUCCESS/SSH_AUTH_DENIED on PAM completion. + */ +static int +nc_ssh_cb_kbdint_pam_request(struct nc_ssh_cb_data *cb_data, ssh_message message, const char *UNUSED(user)) +{ + struct nc_ssh_cb_pam_data *pam_data; + int rc, n_prompts; + const char *name, *instruction, **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->prompts_ready, NULL); + pthread_cond_init(&pam_data->answers_ready, 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_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->prompts_ready); + pthread_cond_destroy(&pam_data->answers_ready); + 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->prompts_ready, &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_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 */ + name = pam_data->name; + instruction = pam_data->instruction; + 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, name, instruction, n_prompts, prompts, echo); + + if (rc != SSH_OK) { + ERR(cb_data->session, "Failed to send an authentication request."); + nc_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, SSH_AUTH_SUCCESS/SSH_AUTH_DENIED on completion. + */ +static int +nc_ssh_cb_kbdint_pam_response(struct nc_ssh_cb_data *cb_data, ssh_message message) +{ + struct nc_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 *name, *instruction, **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_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->answers_ready); + + /* wait for PAM thread to process: either new prompts or completion */ + while (pam_data->state == NC_PAM_ANSWERS_READY) { + pthread_cond_wait(&pam_data->prompts_ready, &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_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 */ + name = pam_data->name; + instruction = pam_data->instruction; + 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, name, instruction, n_prompts, prompts, echo); + + if (rc != SSH_OK) { + ERR(session, "Failed to send an authentication request."); + nc_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_ssh_cb_pam_cancel(pam_data); + cb_data->pam_kbdint = NULL; + return rc; +} + +#endif + #elif defined (HAVE_SHADOW) +#ifndef HAVE_LIBSSH_0_12 /** * @brief Authenticate using credentials stored in the system. @@ -974,8 +1468,87 @@ nc_server_ssh_auth_kbdint_passwd(struct nc_session *session, const char *usernam return ret; } +#else +/** + * @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, SSH_AUTH_DENIED on error. + */ +static int +nc_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, SSH_AUTH_DENIED otherwise. + */ +static int +nc_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 +#endif /* HAVE_SHADOW */ +#ifndef HAVE_LIBSSH_0_12 /** * @brief Keyboard-interactive authentication method using the system's authentication methods. * @@ -1005,6 +1578,8 @@ nc_server_ssh_auth_kbdint_system(struct nc_session *session, ssh_message msg) return rc; } +#endif /* !HAVE_LIBSSH_0_12 */ + 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)) @@ -1022,6 +1597,23 @@ nc_server_ssh_set_interactive_auth_clb(int (*interactive_auth_clb)(const struct nc_rwlock_unlock(&server_opts.config_lock, __func__); } +API void +nc_server_ssh_set_interactive_auth_clb_v2(int (*interactive_auth_clb_v2)(const struct nc_session *session, + ssh_message msg, int is_response, void *user_data), void *user_data, void (*free_user_data)(void *user_data)) +{ + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { + return; + } + + server_opts.interactive_auth_clb_v2 = interactive_auth_clb_v2; + server_opts.interactive_auth_v2_data = user_data; + server_opts.interactive_auth_v2_data_free = free_user_data; + + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); +} + #ifdef HAVE_LIBPAM API int @@ -1293,6 +1885,8 @@ nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts #endif } +#ifndef HAVE_LIBSSH_0_12 + /** * @brief Handle authentication request for the None method. * @@ -1924,31 +2518,95 @@ nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, return 1; } +#else /* HAVE_LIBSSH_0_12 */ + +/** + * @brief Stub for nc_session_ssh_msg when using callback-based auth (libssh >= 0.12). + * + * In callback mode, SSH messages are handled by libssh callbacks, not by polling + * ssh_message_get(). This stub exists only to satisfy the linker since + * nc_ps_poll_session_io() references this function symbol. + * + * @return 1 (message not processed). + */ +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)session; + (void)opts; + (void)msg; + (void)auth_state; + + return 1; +} + +#endif /* !HAVE_LIBSSH_0_12 */ + /* 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; + +#ifdef HAVE_LIBSSH_0_12 + ssh_event event; +#else ssh_message msg; +#endif DBG(session, "Waiting for \"netconf\" SSH subsystem request..."); nc_timeouttime_get(&ts_timeout, NC_TRANSPORT_MSG_TIMEOUT); - while (1) { + +#ifdef HAVE_LIBSSH_0_12 + (void) opts; + + /* Create an event loop */ + event = ssh_event_new(); + ssh_event_add_session(event, session->ti.libssh.session); + + /* 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 while waiting for \"netconf\" subsystem request."); + ERR(session, "Communication SSH socket unexpectedly closed."); + ssh_event_free(event); return -1; } - msg = ssh_message_get(session->ti.libssh.session); - if (msg) { - if (nc_session_ssh_msg(session, opts, msg, NULL)) { - ssh_message_reply_default(msg); - } - ssh_message_free(msg); + /* This function listens to the network and automatically calls callback funcitons. */ + if (ssh_event_dopoll(event, 100) == SSH_ERROR) { + break; } - if (session->ti.libssh.channel && session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { + if (nc_timeouttime_cur_diff(&ts_timeout) < 1) { + /* timeout */ + ERR(session, "Failed to start \"netconf\" SSH subsystem for too long, disconnecting."); + break; + } + } + + ssh_event_free(event); + + if (session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { + VRB(session, "NETCONF subsystem successfully opened."); + return 1; + } +#else + while (1) { + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(session, "Communication SSH socket unexpectedly closed while waiting for \"netconf\" subsystem request."); + return -1; + } + + msg = ssh_message_get(session->ti.libssh.session); + if (msg) { + if (nc_session_ssh_msg(session, opts, msg, NULL)) { + ssh_message_reply_default(msg); + } + ssh_message_free(msg); + } + + if (session->ti.libssh.channel && session->flags & NC_SESSION_SSH_SUBSYS_NETCONF) { return 1; } @@ -1959,7 +2617,7 @@ nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc break; } } - + #endif return 0; } @@ -2004,12 +2662,704 @@ nc_ssh_bind_add_hostkeys(ssh_bind sbind, struct nc_server_ssh_opts *opts) return 0; } +#ifdef HAVE_LIBSSH_0_12 +/** +* @brief Check if local users are supported for the current session. +* +* @param[in] session SSH session to check. +* @return 1 if local users are supported, 0 otherwise. +*/ +static int +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 && (lys_feature_value(mod, "local-users-supported") == 0)) { + return 1; + } + return 0; +} + +/** +* @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, NULL otherwise. +*/ +static struct nc_auth_client * +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 find_auth_client(referenced_endpt->opts.ssh, user); + } + return NULL; +} + +/** + * @brief Initialize the authentication state for multi-method authentication. + * + * @param[in] cb_data Callback data containing the auth state. + * @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). + */ +static void +nc_ssh_cb_init_auth_state(struct nc_ssh_cb_data *cb_data, int local_users_supported, + struct nc_auth_client *auth_client) +{ + if (cb_data->auth_state.method_count) { + return; + } + + if (local_users_supported) { + if (auth_client->pubkey_store != NC_STORE_UNKNOWN) { + cb_data->auth_state.methods |= SSH_AUTH_METHOD_PUBLICKEY; + cb_data->auth_state.method_count++; + } + if (auth_client->password) { + cb_data->auth_state.methods |= SSH_AUTH_METHOD_PASSWORD; + cb_data->auth_state.method_count++; + } + if (auth_client->kbdint_method != NC_KBDINT_AUTH_METHOD_NONE) { + cb_data->auth_state.methods |= SSH_AUTH_METHOD_INTERACTIVE; + cb_data->auth_state.method_count++; + } + if (auth_client->none_enabled) { + cb_data->auth_state.methods |= SSH_AUTH_METHOD_NONE; + cb_data->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 */ + cb_data->auth_state.methods = SSH_AUTH_METHOD_PUBLICKEY | SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE; + cb_data->auth_state.method_count = 1; + } + + ssh_set_auth_methods(cb_data->session->ti.libssh.session, cb_data->auth_state.methods); +} + +/** + * @brief Handle a successful authentication attempt, tracking partial/multi-method auth. + * + * @param[in] cb_data Callback data containing the auth state. + * @param[in] method The SSH auth method that succeeded. + * @return SSH_AUTH_SUCCESS if fully authenticated, SSH_AUTH_PARTIAL if more methods are needed. + */ +static int +nc_ssh_cb_auth_success(struct nc_ssh_cb_data *cb_data, int method) +{ + cb_data->auth_state.success_methods |= method; + cb_data->auth_state.success_count++; + + if (cb_data->auth_state.success_count < cb_data->auth_state.method_count) { + /* success, but he needs to do another method */ + VRB(cb_data->session, "User \"%s\" partially authenticated, but still needs to authenticate via the rest of his configured methods.", + cb_data->session->username); + ssh_set_auth_methods(cb_data->session->ti.libssh.session, + cb_data->auth_state.methods & ~cb_data->auth_state.success_methods); + return SSH_AUTH_PARTIAL; + } + + /* authenticated */ + cb_data->session->flags |= NC_SESSION_SSH_AUTHENTICATED; + return SSH_AUTH_SUCCESS; +} + +/** +* @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_ssh_cb_data). +* @return SSH_AUTH_SUCCESS if authentication is successful, SSH_AUTH_DENIED otherwise. +*/ +static int +nc_ssh_cb_auth_none(ssh_session UNUSED(libssh_sess), const char *user, void *userdata) +{ + struct nc_ssh_cb_data *cb_data = (struct nc_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_server_ssh_opts *opts = cb_data->opts; + struct nc_auth_client *auth_client = NULL; + int local_users_supported = 0; + + if (!user) { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + /* Save the username if this is the first attempt */ + if (!session->username) { + session->username = strdup(user); + if (!session->username) { + NC_CHECK_ERRMEM_RET(!session->username, SSH_AUTH_DENIED); + } + + /* 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 SSH_AUTH_DENIED; + } + + /* Check if local users are supported via the YANG model */ + local_users_supported = check_local_user_support(session); + + /* Find the user's configuration */ + if (local_users_supported) { + auth_client = 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 SSH_AUTH_DENIED; + } + } + + assert(!local_users_supported || auth_client); + + /* configure and advertise the accepted auth methods */ + nc_ssh_cb_init_auth_state(cb_data, local_users_supported, auth_client); + + /* Check if the user is allowed to authenticate with none method */ + if (local_users_supported && auth_client->none_enabled) { + return nc_ssh_cb_auth_success(cb_data, SSH_AUTH_METHOD_NONE); + } + + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; +} + +/** +* @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_ssh_cb_data). +* @return SSH_AUTH_SUCCESS if authentication is successful, SSH_AUTH_DENIED otherwise. +*/ +static int +nc_ssh_cb_auth_password(ssh_session UNUSED(libssh_sess), const char *user, const char *password, void *userdata) +{ + struct nc_ssh_cb_data *cb_data = (struct nc_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_server_ssh_opts *opts = cb_data->opts; + struct nc_auth_client *auth_client = NULL; + int local_users_supported = 0; + int rc = 1; + char *stored_password = NULL; + + if (!user) { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + /* Save the username if this is the first attempt */ + if (!session->username) { + session->username = strdup(user); + if (!session->username) { + NC_CHECK_ERRMEM_RET(!session->username, SSH_AUTH_DENIED); + } + + /* 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 SSH_AUTH_DENIED; + } + + /* Check if local users are supported via the YANG model */ + local_users_supported = check_local_user_support(session); + + /* Find the user's configuration */ + if (local_users_supported) { + auth_client = find_auth_client(opts, user); + + if (!auth_client) { + ERR(session, "User \"%s\" not known by the server.", user); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + } + + assert(!local_users_supported || auth_client); + + /* configure and advertise the accepted auth methods */ + nc_ssh_cb_init_auth_state(cb_data, local_users_supported, auth_client); + + /* 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_cb_auth_success(cb_data, SSH_AUTH_METHOD_PASSWORD); + } else { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } +} + +/** + * @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_ssh_cb_data). + * @return SSH_AUTH_SUCCESS if authentication is successful (or probe accepted), + * SSH_AUTH_DENIED otherwise. + */ +static int +nc_ssh_cb_auth_pubkey(ssh_session UNUSED(libssh_sess), const char *user, struct ssh_key_struct *pubkey, char signature_state, void *userdata) +{ + struct nc_ssh_cb_data *cb_data = (struct nc_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_server_ssh_opts *opts = cb_data->opts; + 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 (!user) { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + /* Save the username if this is the first attempt */ + if (!session->username) { + session->username = strdup(user); + if (!session->username) { + NC_CHECK_ERRMEM_RET(!session->username, SSH_AUTH_DENIED); + } + + /* 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 SSH_AUTH_DENIED; + } + + /* Check if local users are supported via the YANG model */ + local_users_supported = check_local_user_support(session); + + /* Find the user's configuration */ + if (local_users_supported) { + auth_client = find_auth_client(opts, user); + + if (!auth_client) { + ERR(session, "User \"%s\" not known by the server.", user); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + } + + assert(!local_users_supported || auth_client); + + /* configure and advertise the accepted auth methods */ + nc_ssh_cb_init_auth_state(cb_data, 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); + 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_cb_auth_success(cb_data, 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, SSH_AUTH_SUCCESS if authenticated, SSH_AUTH_DENIED on failure. + */ +static int +nc_ssh_cb_kbdint_system(struct nc_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_ssh_cb_kbdint_pam_response(cb_data, message); + } else { + return nc_ssh_cb_kbdint_pam_request(cb_data, message, user); + } +#elif defined (HAVE_SHADOW) + if (is_response) { + return nc_ssh_cb_kbdint_shadow_response(cb_data->session, user); + } else { + return nc_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 +} + +/** + * @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_ssh_cb_data). + * @return SSH_AUTH_INFO if prompts were sent (waiting for client response), + * SSH_AUTH_SUCCESS if authentication is successful, + * SSH_AUTH_DENIED otherwise. + */ +static int +nc_ssh_cb_auth_kbdint(ssh_message message, ssh_session UNUSED(libssh_sess), void *userdata) +{ + struct nc_ssh_cb_data *cb_data = (struct nc_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + struct nc_server_ssh_opts *opts = cb_data->opts; + 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 (!user) { + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + + /* Save the username if this is the first attempt */ + if (!session->username) { + session->username = strdup(user); + if (!session->username) { + NC_CHECK_ERRMEM_RET(!session->username, SSH_AUTH_DENIED); + } + + /* 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 SSH_AUTH_DENIED; + } + + /* Check if local users are supported via the YANG model */ + local_users_supported = check_local_user_support(session); + + /* Find the user's configuration */ + if (local_users_supported) { + auth_client = find_auth_client(opts, user); + + if (!auth_client) { + ERR(session, "User \"%s\" not known by the server.", user); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } + } + + assert(!local_users_supported || auth_client); + + /* configure and advertise the accepted auth methods */ + nc_ssh_cb_init_auth_state(cb_data, local_users_supported, auth_client); + + /* determine kbdint path and execute the appropriate phase */ + if (!local_users_supported) { + /* system kbdint (PAM or shadow) */ + ret = nc_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_v2) { + /* v2 callback — designed for callback-based auth (libssh >= 0.12) */ + ret = server_opts.interactive_auth_clb_v2(session, message, + ssh_message_auth_kbdint_is_response(message), server_opts.interactive_auth_v2_data); + } else if (server_opts.interactive_auth_clb) { + /* v1 callback — incompatible with callback mode (uses ssh_message_get internally) */ + ERR(session, "Custom interactive auth callback (v1) not supported in callback mode, use the v2 API."); + session->opts.server.ssh_auth_attempts++; + return SSH_AUTH_DENIED; + } else if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_SYSTEM) { + ret = nc_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_cb_auth_success(cb_data, SSH_AUTH_METHOD_INTERACTIVE); + } else { + VRB(session, "User \"%s\" denied authenticated 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_session). + * @return 0 on success, 1 on error (unknown subsystem, duplicate request, or memory error). + */ +static int +nc_ssh_cb_channel_subsystem(ssh_session UNUSED(libssh_sess), ssh_channel channel, const char *subsystem, void *userdata) +{ + struct nc_session *new_session; + struct nc_session *session = (struct nc_session *) userdata; + struct nc_ssh_cb_data *cb_data; + + 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 */ + cb_data = session->ti.libssh.cb_data; + if (!cb_data) { + ERRINT; + return 1; + } + + 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; + + /* move the channel_cb from the pending slot to the new session */ + new_session->ti.libssh.channel_cb = cb_data->pending_channel_cb; + cb_data->pending_channel_cb = NULL; + } + + return 0; +} + +/** + * @brief Callback function for SSH channel open request. + * + * @param[in] libssh_sess SSH session object. + * @param[in] userdata Pointer to user data (struct nc_ssh_cb_data). + * @return The new SSH channel on success, NULL on failure. + */ +static ssh_channel +nc_ssh_cb_channel_open_request_session(ssh_session libssh_sess, void *userdata) +{ + struct nc_ssh_cb_data *cb_data = (struct nc_ssh_cb_data *)userdata; + struct nc_session *session = cb_data->session; + ssh_channel chan; + struct ssh_channel_callbacks_struct *channel_cb; + + /* Create the new channel */ + chan = ssh_channel_new(libssh_sess); + if (!chan) { + return NULL; + } + + channel_cb = calloc(1, sizeof(*channel_cb)); + if (!channel_cb) { + ssh_channel_free(chan); + return NULL; + } + channel_cb->userdata = session; + channel_cb->channel_subsystem_request_function = nc_ssh_cb_channel_subsystem; + ssh_callbacks_init(channel_cb); + + /* Bind the subsystem callback to this specific channel */ + ssh_set_channel_callbacks(chan, channel_cb); + + if (!session->ti.libssh.channel) { + /* first channel */ + session->ti.libssh.channel_cb = channel_cb; + session->ti.libssh.channel = chan; + } else { + /* additional channel, store channel_cb for nc_ssh_cb_channel_subsystem to pick up */ + cb_data->pending_channel_cb = channel_cb; + } + return chan; +} + +#endif /* HAVE_LIBSSH_0_12 */ + static int nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts *opts) { struct timespec ts_timeout; + +#ifdef HAVE_LIBSSH_0_12 + ssh_event event; +#else ssh_message msg; struct nc_auth_state auth_state = {0}; +#endif DBG(session, "SSH authentication..."); @@ -2017,6 +3367,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); } +#ifdef HAVE_LIBSSH_0_12 + /* Create an event loop */ + event = ssh_event_new(); + ssh_event_add_session(event, session->ti.libssh.session); + + /* 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."); + ssh_event_free(event); + return -1; + } + + /* This function listens to the network and automatically calls callback functions. */ + if (ssh_event_dopoll(event, 100) == SSH_ERROR) { + break; + } + + if (opts->auth_timeout && (nc_timeouttime_cur_diff(&ts_timeout) < 1)) { + /* timeout */ + break; + } + } + + ssh_event_free(event); +#else while (1) { if (!ssh_is_connected(session->ti.libssh.session)) { ERR(session, "Communication SSH socket unexpectedly closed while waiting for authentication."); @@ -2041,6 +3417,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 +3441,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; +#ifdef HAVE_LIBSSH_0_12 + struct nc_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 +3454,24 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt goto cleanup; } +#ifdef HAVE_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_ssh_cb_auth_password; + cb_data->server_cb.auth_pubkey_function = nc_ssh_cb_auth_pubkey; + cb_data->server_cb.auth_none_function = nc_ssh_cb_auth_none; + cb_data->server_cb.auth_kbdint_function = nc_ssh_cb_auth_kbdint; + cb_data->server_cb.channel_open_request_session_function = nc_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 /* HAVE_LIBSSH_0_12 */ + sbind = ssh_bind_new(); if (!sbind) { ERR(session, "Failed to create an SSH bind."); @@ -2190,6 +3589,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 defined (HAVE_LIBSSH_0_12) && defined (HAVE_LIBPAM) + /* if PAM thread is still running, cancel and clean it up */ + if (cb_data) { + nc_ssh_cb_pam_cancel(cb_data->pam_kbdint); + cb_data->pam_kbdint = NULL; + } +#endif goto cleanup; }