diff --git a/CMakeLists.txt b/CMakeLists.txt index b9836261..db6e4e80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -244,6 +244,7 @@ target_link_libraries(netconf2 ${CMAKE_THREAD_LIBS_INIT}) # check availability for some pthread functions set(CMAKE_REQUIRED_LIBRARIES pthread) check_function_exists(pthread_rwlockattr_setkind_np HAVE_PTHREAD_RWLOCKATTR_SETKIND_NP) +check_function_exists(pthread_timedjoin_np HAVE_PTHREAD_TIMEDJOIN_NP) # header file compatibility check_include_file("shadow.h" HAVE_SHADOW) @@ -274,6 +275,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/config.h.in b/src/config.h.in index 67d72900..2e982fe7 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -93,6 +93,7 @@ /* Portability feature-check macros. */ #cmakedefine HAVE_PTHREAD_RWLOCKATTR_SETKIND_NP +#cmakedefine HAVE_PTHREAD_TIMEDJOIN_NP /* Enable IP_FREEBIND/IPV6_FREEBIND on listening sockets. */ #cmakedefine NC_ENABLE_IP_FREEBIND diff --git a/src/session.c b/src/session.c index 7530cbc5..0460716d 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 @@ -923,6 +924,8 @@ nc_session_free_transport(struct nc_session *session, int *multisession) case NC_TI_SSH: { int r; struct nc_session *siter; + void **channel_cbs = NULL, **channel_cbs_tmp; + uint16_t channel_cbs_count = 0, i; /* There can be multiple NETCONF sessions on the same SSH session (NETCONF session maps to * SSH channel). So destroy the SSH session only if there is no other NETCONF session using @@ -944,6 +947,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 +969,18 @@ 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); + if (siter->ti.libssh.channel_cb) { + /* channel callbacks must stay valid until all the channels are freed in + * ssh_free(), so only collect them here and free them afterwards */ + channel_cbs_tmp = realloc(channel_cbs, (channel_cbs_count + 1) * sizeof *channel_cbs); + if (channel_cbs_tmp) { + channel_cbs = channel_cbs_tmp; + channel_cbs[channel_cbs_count++] = siter->ti.libssh.channel_cb; + } else { + /* leak this one struct, the channel would otherwise use freed callbacks */ + ERRMEM; + } + } if (!(siter->flags & NC_SESSION_SHAREDCTX)) { ly_ctx_destroy((struct ly_ctx *)siter->ctx); } @@ -982,8 +998,24 @@ nc_session_free_transport(struct nc_session *session, int *multisession) sock = -1; #endif +#if LIBSSH_0_12 + /* Free ssh event before freeing the session */ + ssh_event_free(session->ti.libssh.event); +#endif + /* closes sock if set */ ssh_free(session->ti.libssh.session); + + /* all the SSH channels were freed now, free their callback data */ + for (i = 0; i < channel_cbs_count; ++i) { + free(channel_cbs[i]); + } + free(channel_cbs); + +#if LIBSSH_0_12 + /* Free callback data after session is destroyed */ + nc_server_ssh_cb_data_free(session->ti.libssh.cb_data); +#endif } else { /* remove the session from the list */ for (siter = session->ti.libssh.next; siter->ti.libssh.next != session; siter = siter->ti.libssh.next) {} @@ -994,6 +1026,19 @@ 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; } +#if LIBSSH_0_12 + /* transfer cb_data to a remaining session so it's freed when the SSH session is freed */ + if (session->ti.libssh.cb_data) { + /* the callback data now belongs to the surviving session */ + ((struct nc_server_ssh_cb_data *)session->ti.libssh.cb_data)->session = siter; + 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; + } +#endif } /* SESSION IO UNLOCK */ diff --git a/src/session_p.h b/src/session_p.h index 643cf8ca..8f591a73 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -887,9 +887,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 { @@ -1426,17 +1430,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 58150a04..f6cfaa83 100644 --- a/src/session_server.c +++ b/src/session_server.c @@ -45,6 +45,7 @@ #ifdef NC_ENABLED_SSH_TLS +#include "session_server_ssh_wrapper.h" #include "session_wrapper.h" #include @@ -102,7 +103,6 @@ nc_server_endpt_get(const char *name, struct nc_endpt **endpt) } } - ERR(NULL, "Endpoint \"%s\" not found in the configuration.", name); return 1; } @@ -2294,6 +2294,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 1 if a new SSH channel is found, 0 otherwise. + */ +static int +nc_ps_ssh_find_new_channel(struct nc_session *session) +{ + struct nc_session *new; + + if (!session->ti.libssh.next) { + return 0; + } + + 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 1; + } + } + + return 0; +} + +#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 +2345,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 +2370,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..c4b82289 100644 --- a/src/session_server.h +++ b/src/session_server.h @@ -525,17 +525,36 @@ 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). With libssh 0.12+, + * `SSH_AUTH_PARTIAL` may also be returned if the method succeeded but more authentication + * methods are required based on the server configuration; if none are required, the + * authentication completes instead of returning a partial success. */ 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 +562,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 ffefe0a6..ff9ec325 100644 --- a/src/session_server_ssh.c +++ b/src/session_server_ssh.c @@ -4,7 +4,7 @@ * @brief libnetconf2 SSH server session manipulation functions * * @copyright - * Copyright (c) 2017 - 2021 CESNET, z.s.p.o. + * Copyright (c) 2017 - 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. @@ -17,13 +17,6 @@ #include "config.h" /* Expose HAVE_LIBPAM and HAVE_SHADOW */ -#ifdef HAVE_LIBPAM -# include -#endif -#ifdef HAVE_SHADOW -# include -#endif - #include #include #include @@ -40,1888 +33,1482 @@ #include #include +#ifdef HAVE_LIBPAM +# include +#endif +#ifdef HAVE_SHADOW +# include +#endif + #include "compat.h" #include "log_p.h" #include "nc_version.h" #include "session.h" #include "session_p.h" +#include "session_server_ssh_wrapper.h" #include "session_wrapper.h" -/** - * @brief Stores the private key data as a temporary file. - * - * @param[in] in Private key data. - * @param[in] privkey_format String representation of the private key format. - * @return Path to the created temporary file or NULL on fail. - */ -static char * -nc_server_ssh_privkey_data_to_tmp_file(const char *in, const char *privkey_format) +int +nc_ssh_check_local_user_support(struct nc_session *session) { - char path[12] = "/tmp/XXXXXX"; - int fd, written; - unsigned len; - mode_t umode; - FILE *file; - - NC_CHECK_ARG_RET(NULL, in, NULL); - - umode = umask(0177); - fd = mkstemp(path); - umask(umode); - if (fd == -1) { - return NULL; - } + const struct ly_ctx *ctx; + struct lys_module *mod; + int rc; - file = fdopen(fd, "w"); - if (!file) { - close(fd); - return NULL; + ctx = nc_session_get_ctx(session); + mod = ly_ctx_get_module_latest(ctx, "ietf-ssh-server"); + if (!mod) { + ERRINT; + return -1; } - /* write header */ - written = fwrite("-----BEGIN", 1, 10, file); - if (privkey_format) { - written += fwrite(privkey_format, 1, strlen(privkey_format), file); - written += fwrite("PRIVATE KEY-----\n", 1, 17, file); + rc = lys_feature_value(mod, "local-users-supported"); + if (rc == LY_SUCCESS) { + return 1; + } else if (rc == LY_ENOTFOUND) { + return 0; } else { - written += fwrite(" PRIVATE KEY-----\n", 1, 18, file); + return -1; } +} - /* write data */ - written += fwrite(in, 1, strlen(in), file); +struct nc_auth_client * +nc_ssh_find_auth_client(struct nc_server_ssh_opts *opts, const char *user, struct nc_session *session) +{ + struct nc_endpt *referenced_endpt; + LY_ARRAY_COUNT_TYPE u; - /* write footer */ - written += fwrite("\n-----END", 1, 9, file); - if (privkey_format) { - written += fwrite(privkey_format, 1, strlen(privkey_format), file); - written += fwrite("PRIVATE KEY-----", 1, 16, file); - } else { - written += fwrite(" PRIVATE KEY-----", 1, 17, file); + if (!user) { + return NULL; } - fclose(file); - - /* checksum */ - if (privkey_format) { - len = 10 + strlen(privkey_format) + 17 + strlen(in) + 9 + strlen(privkey_format) + 16; - } else { - len = 10 + 18 + strlen(in) + 9 + 17; + for (u = 0; u < LY_ARRAY_COUNT(opts->auth_clients); u++) { + if (!strcmp(opts->auth_clients[u].username, user)) { + return &opts->auth_clients[u]; + } } - if ((unsigned)written != len) { - unlink(path); - return NULL; + /* 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)) { + ERR(session, "Referenced endpoint \"%s\" not found.", opts->referenced_endpt_name); + return NULL; + } + return nc_ssh_find_auth_client(referenced_endpt->opts.ssh, user, session); } - - return strdup(path); + return NULL; } -/** - * @brief Get asymmetric key from the keystore. - * - * @param[in] referenced_name Name of the asymmetric key in the keystore. - * @param[out] askey Referenced asymmetric key. - * @return 0 on success, 1 on error. - */ -static int -nc_server_ssh_ks_ref_get_key(const char *referenced_name, struct nc_asymmetric_key **askey) +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) { - LY_ARRAY_COUNT_TYPE i; - struct nc_keystore *ks = &server_opts.config.keystore; - - *askey = NULL; + if (auth_state->method_count) { + return; + } - /* lookup name */ - LY_ARRAY_FOR(ks->entries, i) { - if (!strcmp(referenced_name, ks->entries[i].asym_key.name)) { - break; + if (local_users_supported) { + if (auth_client->pubkey_store != NC_STORE_UNKNOWN) { + auth_state->methods |= SSH_AUTH_METHOD_PUBLICKEY; + auth_state->method_count++; } - } - if (i == LY_ARRAY_COUNT(ks->entries)) { - ERR(NULL, "Keystore entry \"%s\" not found.", referenced_name); - return 1; + 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; } - *askey = &ks->entries[i].asym_key; + ssh_set_auth_methods(session->ti.libssh.session, auth_state->methods); +} - /* check if the referenced public key is SubjectPublicKeyInfo */ - if ((*askey)->pubkey.data && nc_is_pk_subject_public_key_info((*askey)->pubkey.data)) { - ERR(NULL, "The public key of the referenced hostkey \"%s\" is in the SubjectPublicKeyInfo format, " - "which is not allowed in the SSH!", referenced_name); - return 1; +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; } - return 0; + /* authenticated */ + session->flags |= NC_SESSION_SSH_AUTHENTICATED; + VRB(session, "User \"%s\" authenticated.", session->username); + return SSH_AUTH_SUCCESS; } -/** - * @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 -nc_server_ssh_ts_ref_get_keys(const char *referenced_name, struct nc_public_key **pubkeys, uint32_t *pubkey_count) +void +nc_server_ssh_auth_attempt_failed(struct nc_session *session) { - LY_ARRAY_COUNT_TYPE i; - struct nc_public_key *pubkey; - struct nc_truststore *ts = &server_opts.config.truststore; + ++session->opts.server.ssh_auth_attempts; + VRB(session, "Failed user \"%s\" authentication attempt (#%d).", + session->username ? session->username : "unknown", session->opts.server.ssh_auth_attempts); +} - *pubkeys = NULL; - *pubkey_count = 0; +int +nc_server_ssh_auth_password_check(struct nc_session *session, const char *user, + const char *password, struct nc_auth_client *auth_client, int local_users_supported) +{ + int rc; + char *stored_password = NULL; - /* lookup name */ - LY_ARRAY_FOR(ts->pubkey_bags, i) { - if (!strcmp(referenced_name, ts->pubkey_bags[i].name)) { - break; - } - } - if (i == LY_ARRAY_COUNT(ts->pubkey_bags)) { - ERR(NULL, "Truststore entry \"%s\" not found.", referenced_name); - return 1; - } + assert(!local_users_supported || auth_client); - /* check if any of the referenced public keys is SubjectPublicKeyInfo */ - LY_ARRAY_FOR(ts->pubkey_bags[i].pubkeys, struct nc_public_key, pubkey) { - if (nc_is_pk_subject_public_key_info(pubkey->data)) { - ERR(NULL, "A public key of the referenced public key bag \"%s\" is in the SubjectPublicKeyInfo format, " - "which is not allowed in SSH!", referenced_name); + /* Get the stored password */ + if (local_users_supported) { + stored_password = auth_client->password; + if (!stored_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.", user); + return 1; + } + } else { +#ifdef HAVE_SHADOW + stored_password = nc_server_ssh_get_pwd_hash(user); + if (!stored_password) { return 1; } +#else + ERR(session, "Obtaining password from system not supported."); + return 1; +#endif } - *pubkeys = ts->pubkey_bags[i].pubkeys; - *pubkey_count = LY_ARRAY_COUNT(ts->pubkey_bags[i].pubkeys); - return 0; -} - -/** - * @brief Convert UID to string. - * - * @param[in] uid UID to convert. - * @return UID converted to string or NULL on fail. - */ -static char * -nc_server_ssh_uid_to_str(uid_t uid) -{ - int buf_len; - char *uid_str; + /* Compare the passwords */ + rc = nc_server_ssh_compare_password(stored_password, password); - /* get the number of digits and alloc */ - buf_len = snprintf(NULL, 0, "%u", uid); - uid_str = malloc(buf_len + 1); - NC_CHECK_ERRMEM_RET(!uid_str, NULL); + if (!local_users_supported) { + free(stored_password); + } - /* convert to string */ - sprintf(uid_str, "%u", uid); - uid_str[buf_len] = '\0'; - return uid_str; + return rc; } -/** - * @brief Append a character or a string to a string. - * - * @param[in] src_c Source character. - * @param[in] src_str Source string. - * @param[in,out] size Size of the destination string. - * @param[out] idx Index of the next character to write. - * @param[out] dst Destination string. - * @return 0 on success, 1 on error. - */ -static int -nc_server_ssh_str_append(const char src_c, const char *src_str, int *size, int *idx, char **dst) +int +nc_server_ssh_kbdint_select_method(struct nc_session *session, int local_users_supported, + struct nc_auth_client *auth_client, enum nc_kbdint_backend *backend) { - int src_size, allocate = 0, ret; + assert(!local_users_supported || auth_client); - /* get size of char/string we want to append */ - if (src_str) { - src_size = strlen(src_str); - } else { - src_size = 1; + if (!local_users_supported) { + /* system users always authenticate against the system method */ + *backend = NC_KBDINT_BACKEND_SYSTEM; + return 0; } - /* check if we have enough space, if not realloc */ - while ((src_size + *idx) >= *size) { - (*size) += 16; - allocate = 1; - } - if (allocate) { - *dst = nc_realloc(*dst, *size); - NC_CHECK_ERRMEM_RET(!*dst, 1); + 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; } - /* append the char/string */ - if (src_str) { - ret = sprintf(*dst + *idx, "%s", src_str); - } else { - ret = sprintf(*dst + *idx, "%c", src_c); + if (server_opts.interactive_auth_clb) { + /* custom callback has higher priority */ + *backend = NC_KBDINT_BACKEND_CUSTOM_CLB; + return 0; } - if (ret < 0) { - return 1; + + if (auth_client->kbdint_method == NC_KBDINT_AUTH_METHOD_SYSTEM) { + *backend = NC_KBDINT_BACKEND_SYSTEM; + return 0; } - *idx += ret; - return 0; + /* add future methods here */ + ERR(session, "Keyboard-interactive authentication method not supported."); + return 1; } -/** - * @brief Get the path to the system public keys from format set by an API. - * - * @param[in] username Username. - * @param[out] out_path Path to the system public keys. - * @return 0 on success, 1 on error. - */ -static int -nc_server_ssh_get_system_keys_path(const char *username, char **out_path) +int +nc_server_ssh_auth_pubkey_check(struct nc_session *session, ssh_key pubkey, + struct nc_auth_client *auth_client, int local_users_supported) { - int ret = 0, i, have_percent = 0, size = 0, idx = 0; - const char *path_fmt = server_opts.authkey_path_fmt; - char *path = NULL, *buf = NULL, *uid = NULL; - struct passwd *pw, pw_buf; - size_t buf_len = 0; + struct nc_public_key *pubkeys = NULL; + uint32_t pubkey_count = 0, i; + int ret = 0; - if (!path_fmt) { - ERR(NULL, "System public keys path format not set."); - return 1; - } - - /* check if the path format contains any tokens */ - if (strstr(path_fmt, "%h") || strstr(path_fmt, "%U") || strstr(path_fmt, "%u") || strstr(path_fmt, "%%")) { - /* get pw */ - pw = nc_getpw(0, username, &pw_buf, &buf, &buf_len); - if (!pw) { - ERR(NULL, "Unable to get passwd entry for user \"%s\".", username); - ret = 1; - goto cleanup; - } + assert(!local_users_supported || auth_client); - /* convert UID to a string */ - uid = nc_server_ssh_uid_to_str(pw->pw_uid); - if (!uid) { - ret = 1; + /* 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 { - /* no tokens, just copy the path and return */ - *out_path = strdup(path_fmt); - NC_CHECK_ERRMEM_RET(!*out_path, 1); - goto cleanup; - } + 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; + } - /* go over characters from format, copy them to path and interpret tokens correctly */ - for (i = 0; path_fmt[i]; i++) { - if (have_percent) { - /* special token, need to convert it */ - if (path_fmt[i] == '%') { - ret = nc_server_ssh_str_append('%', NULL, &size, &idx, &path); - } else if (path_fmt[i] == 'h') { - /* user home */ - ret = nc_server_ssh_str_append(0, pw->pw_dir, &size, &idx, &path); - } else if (path_fmt[i] == 'u') { - /* username */ - ret = nc_server_ssh_str_append(0, username, &size, &idx, &path); - } else if (path_fmt[i] == 'U') { - /* UID */ - ret = nc_server_ssh_str_append(0, uid, &size, &idx, &path); - } else { - ERR(NULL, "Failed to parse system public keys path format \"%s\".", server_opts.authkey_path_fmt); - ret = 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; } - - have_percent = 0; - } else { - if (path_fmt[i] == '%') { - have_percent = 1; - } else { - /* ordinary character with no meaning */ - ret = nc_server_ssh_str_append(path_fmt[i], NULL, &size, &idx, &path); + } 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; } - } - - if (ret) { - goto cleanup; + } else { + ERRINT; + return 1; } } - *out_path = path; - path = NULL; + /* 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: - free(uid); - free(buf); - free(path); + 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 Read public keys from the authorized keys file. - * - * @param[in] path Path to the authorized keys file. - * @param[out] pubkeys Public keys. - * @param[out] pubkey_count Public key count. - * @return 0 on success, 1 on error. - */ -static int -nc_server_ssh_read_authorized_keys_file(const char *path, struct nc_public_key **pubkeys, uint32_t *pubkey_count) -{ - int ret = 0, rc, line_num = 0; - FILE *f = NULL; - char *line = NULL, *ptr, *ptr2; - size_t n; - enum ssh_keytypes_e ktype; +#ifdef HAVE_LIBPAM - NC_CHECK_ARG_RET(NULL, path, pubkeys, 1); +int +nc_server_ssh_pam_conv_parse(struct nc_session *session, int n_messages, + const struct pam_message **msg, struct pam_response **resp, + int *n_prompts, const char ***prompts, char **echo) +{ + int i, j, t, n_requests = n_messages; - *pubkeys = NULL; - *pubkey_count = 0; + *resp = NULL; + *n_prompts = 0; + *prompts = NULL; + *echo = NULL; - f = fopen(path, "r"); - if (!f) { - ERR(NULL, "Unable to open \"%s\" (%s).", path, strerror(errno)); - ret = 1; - goto cleanup; + /* PAM_MAX_NUM_MSG == 32 by default */ + if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) { + ERR(session, "Bad number of PAM messages (#%d).", n_messages); + return PAM_CONV_ERR; } - while (getline(&line, &n, f) > -1) { - ++line_num; - if ((line[0] == '#') || (line[0] == '\n')) { - /* comment or empty line */ - continue; + /* 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(session, "PAM conversation callback received an unexpected type of message."); + return PAM_CONV_ERR; } + } - /* separate key type */ - ptr = line; - for (ptr2 = ptr; ptr2[0] && !isspace(ptr2[0]); ptr2++) {} - if (!ptr2[0]) { - ERR(NULL, "Invalid format of authorized keys file \"%s\" on line %d.", path, line_num); - ret = 1; - goto cleanup; + /* handle info/error messages, count actual prompts */ + for (i = 0; i < n_messages; i++) { + if (msg[i]->msg_style == PAM_TEXT_INFO) { + VRB(session, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg); + n_requests--; } - ptr2[0] = '\0'; + if (msg[i]->msg_style == PAM_ERROR_MSG) { + ERR(session, "PAM conversation callback received an error message (%s).", msg[i]->msg); + return PAM_CONV_ERR; + } + } - /* detect key type */ - ktype = ssh_key_type_from_name(ptr); - if ((ktype != SSH_KEYTYPE_RSA) && (ktype != SSH_KEYTYPE_ECDSA_P256) && (ktype != SSH_KEYTYPE_ECDSA_P384) && - (ktype != SSH_KEYTYPE_ECDSA_P521) && (ktype != SSH_KEYTYPE_ED25519)) { - WRN(NULL, "Unsupported key type \"%s\" in authorized keys file \"%s\" on line %d.", ptr, path, line_num); - continue; + /* no actual prompts */ + if (n_requests <= 0) { + return PAM_SUCCESS; + } + + /* build response, prompt 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)) { + ERRMEM; + free(*resp); + *resp = NULL; + free(*prompts); + *prompts = NULL; + free(*echo); + *echo = NULL; + 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++; } + } - /* get key data */ - ptr = ptr2 + 1; - for (ptr2 = ptr; ptr2[0] && !isspace(ptr2[0]); ptr2++) {} - ptr2[0] = '\0'; + *n_prompts = n_requests; + return PAM_SUCCESS; +} - /* add the key */ - *pubkeys = nc_realloc(*pubkeys, (*pubkey_count + 1) * sizeof **pubkeys); - NC_CHECK_ERRMEM_GOTO(!(*pubkeys), ret = 1, cleanup); - rc = asprintf(&(*pubkeys)[*pubkey_count].name, "authorized_key_%" PRIu32, *pubkey_count); - NC_CHECK_ERRMEM_GOTO(rc == -1, (*pubkeys)[*pubkey_count].name = NULL; ret = 1, cleanup); - (*pubkeys)[*pubkey_count].type = NC_PUBKEY_FORMAT_SSH; - (*pubkeys)[*pubkey_count].data = strdup(ptr); - NC_CHECK_ERRMEM_GOTO(!(*pubkeys)[*pubkey_count].data, ret = 1, cleanup); - (*pubkey_count)++; +int +nc_server_ssh_pam_conv_fill(struct nc_session *session, struct pam_response *resp, + int n_prompts, int n_answers, const char **answers) +{ + int i, j; + + if (n_answers != n_prompts) { + ERR(session, "Expected %d response(s), got %d.", n_prompts, n_answers); + return PAM_CONV_ERR; } - /* ok */ - ret = 0; -cleanup: - if (f) { - fclose(f); + for (i = 0; i < n_answers; i++) { + resp[i].resp = strdup(answers[i]); + /* freeing the responses is the caller's responsibility, however on mem alloc failure + * it is safer to free the already copied responses here and set them to NULL */ + if (!resp[i].resp) { + for (j = 0; j < i; j++) { + free(resp[j].resp); + resp[j].resp = NULL; + } + ERRMEM; + return PAM_BUF_ERR; + } } - free(line); - return ret; + + return PAM_SUCCESS; } -/** - * @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 -nc_server_ssh_get_system_keys(const char *username, struct nc_public_key **pubkeys, uint32_t *pubkey_count) +int +nc_server_ssh_pam_authenticate(struct nc_session *session, const char *username, + const struct pam_conv *conv) { - int ret = 0; - char *path = NULL; + pam_handle_t *pam_h = NULL; + int ret; - /* convert the path format to get the actual path */ - ret = nc_server_ssh_get_system_keys_path(username, &path); - if (ret) { - ERR(NULL, "Getting system keys path failed."); + /* check the PAM configuration */ + if (!server_opts.pam_config_name) { + ERR(session, "PAM configuration filename not set."); + return 1; + } + + /* 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; } - /* get the keys */ - ret = nc_server_ssh_read_authorized_keys_file(path, pubkeys, pubkey_count); - if (ret) { - ERR(NULL, "Reading system keys failed."); + /* 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)); + } 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)); + } + } + cleanup: - free(path); + /* 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; } -#ifdef HAVE_SHADOW +#endif /* HAVE_LIBPAM */ -/** - * @brief Get the user's /etc/passwd entry. - * - * @param[in] username Username. - * @param[out] pwd_buf Buffer for the passwd structure. - * @param[out] buf Buffer for the pwd's strings. - * @param[out] buf_size Size of the buffer. - * @return User's passwd entry or NULL on error. - */ -static struct passwd * -nc_server_ssh_getpwnam(const char *username, struct passwd *pwd_buf, char **buf, size_t *buf_size) +int +nc_server_ssh_channel_subsys_check(struct nc_session *session, ssh_channel channel, const char *subsystem) { - struct passwd *pwd = NULL; - char *mem; - int r = 0; + struct nc_session *siter; - do { - r = getpwnam_r(username, pwd_buf, *buf, *buf_size, &pwd); - if (pwd) { - /* entry found */ - break; + 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; } - if (r == ERANGE) { - /* small buffer, enlarge */ - *buf_size <<= 2; - mem = realloc(*buf, *buf_size); - if (!mem) { - ERRMEM; - return NULL; - } - *buf = mem; + session->flags |= NC_SESSION_SSH_SUBSYS_NETCONF; + return 0; + } + + /* an additional channel must not be claimed by a session created by an earlier subsystem request */ + for (siter = session->ti.libssh.next; siter && (siter != session); siter = siter->ti.libssh.next) { + if (siter->ti.libssh.channel == channel) { + ERR(session, "Subsystem \"netconf\" requested for an already claimed channel."); + return -1; } - } while (r == ERANGE); + } - return pwd; + /* an additional channel needs a new session */ + return 1; } -/** - * @brief Get the user's /etc/shadow entry. - * - * @param[in] username Username. - * @param[out] spwd_buf Buffer for the spwd structure. - * @param[out] buf Buffer for the spwd's strings. - * @param[out] buf_size Size of the buffer. - * @return User's shadow entry or NULL on error. - */ -static struct spwd * -nc_server_ssh_getspnam(const char *username, struct spwd *spwd_buf, char **buf, size_t *buf_size) +struct nc_session * +nc_server_ssh_new_channel_session(struct nc_session *session, ssh_channel channel) { - struct spwd *spwd = NULL; - char *mem; - int r = 0; + struct nc_session *new_session; - do { -# ifndef __QNXNTO__ - r = getspnam_r(username, spwd_buf, *buf, *buf_size, &spwd); -# else - spwd = getspnam_r(username, spwd_buf, *buf, *buf_size); - r = errno; -# endif - if (spwd) { - /* entry found */ - break; - } + new_session = nc_new_session(NC_SERVER, 1); + NC_CHECK_ERRMEM_RET(!new_session, NULL); - if (r == ERANGE) { - /* small buffer, enlarge */ - *buf_size <<= 2; - mem = realloc(*buf, *buf_size); - if (!mem) { - ERRMEM; - return NULL; - } - *buf = mem; - } - } while (r == ERANGE); + 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); + NC_CHECK_ERRMEM_GOTO(!new_session->username, , error); - return spwd; + if (session->host) { + new_session->host = strdup(session->host); + NC_CHECK_ERRMEM_GOTO(!new_session->host, , error); + } + + 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; + + /* insert the new session into the ring now that it is fully constructed */ + 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; + + return new_session; + +error: + /* detach the state shared with the parent session, so that nc_session_free() does not free it */ + new_session->ti.libssh.session = NULL; + new_session->io_lock = NULL; + nc_session_free(new_session, NULL); + return NULL; } /** - * @brief Get the user's hashed password from the system. + * @brief Stores the private key data as a temporary file. * - * @param[in] username Username. - * @return User's hashed password or NULL on error. + * @param[in] in Private key data. + * @param[in] privkey_format String representation of the private key format. + * @return Path to the created temporary file or NULL on fail. */ static char * -nc_server_ssh_get_pwd_hash(const char *username) +nc_server_ssh_privkey_data_to_tmp_file(const char *in, const char *privkey_format) { - struct passwd *pwd, pwd_buf; - struct spwd *spwd, spwd_buf; - char *pass_hash = NULL, *buf = NULL; - size_t buf_size = 256; + char path[12] = "/tmp/XXXXXX"; + int fd, written; + unsigned len; + mode_t umode; + FILE *file; - buf = malloc(buf_size); - NC_CHECK_ERRMEM_GOTO(!buf, , error); + NC_CHECK_ARG_RET(NULL, in, NULL); - pwd = nc_server_ssh_getpwnam(username, &pwd_buf, &buf, &buf_size); - if (!pwd) { - VRB(NULL, "User \"%s\" not found in the system.", username); - goto error; + umode = umask(0177); + fd = mkstemp(path); + umask(umode); + if (fd == -1) { + return NULL; } - if (!strcmp(pwd->pw_passwd, "x")) { - spwd = nc_server_ssh_getspnam(username, &spwd_buf, &buf, &buf_size); - if (!spwd) { - VRB(NULL, "Failed to retrieve the shadow entry for \"%s\".", username); - goto error; - } else if ((spwd->sp_expire > -1) && (spwd->sp_expire <= (time(NULL) / (60 * 60 * 24)))) { - WRN(NULL, "User \"%s\" account has expired.", username); - goto error; - } + file = fdopen(fd, "w"); + if (!file) { + close(fd); + return NULL; + } - pass_hash = spwd->sp_pwdp; + /* write header */ + written = fwrite("-----BEGIN", 1, 10, file); + if (privkey_format) { + written += fwrite(privkey_format, 1, strlen(privkey_format), file); + written += fwrite("PRIVATE KEY-----\n", 1, 17, file); } else { - pass_hash = pwd->pw_passwd; + written += fwrite(" PRIVATE KEY-----\n", 1, 18, file); } - if (!pass_hash) { - ERR(NULL, "No password could be retrieved for \"%s\".", username); - goto error; - } + /* write data */ + written += fwrite(in, 1, strlen(in), file); - /* check the hash structure for special meaning */ - if (!strcmp(pass_hash, "*") || !strcmp(pass_hash, "!")) { - VRB(NULL, "User \"%s\" is not allowed to authenticate using a password.", username); - goto error; + /* write footer */ + written += fwrite("\n-----END", 1, 9, file); + if (privkey_format) { + written += fwrite(privkey_format, 1, strlen(privkey_format), file); + written += fwrite("PRIVATE KEY-----", 1, 16, file); + } else { + written += fwrite(" PRIVATE KEY-----", 1, 17, file); } - if (!strcmp(pass_hash, "*NP*")) { - VRB(NULL, "Retrieving password for \"%s\" from a NIS+ server not supported.", username); - goto error; + + fclose(file); + + /* checksum */ + if (privkey_format) { + len = 10 + strlen(privkey_format) + 17 + strlen(in) + 9 + strlen(privkey_format) + 16; + } else { + len = 10 + 18 + strlen(in) + 9 + 17; } - pass_hash = strdup(pass_hash); - free(buf); - return pass_hash; + if ((unsigned)written != len) { + unlink(path); + return NULL; + } -error: - free(buf); - return NULL; + return strdup(path); } -#endif - /** - * @brief Compare stored hashed password with a cleartext received password. + * @brief Get asymmetric key from the keystore. * - * @param[in] stored_pw Hashed stored password. - * @param[in] received_pw Cleartext received password. - * @return 0 on match, non-zero otherwise. + * @param[in] referenced_name Name of the asymmetric key in the keystore. + * @param[out] askey Referenced asymmetric key. + * @return 0 on success, 1 on error. */ static int -nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw) +nc_server_ssh_ks_ref_get_key(const char *referenced_name, struct nc_asymmetric_key **askey) { - char *received_pw_hash = NULL; - struct crypt_data *cdata; - int ret; - - NC_CHECK_ARG_RET(NULL, stored_pw, received_pw, 1); - - if (!stored_pw[0]) { - if (!received_pw[0]) { - WRN(NULL, "User authentication successful with an empty password!"); - return 0; - } else { - /* the user did now know he does not need any password, - * (which should not be used) so deny authentication */ - return 1; + LY_ARRAY_COUNT_TYPE i; + struct nc_keystore *ks = &server_opts.config.keystore; + + *askey = NULL; + + /* lookup name */ + LY_ARRAY_FOR(ks->entries, i) { + if (!strcmp(referenced_name, ks->entries[i].asym_key.name)) { + break; } } - - if (!strncmp(stored_pw, "$0$", 3)) { - /* cleartext password, simply compare the values */ - return strcmp(stored_pw + 3, received_pw); + if (i == LY_ARRAY_COUNT(ks->entries)) { + ERR(NULL, "Keystore entry \"%s\" not found.", referenced_name); + return 1; } - cdata = calloc(1, sizeof *cdata); - NC_CHECK_ERRMEM_RET(!cdata, 1); + *askey = &ks->entries[i].asym_key; - received_pw_hash = crypt_r(received_pw, stored_pw, cdata); - if (!received_pw_hash) { - ERR(NULL, "Hashing the password failed (%s).", strerror(errno)); - free(cdata); + /* check if the referenced public key is SubjectPublicKeyInfo */ + if ((*askey)->pubkey.data && nc_is_pk_subject_public_key_info((*askey)->pubkey.data)) { + ERR(NULL, "The public key of the referenced hostkey \"%s\" is in the SubjectPublicKeyInfo format, " + "which is not allowed in the SSH!", referenced_name); return 1; } - ret = strcmp(received_pw_hash, stored_pw); - free(cdata); - - return ret; + return 0; } -API int -nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_session libssh_session) +int +nc_server_ssh_ts_ref_get_keys(const char *referenced_name, struct nc_public_key **pubkeys, uint32_t *pubkey_count) { - int ret = 0; - struct timespec ts_timeout = {0}; - ssh_message reply = NULL; - uint16_t auth_timeout = *((uint16_t *)session->data); + LY_ARRAY_COUNT_TYPE i; + struct nc_public_key *pubkey; + struct nc_truststore *ts = &server_opts.config.truststore; - NC_CHECK_ARG_RET(NULL, session, libssh_session, -1); + *pubkeys = NULL; + *pubkey_count = 0; - if (auth_timeout) { - nc_timeouttime_get(&ts_timeout, auth_timeout * 1000); + /* lookup name */ + LY_ARRAY_FOR(ts->pubkey_bags, i) { + if (!strcmp(referenced_name, ts->pubkey_bags[i].name)) { + break; + } + } + if (i == LY_ARRAY_COUNT(ts->pubkey_bags)) { + ERR(NULL, "Truststore entry \"%s\" not found.", referenced_name); + return 1; } - /* wait for answers from the client */ - do { - if (!ssh_is_connected(session->ti.libssh.session)) { - ERR(NULL, "SSH communication socket unexpectedly closed while waiting for keyboard-interactive authentication answers."); - ret = -1; - goto cleanup; + /* check if any of the referenced public keys is SubjectPublicKeyInfo */ + LY_ARRAY_FOR(ts->pubkey_bags[i].pubkeys, struct nc_public_key, pubkey) { + if (nc_is_pk_subject_public_key_info(pubkey->data)) { + ERR(NULL, "A public key of the referenced public key bag \"%s\" is in the SubjectPublicKeyInfo format, " + "which is not allowed in SSH!", referenced_name); + return 1; } + } - reply = ssh_message_get(libssh_session); - if (reply) { - break; - } + *pubkeys = ts->pubkey_bags[i].pubkeys; + *pubkey_count = LY_ARRAY_COUNT(ts->pubkey_bags[i].pubkeys); + return 0; +} - usleep(NC_TIMEOUT_STEP); - } while (auth_timeout && (nc_timeouttime_cur_diff(&ts_timeout) >= 1)); - if (!reply) { - ERR(NULL, "Authentication timeout."); - ret = -1; - goto cleanup; - } +/** + * @brief Convert UID to string. + * + * @param[in] uid UID to convert. + * @return UID converted to string or NULL on fail. + */ +static char * +nc_server_ssh_uid_to_str(uid_t uid) +{ + int buf_len; + char *uid_str; - ret = ssh_userauth_kbdint_getnanswers(libssh_session); + /* get the number of digits and alloc */ + buf_len = snprintf(NULL, 0, "%u", uid); + uid_str = malloc(buf_len + 1); + NC_CHECK_ERRMEM_RET(!uid_str, NULL); -cleanup: - ssh_message_free(reply); - return ret; + /* convert to string */ + sprintf(uid_str, "%u", uid); + uid_str[buf_len] = '\0'; + return uid_str; } -#ifdef HAVE_LIBPAM - /** - * @brief PAM conversation function, which serves as a callback for exchanging messages between the client and a PAM module. + * @brief Append a character or a string to a string. * - * @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. + * @param[in] src_c Source character. + * @param[in] src_str Source string. + * @param[in,out] size Size of the destination string. + * @param[out] idx Index of the next character to write. + * @param[out] dst Destination string. + * @return 0 on success, 1 on error. */ static int -nc_pam_conv_clb(int n_messages, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) +nc_server_ssh_str_append(const char src_c, const char *src_str, int *size, int *idx, char **dst) { - 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; + int src_size, allocate = 0, ret; - /* 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; + /* get size of char/string we want to append */ + if (src_str) { + src_size = strlen(src_str); + } else { + src_size = 1; } - /* 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; - } + /* check if we have enough space, if not realloc */ + while ((src_size + *idx) >= *size) { + (*size) += 16; + allocate = 1; } - - /* 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; - } + if (allocate) { + *dst = nc_realloc(*dst, *size); + NC_CHECK_ERRMEM_RET(!*dst, 1); } - /* 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; + /* append the char/string */ + if (src_str) { + ret = sprintf(*dst + *idx, "%s", src_str); + } else { + ret = sprintf(*dst + *idx, "%c", src_c); + } + if (ret < 0) { + return 1; } - /* 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); + *idx += ret; + return 0; +} - /* 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; - } +/** + * @brief Get the path to the system public keys from format set by an API. + * + * @param[in] username Username. + * @param[out] out_path Path to the system public keys. + * @return 0 on success, 1 on error. + */ +static int +nc_server_ssh_get_system_keys_path(const char *username, char **out_path) +{ + int ret = 0, i, have_percent = 0, size = 0, idx = 0; + const char *path_fmt = server_opts.authkey_path_fmt; + char *path = NULL, *buf = NULL, *uid = NULL; + struct passwd *pw, pw_buf; + size_t buf_len = 0; + + if (!path_fmt) { + ERR(NULL, "System public keys path format not set."); + return 1; } - /* 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++; + /* check if the path format contains any tokens */ + if (strstr(path_fmt, "%h") || strstr(path_fmt, "%U") || strstr(path_fmt, "%u") || strstr(path_fmt, "%%")) { + /* get pw */ + pw = nc_getpw(0, username, &pw_buf, &buf, &buf_len); + if (!pw) { + ERR(NULL, "Unable to get passwd entry for user \"%s\".", username); + ret = 1; + goto cleanup; } - } - /* 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; + /* convert UID to a string */ + uid = nc_server_ssh_uid_to_str(pw->pw_uid); + if (!uid) { + ret = 1; + goto cleanup; + } + } else { + /* no tokens, just copy the path and return */ + *out_path = strdup(path_fmt); + NC_CHECK_ERRMEM_RET(!*out_path, 1); 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; - } + /* go over characters from format, copy them to path and interpret tokens correctly */ + for (i = 0; path_fmt[i]; i++) { + if (have_percent) { + /* special token, need to convert it */ + if (path_fmt[i] == '%') { + ret = nc_server_ssh_str_append('%', NULL, &size, &idx, &path); + } else if (path_fmt[i] == 'h') { + /* user home */ + ret = nc_server_ssh_str_append(0, pw->pw_dir, &size, &idx, &path); + } else if (path_fmt[i] == 'u') { + /* username */ + ret = nc_server_ssh_str_append(0, username, &size, &idx, &path); + } else if (path_fmt[i] == 'U') { + /* UID */ + ret = nc_server_ssh_str_append(0, uid, &size, &idx, &path); + } else { + ERR(NULL, "Failed to parse system public keys path format \"%s\".", server_opts.authkey_path_fmt); + ret = 1; + } - /* 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; + have_percent = 0; + } else { + if (path_fmt[i] == '%') { + have_percent = 1; + } else { + /* ordinary character with no meaning */ + ret = nc_server_ssh_str_append(path_fmt[i], NULL, &size, &idx, &path); } - ERRMEM; - r = PAM_BUF_ERR; + } + + if (ret) { goto cleanup; } } + *out_path = path; + path = NULL; + cleanup: - ssh_message_free(reply); - free(prompts); - free(echo); - return r; + free(uid); + free(buf); + free(path); + return ret; } /** - * @brief Handles authentication via Linux PAM. + * @brief Read public keys from the authorized keys file. * - * @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. + * @param[in] path Path to the authorized keys file. + * @param[out] pubkeys Public keys. + * @param[out] pubkey_count Public key count. + * @return 0 on success, 1 on error. */ static int -nc_server_ssh_auth_kbdint_pam(struct nc_session *session, const char *username, ssh_message ssh_msg) +nc_server_ssh_read_authorized_keys_file(const char *path, struct nc_public_key **pubkeys, uint32_t *pubkey_count) { - pam_handle_t *pam_h = NULL; - int ret; - struct nc_pam_thread_arg clb_data; - struct pam_conv conv; + int ret = 0, rc, line_num = 0; + FILE *f = NULL; + char *line = NULL, *ptr, *ptr2; + size_t n; + enum ssh_keytypes_e ktype; - /* structure holding callback's data */ - clb_data.msg = ssh_msg; - clb_data.session = session; + NC_CHECK_ARG_RET(NULL, path, pubkeys, 1); - /* PAM conversation structure holding the callback and it's data */ - conv.conv = nc_pam_conv_clb; - conv.appdata_ptr = &clb_data; + *pubkeys = NULL; + *pubkey_count = 0; - if (!server_opts.pam_config_name) { - ERR(session, "PAM configuration filename not set."); + f = fopen(path, "r"); + if (!f) { + ERR(NULL, "Unable to open \"%s\" (%s).", path, strerror(errno)); 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; - } + while (getline(&line, &n, f) > -1) { + ++line_num; + if ((line[0] == '#') || (line[0] == '\n')) { + /* comment or empty line */ + continue; + } - /* 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)); + /* separate key type */ + ptr = line; + for (ptr2 = ptr; ptr2[0] && !isspace(ptr2[0]); ptr2++) {} + if (!ptr2[0]) { + ERR(NULL, "Invalid format of authorized keys file \"%s\" on line %d.", path, line_num); + ret = 1; 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; - } + ptr2[0] = '\0'; - /* 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; + /* detect key type */ + ktype = ssh_key_type_from_name(ptr); + if ((ktype != SSH_KEYTYPE_RSA) && (ktype != SSH_KEYTYPE_ECDSA_P256) && (ktype != SSH_KEYTYPE_ECDSA_P384) && + (ktype != SSH_KEYTYPE_ECDSA_P521) && (ktype != SSH_KEYTYPE_ED25519)) { + WRN(NULL, "Unsupported key type \"%s\" in authorized keys file \"%s\" on line %d.", ptr, path, line_num); + continue; } + + /* get key data */ + ptr = ptr2 + 1; + for (ptr2 = ptr; ptr2[0] && !isspace(ptr2[0]); ptr2++) {} + ptr2[0] = '\0'; + + /* add the key */ + *pubkeys = nc_realloc(*pubkeys, (*pubkey_count + 1) * sizeof **pubkeys); + NC_CHECK_ERRMEM_GOTO(!(*pubkeys), ret = 1, cleanup); + rc = asprintf(&(*pubkeys)[*pubkey_count].name, "authorized_key_%" PRIu32, *pubkey_count); + NC_CHECK_ERRMEM_GOTO(rc == -1, (*pubkeys)[*pubkey_count].name = NULL; ret = 1, cleanup); + (*pubkeys)[*pubkey_count].type = NC_PUBKEY_FORMAT_SSH; + (*pubkeys)[*pubkey_count].data = strdup(ptr); + NC_CHECK_ERRMEM_GOTO(!(*pubkeys)[*pubkey_count].data, ret = 1, cleanup); + (*pubkey_count)++; } + /* ok */ + ret = 0; 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)); + if (f) { + fclose(f); } + free(line); 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 +nc_server_ssh_get_system_keys(const char *username, struct nc_public_key **pubkeys, uint32_t *pubkey_count) { - 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); + int ret = 0; + char *path = NULL; - /* send the password prompt to the client */ - ret = ssh_message_auth_interactive_request(msg, name, instruction, 1, (const char **) &prompt, echo); + /* convert the path format to get the actual path */ + ret = nc_server_ssh_get_system_keys_path(username, &path); if (ret) { - ERR(session, "Failed to send an authentication request to client \"%s\".", username); + ERR(NULL, "Getting system keys path failed."); 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; + /* get the keys */ + ret = nc_server_ssh_read_authorized_keys_file(path, pubkeys, pubkey_count); + if (ret) { + ERR(NULL, "Reading system keys failed."); 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); + free(path); return ret; } -#endif +#ifdef HAVE_SHADOW /** - * @brief Keyboard-interactive authentication method using the system's authentication methods. + * @brief Get the user's /etc/passwd entry. * - * @param[in] session NETCONF session. - * @param[in] msg SSH message with a keyboard-interactive authentication request. - * @return 0 on success, non-zero otherwise. + * @param[in] username Username. + * @param[out] pwd_buf Buffer for the passwd structure. + * @param[out] buf Buffer for the pwd's strings. + * @param[out] buf_size Size of the buffer. + * @return User's passwd entry or NULL on error. */ -static int -nc_server_ssh_auth_kbdint_system(struct nc_session *session, ssh_message msg) +static struct passwd * +nc_server_ssh_getpwnam(const char *username, struct passwd *pwd_buf, char **buf, size_t *buf_size) { - 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; -} + struct passwd *pwd = NULL; + char *mem; + int r = 0; -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)) -{ - /* CONFIG LOCK */ - if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { - return; - } + do { + r = getpwnam_r(username, pwd_buf, *buf, *buf_size, &pwd); + if (pwd) { + /* entry found */ + break; + } - server_opts.interactive_auth_clb = interactive_auth_clb; - server_opts.interactive_auth_data = user_data; - server_opts.interactive_auth_data_free = free_user_data; + if (r == ERANGE) { + /* small buffer, enlarge */ + *buf_size <<= 2; + mem = realloc(*buf, *buf_size); + if (!mem) { + ERRMEM; + return NULL; + } + *buf = mem; + } + } while (r == ERANGE); - /* CONFIG UNLOCK */ - nc_rwlock_unlock(&server_opts.config_lock, __func__); + return pwd; } -#ifdef HAVE_LIBPAM - -API int -nc_server_ssh_set_pam_conf_filename(const char *filename) +/** + * @brief Get the user's /etc/shadow entry. + * + * @param[in] username Username. + * @param[out] spwd_buf Buffer for the spwd structure. + * @param[out] buf Buffer for the spwd's strings. + * @param[out] buf_size Size of the buffer. + * @return User's shadow entry or NULL on error. + */ +static struct spwd * +nc_server_ssh_getspnam(const char *username, struct spwd *spwd_buf, char **buf, size_t *buf_size) { - int ret = 0; - - NC_CHECK_ARG_RET(NULL, filename, 1); + struct spwd *spwd = NULL; + char *mem; + int r = 0; - /* CONFIG LOCK */ - if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { - return 1; - } + do { +# ifndef __QNXNTO__ + r = getspnam_r(username, spwd_buf, *buf, *buf_size, &spwd); +# else + spwd = getspnam_r(username, spwd_buf, *buf, *buf_size); + r = errno; +# endif + if (spwd) { + /* entry found */ + break; + } - free(server_opts.pam_config_name); - server_opts.pam_config_name = strdup(filename); - if (!server_opts.pam_config_name) { - ERRMEM; - ret = 1; - } + if (r == ERANGE) { + /* small buffer, enlarge */ + *buf_size <<= 2; + mem = realloc(*buf, *buf_size); + if (!mem) { + ERRMEM; + return NULL; + } + *buf = mem; + } + } while (r == ERANGE); - /* CONFIG UNLOCK */ - nc_rwlock_unlock(&server_opts.config_lock, __func__); - return ret; + return spwd; } -#else - -API int -nc_server_ssh_set_pam_conf_filename(const char *filename) +char * +nc_server_ssh_get_pwd_hash(const char *username) { - /* LibPAM not supported */ - (void) filename; - return 1; -} + struct passwd *pwd, pwd_buf; + struct spwd *spwd, spwd_buf; + char *pass_hash = NULL, *buf = NULL; + size_t buf_size = 256; -#endif /* HAVE_LIBPAM */ + buf = malloc(buf_size); + NC_CHECK_ERRMEM_GOTO(!buf, , error); + + pwd = nc_server_ssh_getpwnam(username, &pwd_buf, &buf, &buf_size); + if (!pwd) { + VRB(NULL, "User \"%s\" not found in the system.", username); + goto error; + } -API int -nc_server_ssh_set_authkey_path_format(const char *path) -{ - int ret = 0; + if (!strcmp(pwd->pw_passwd, "x")) { + spwd = nc_server_ssh_getspnam(username, &spwd_buf, &buf, &buf_size); + if (!spwd) { + VRB(NULL, "Failed to retrieve the shadow entry for \"%s\".", username); + goto error; + } else if ((spwd->sp_expire > -1) && (spwd->sp_expire <= (time(NULL) / (60 * 60 * 24)))) { + WRN(NULL, "User \"%s\" account has expired.", username); + goto error; + } - NC_CHECK_ARG_RET(NULL, path, 1); + pass_hash = spwd->sp_pwdp; + } else { + pass_hash = pwd->pw_passwd; + } - /* CONFIG LOCK */ - if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { - return 1; + if (!pass_hash) { + ERR(NULL, "No password could be retrieved for \"%s\".", username); + goto error; } - free(server_opts.authkey_path_fmt); - server_opts.authkey_path_fmt = strdup(path); - if (!server_opts.authkey_path_fmt) { - ERRMEM; - ret = 1; + /* check the hash structure for special meaning */ + if (!strcmp(pass_hash, "*") || !strcmp(pass_hash, "!")) { + VRB(NULL, "User \"%s\" is not allowed to authenticate using a password.", username); + goto error; + } + if (!strcmp(pass_hash, "*NP*")) { + VRB(NULL, "Retrieving password for \"%s\" from a NIS+ server not supported.", username); + goto error; } - /* CONFIG UNLOCK */ - nc_rwlock_unlock(&server_opts.config_lock, __func__); - return ret; + pass_hash = strdup(pass_hash); + free(buf); + return pass_hash; + +error: + free(buf); + return NULL; } -/** - * @brief Forge the SSH protocol identification string based on the given prefix and the library versions. - * - * @param[in] prefix Optional prefix to include in the protocol string, can be NULL. - * @return Protocol string on success, NULL on error. - */ -static char * -nc_server_ssh_forge_protocol_string(const char *prefix) +int +nc_server_ssh_kbdint_send_passwd_prompt(struct nc_session *session, const char *username, ssh_message msg) { - int r; - char *protocol_str = NULL; + const char *name = "Keyboard-Interactive Authentication"; + const char *instruction = "Please enter your authentication token"; + char *prompt = NULL; + char echo[] = {0}; + int rc; - if (prefix) { - r = asprintf(&protocol_str, "%s-libnetconf2_%s-libssh_%d.%d.%d", - prefix, NC_VERSION, - LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); - } else { - r = asprintf(&protocol_str, "libnetconf2_%s-libssh_%d.%d.%d", - NC_VERSION, - LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); - } - NC_CHECK_ERRMEM_RET(r == -1, NULL); + rc = asprintf(&prompt, "%s's password:", username); + NC_CHECK_ERRMEM_RET(rc == -1, 1); - if (strlen(protocol_str) > 245) { - ERR(NULL, "SSH protocol identification string too long (max 245 characters)."); - free(protocol_str); - return NULL; + 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 1; } - return protocol_str; + return 0; } -API int -nc_server_ssh_set_protocol_string(const char *prefix) +int +nc_server_ssh_kbdint_verify_passwd(struct nc_session *session, const char *username, int n_answers) { - int rc = 0; - char *protocol_str = NULL; - - NC_CHECK_ARG_RET(NULL, prefix, 1); + char *pw = NULL, *received_pw = NULL; + const char *answer; + int rc; - protocol_str = nc_server_ssh_forge_protocol_string(prefix); - NC_CHECK_ERRMEM_GOTO(!protocol_str, rc = 1, cleanup); + if (n_answers != 1) { + ERR(session, "Unexpected amount of answers in system auth. Expected 1, got \"%d\".", n_answers); + return 1; + } - /* CONFIG LOCK */ - if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { - rc = 1; - goto cleanup; + pw = nc_server_ssh_get_pwd_hash(username); + if (!pw) { + return 1; } - /* transfer ownership */ - free(server_opts.ssh_protocol_string); - server_opts.ssh_protocol_string = protocol_str; - protocol_str = NULL; + answer = ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0); + if (!answer) { + ERR(session, "Failed to get keyboard-interactive password answer."); + free(pw); + return 1; + } + received_pw = strdup(answer); + if (!received_pw) { + ERRMEM; + free(pw); + return 1; + } - /* CONFIG UNLOCK */ - nc_rwlock_unlock(&server_opts.config_lock, __func__); + rc = nc_server_ssh_compare_password(pw, received_pw); + free(pw); + free(received_pw); -cleanup: - free(protocol_str); return rc; } -/** - * @brief Get the public key type from binary data. - * - * @param[in] buffer Binary key data, which is in the form of: 4 bytes = data length, then data of data length. - * Data is in network byte order. The key has to be in the SSH2 format. - * @param[out] len Length of the key type. - * @return Pointer to where the key type starts in the buffer and is of the length @p len . - */ -static const char * -nc_server_ssh_get_pubkey_type(const unsigned char *buffer, uint32_t *len) -{ - uint32_t type_len; - - /* copy the 4 bytes */ - memcpy(&type_len, buffer, sizeof type_len); - /* type_len now stores the length of the key type */ - type_len = ntohl(type_len); - *len = type_len; - - /* move 4 bytes in the buffer, this is where the type should be */ - buffer += sizeof type_len; - return (const char *)buffer; -} +#endif -/** - * @brief Create ssh key from base64 pubkey data. - * - * @param[in] base64 base64 encoded public key. - * @param[out] key created ssh key. - * @return 0 on success, 1 otherwise. - */ -static int -nc_server_ssh_create_ssh_pubkey(const char *base64, ssh_key *key) +int +nc_server_ssh_compare_password(const char *stored_pw, const char *received_pw) { - int ret = 0; - unsigned char *bin = NULL; - const char *pub_type = NULL; - uint32_t pub_type_len = 0; - - NC_CHECK_ARG_RET(NULL, base64, key, 1); + char *received_pw_hash = NULL; + struct crypt_data *cdata; + int ret; - *key = NULL; + NC_CHECK_ARG_RET(NULL, stored_pw, received_pw, 1); - /* convert base64 to binary */ - if (nc_base64_decode_wrap(base64, &bin) == -1) { - ret = 1; - goto cleanup; + if (!stored_pw[0]) { + if (!received_pw[0]) { + WRN(NULL, "User authentication successful with an empty password!"); + return 0; + } else { + /* the user did now know he does not need any password, + * (which should not be used) so deny authentication */ + return 1; + } } - /* get the key type and try to import it if possible */ - pub_type = nc_server_ssh_get_pubkey_type(bin, &pub_type_len); - if (!pub_type) { - ret = 1; - goto cleanup; - } else if (!strncmp(pub_type, "ssh-dss", pub_type_len)) { - ERR(NULL, "DSA keys are not supported."); - ret = 1; - goto cleanup; - } else if (!strncmp(pub_type, "ssh-rsa", pub_type_len)) { - ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_RSA, key); - } else if (!strncmp(pub_type, "ecdsa-sha2-nistp256", pub_type_len)) { - ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P256, key); - } else if (!strncmp(pub_type, "ecdsa-sha2-nistp384", pub_type_len)) { - ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P384, key); - } else if (!strncmp(pub_type, "ecdsa-sha2-nistp521", pub_type_len)) { - ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P521, key); - } else if (!strncmp(pub_type, "ssh-ed25519", pub_type_len)) { - ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ED25519, key); - } else { - ERR(NULL, "Public key type not recognised."); - ret = 1; - goto cleanup; + if (!strncmp(stored_pw, "$0$", 3)) { + /* cleartext password, simply compare the values */ + return strcmp(stored_pw + 3, received_pw); } -cleanup: - if (ret != SSH_OK) { - ERR(NULL, "Error importing public key."); + cdata = calloc(1, sizeof *cdata); + NC_CHECK_ERRMEM_RET(!cdata, 1); + + received_pw_hash = crypt_r(received_pw, stored_pw, cdata); + if (!received_pw_hash) { + ERR(NULL, "Hashing the password failed (%s).", strerror(errno)); + free(cdata); + return 1; } - free(bin); + + ret = strcmp(received_pw_hash, stored_pw); + free(cdata); + 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 -nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys, uint16_t pubkey_count) +API int +nc_server_ssh_kbdint_get_nanswers(const struct nc_session *session, ssh_session libssh_session) { - uint16_t i; int ret = 0; - ssh_key new_key = NULL; + struct timespec ts_timeout = {0}; + ssh_message reply = NULL; + uint16_t auth_timeout = *((uint16_t *)session->data); - /* try to compare all of the client's keys with the key received in the SSH message */ - for (i = 0; i < pubkey_count; i++) { - /* create the SSH key from the data */ - if (nc_server_ssh_create_ssh_pubkey(pubkeys[i].data, &new_key)) { - /* skip */ - ssh_key_free(new_key); - continue; + NC_CHECK_ARG_RET(NULL, session, libssh_session, -1); + + if (auth_timeout) { + nc_timeouttime_get(&ts_timeout, auth_timeout * 1000); + } + + /* wait for answers from the client */ + do { + if (!ssh_is_connected(session->ti.libssh.session)) { + ERR(NULL, "SSH communication socket unexpectedly closed while waiting for keyboard-interactive authentication answers."); + ret = -1; + goto cleanup; } - /* compare the keys */ - ret = ssh_key_cmp(key, new_key, SSH_KEY_CMP_PUBLIC); - ssh_key_free(new_key); - if (!ret) { - /* found a match */ + reply = ssh_message_get(libssh_session); + if (reply) { break; } - } - if (i == pubkey_count) { - ret = 1; + + usleep(NC_TIMEOUT_STEP); + } while (auth_timeout && (nc_timeouttime_cur_diff(&ts_timeout) >= 1)); + if (!reply) { + ERR(NULL, "Authentication timeout."); + ret = -1; + goto cleanup; } + ret = ssh_userauth_kbdint_getnanswers(libssh_session); + +cleanup: + ssh_message_free(reply); return ret; } -/** - * @brief Send the SSH issue banner if configured. - * - * @param[in] session NETCONF session. - * @param[in] opts SSH server options. - */ -static void -nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts *opts) +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)) { - if (!opts->banner) { + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { return; } -#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) - ssh_string ban; + server_opts.interactive_auth_clb = interactive_auth_clb; + server_opts.interactive_auth_data = user_data; + server_opts.interactive_auth_data_free = free_user_data; - ban = ssh_string_from_char(opts->banner); - if (ban) { - if (ssh_send_issue_banner(session->ti.libssh.session, ban)) { - ERR(session, "Failed to send SSH banner (%s).", ssh_get_error(session->ti.libssh.session)); - } - ssh_string_free(ban); - } -#else - WRN(session, "SSH banner set but cannot be sent (libssh version 0.10.0 or later required)."); -#endif + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); } -/** - * @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; -} +#ifdef HAVE_LIBPAM -/** - * @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_auth_password(struct nc_session *session, int local_users_supported, - struct nc_auth_client *auth_client, ssh_message msg) +API int +nc_server_ssh_set_pam_conf_filename(const char *filename) { - int rc; - char *password = NULL; + int ret = 0; - assert(!local_users_supported || auth_client); + NC_CHECK_ARG_RET(NULL, filename, 1); - 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."); + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { return 1; -#endif } - /* compare the passwords */ - rc = nc_server_ssh_compare_password(password, ssh_message_auth_password(msg)); - - if (!local_users_supported) { - free(password); + free(server_opts.pam_config_name); + server_opts.pam_config_name = strdup(filename); + if (!server_opts.pam_config_name) { + ERRMEM; + ret = 1; } - return rc ? 1 : 0; + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); + return ret; } -/** - * @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; +#else - assert(!local_users_supported || auth_client); +API int +nc_server_ssh_set_pam_conf_filename(const char *filename) +{ + /* LibPAM not supported */ + (void) filename; + return 1; +} - /* 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; - } +#endif /* HAVE_LIBPAM */ - 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; - } - } +API int +nc_server_ssh_set_authkey_path_format(const char *path) +{ + int ret = 0; - /* 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; - } + NC_CHECK_ARG_RET(NULL, path, 1); - 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; + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { + return 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); + free(server_opts.authkey_path_fmt); + server_opts.authkey_path_fmt = strdup(path); + if (!server_opts.authkey_path_fmt) { + ERRMEM; + ret = 1; } + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); return ret; } /** - * @brief Handle authentication request for the Keyboard-interactive method. + * @brief Forge the SSH protocol identification string based on the given prefix and the library versions. * - * @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. + * @param[in] prefix Optional prefix to include in the protocol string, can be NULL. + * @return Protocol string on success, NULL on error. */ -static int -nc_server_ssh_auth_kbdint(struct nc_session *session, int local_users_supported, struct nc_auth_client *auth_client, ssh_message msg) +static char * +nc_server_ssh_forge_protocol_string(const char *prefix) { - int r = 0; - - assert(!local_users_supported || auth_client); + int r; + char *protocol_str = NULL; - if (!local_users_supported) { - /* no local users supported, use the system method */ - r = nc_server_ssh_auth_kbdint_system(session, msg); + if (prefix) { + r = asprintf(&protocol_str, "%s-libnetconf2_%s-libssh_%d.%d.%d", + prefix, NC_VERSION, + LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); } 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; - } + r = asprintf(&protocol_str, "libnetconf2_%s-libssh_%d.%d.%d", + NC_VERSION, + LIBSSH_VERSION_MAJOR, LIBSSH_VERSION_MINOR, LIBSSH_VERSION_MICRO); + } + NC_CHECK_ERRMEM_RET(r == -1, NULL); - 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; - } - } + if (strlen(protocol_str) > 245) { + ERR(NULL, "SSH protocol identification string too long (max 245 characters)."); + free(protocol_str); + return NULL; } - return r ? 1 : 0; + return protocol_str; } -/** - * @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) +API int +nc_server_ssh_set_protocol_string(const char *prefix) { - 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 */ - } + int rc = 0; + char *protocol_str = NULL; - return 0; -} + NC_CHECK_ARG_RET(NULL, prefix, 1); -/** - * @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; + protocol_str = nc_server_ssh_forge_protocol_string(prefix); + NC_CHECK_ERRMEM_GOTO(!protocol_str, rc = 1, cleanup); - if (strcmp(subsystem, "netconf")) { - WRN(session, "Received an unknown subsystem \"%s\" request.", subsystem); - return -1; + /* CONFIG LOCK */ + if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) { + rc = 1; + goto cleanup; } - 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; - } + /* transfer ownership */ + free(server_opts.ssh_protocol_string); + server_opts.ssh_protocol_string = protocol_str; + protocol_str = NULL; + + /* CONFIG UNLOCK */ + nc_rwlock_unlock(&server_opts.config_lock, __func__); - 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); +cleanup: + free(protocol_str); + return rc; +} - /* 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; +/** + * @brief Get the public key type from binary data. + * + * @param[in] buffer Binary key data, which is in the form of: 4 bytes = data length, then data of data length. + * Data is in network byte order. The key has to be in the SSH2 format. + * @param[out] len Length of the key type. + * @return Pointer to where the key type starts in the buffer and is of the length @p len . + */ +static const char * +nc_server_ssh_get_pubkey_type(const unsigned char *buffer, uint32_t *len) +{ + uint32_t type_len; - 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; - } + /* copy the 4 bytes */ + memcpy(&type_len, buffer, sizeof type_len); + /* type_len now stores the length of the key type */ + type_len = ntohl(type_len); + *len = type_len; - return 0; + /* move 4 bytes in the buffer, this is where the type should be */ + buffer += sizeof type_len; + return (const char *)buffer; } /** - * @brief Handle NETCONF SSH authentication. + * @brief Create ssh key from base64 pubkey data. * - * @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. + * @param[in] base64 base64 encoded public key. + * @param[out] key created ssh key. + * @return 0 on success, 1 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) +nc_server_ssh_create_ssh_pubkey(const char *base64, ssh_key *key) { - 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); + unsigned char *bin = NULL; + const char *pub_type = NULL; + uint32_t pub_type_len = 0; - /* send the SSH issue banner on the first userauth request */ - nc_server_ssh_send_banner(session, opts); + NC_CHECK_ARG_RET(NULL, base64, key, 1); - /* 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; - } + *key = NULL; - 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; - } + /* convert base64 to binary */ + if (nc_base64_decode_wrap(base64, &bin) == -1) { + ret = 1; + goto cleanup; } - /* 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); + /* get the key type and try to import it if possible */ + pub_type = nc_server_ssh_get_pubkey_type(bin, &pub_type_len); + if (!pub_type) { + ret = 1; + goto cleanup; + } else if (!strncmp(pub_type, "ssh-dss", pub_type_len)) { + ERR(NULL, "DSA keys are not supported."); + ret = 1; + goto cleanup; + } else if (!strncmp(pub_type, "ssh-rsa", pub_type_len)) { + ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_RSA, key); + } else if (!strncmp(pub_type, "ecdsa-sha2-nistp256", pub_type_len)) { + ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P256, key); + } else if (!strncmp(pub_type, "ecdsa-sha2-nistp384", pub_type_len)) { + ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P384, key); + } else if (!strncmp(pub_type, "ecdsa-sha2-nistp521", pub_type_len)) { + ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ECDSA_P521, key); + } else if (!strncmp(pub_type, "ssh-ed25519", pub_type_len)) { + ret = ssh_pki_import_pubkey_base64(base64, SSH_KEYTYPE_ED25519, key); } else { - ++session->opts.server.ssh_auth_attempts; - VRB(session, "Authentication method \"%s\" not supported.", str_method); - ssh_message_reply_default(msg); - return 0; + ERR(NULL, "Public key type not recognised."); + ret = 1; + goto cleanup; } - 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); +cleanup: + if (ret != SSH_OK) { + ERR(NULL, "Error importing public key."); } - - return 0; + free(bin); + return ret; } int -nc_session_ssh_msg(struct nc_session *session, struct nc_server_ssh_opts *opts, ssh_message msg, struct nc_auth_state *auth_state) +nc_server_ssh_auth_pubkey_compare_key(ssh_key key, struct nc_public_key *pubkeys, uint16_t pubkey_count) { - 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"; - break; - } - break; + uint16_t i; + int ret = 0; + ssh_key new_key = NULL; - 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; + /* try to compare all of the client's keys with the key received in the SSH message */ + for (i = 0; i < pubkey_count; i++) { + /* create the SSH key from the data */ + if (nc_server_ssh_create_ssh_pubkey(pubkeys[i].data, &new_key)) { + /* skip */ + ssh_key_free(new_key); + continue; } - 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"; + /* compare the keys */ + ret = ssh_key_cmp(key, new_key, SSH_KEY_CMP_PUBLIC); + ssh_key_free(new_key); + if (!ret) { + /* found a match */ 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; + if (i == pubkey_count) { + ret = 1; } - /* - * 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; - } + return ret; +} - /* 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; - } +void +nc_server_ssh_send_banner(struct nc_session *session, struct nc_server_ssh_opts *opts) +{ + if (!opts->banner) { + return; + } - /* 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; +#if (LIBSSH_VERSION_MAJOR > 0) || (LIBSSH_VERSION_MAJOR == 0 && LIBSSH_VERSION_MINOR >= 10) + ssh_string ban; - } 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; + ban = ssh_string_from_char(opts->banner); + if (ban) { + if (ssh_send_issue_banner(session->ti.libssh.session, ban)) { + ERR(session, "Failed to send SSH banner (%s).", ssh_get_error(session->ti.libssh.session)); } + ssh_string_free(ban); } - - /* we did not process it */ - return 1; +#else + WRN(session, "SSH banner set but cannot be sent (libssh version 0.10.0 or later required)."); +#endif } /* ret 1 on success, 0 on timeout, -1 on error */ @@ -1929,11 +1516,51 @@ static int nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc_server_ssh_opts *opts) { struct timespec ts_timeout; + +#if LIBSSH_0_12 + int32_t time_diff; + int ret; +#else ssh_message msg; +#endif DBG(session, "Waiting for \"netconf\" SSH subsystem request..."); nc_timeouttime_get(&ts_timeout, NC_TRANSPORT_MSG_TIMEOUT); + +#if LIBSSH_0_12 + (void) opts; + + /* 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; + } + + 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; + } + + /* This functions listens to the network and automatically calls callback funcitons. */ + ret = ssh_event_dopoll(session->ti.libssh.event, time_diff); + if (ret == SSH_ERROR) { + ERR(session, "Failed to poll SSH event (%s).", ssh_get_error(session->ti.libssh.session)); + return -1; + } else if (ret == SSH_AGAIN) { + /* Timeout reached */ + break; + } + } + + 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."); @@ -1959,7 +1586,7 @@ nc_accept_ssh_session_open_netconf_channel(struct nc_session *session, struct nc break; } } - +#endif return 0; } @@ -2007,9 +1634,16 @@ nc_ssh_bind_add_hostkeys(ssh_bind sbind, struct nc_server_ssh_opts *opts) static int nc_accept_ssh_session_auth(struct nc_session *session, struct nc_server_ssh_opts *opts) { - struct timespec ts_timeout; + struct timespec ts_timeout = {0}; + +#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 +1651,50 @@ 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(); + if (!event) { + ERR(session, "Failed to create SSH event."); + return -1; + } + + if (ssh_event_add_session(event, session->ti.libssh.session) == SSH_ERROR) { + ERR(session, "Failed to add SSH session to event."); + ssh_event_free(event); + return -1; + } + 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; + } + + if (opts->auth_timeout) { + time_diff = nc_timeouttime_cur_diff(&ts_timeout); + if (time_diff < 1) { + /* timeout */ + break; + } + } else { + /* no authentication timeout, wait indefinitely */ + time_diff = -1; + } + + /* This functions listens to the network and automatically calls callback funcitons. */ + ret = ssh_event_dopoll(event, time_diff); + if (ret == SSH_ERROR) { + ERR(session, "Failed to poll SSH event (%s).", ssh_get_error(session->ti.libssh.session)); + return -1; + } else if (ret == SSH_AGAIN) { + /* Timeout reached */ + 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 +1719,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 +1743,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 +1756,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."); @@ -2189,6 +1890,15 @@ nc_accept_ssh_session(struct nc_session *session, struct nc_server_ssh_opts *opt session->data = &opts->auth_timeout; rc = nc_accept_ssh_session_auth(session, opts); session->data = NULL; + +#if LIBSSH_0_12 && defined (HAVE_LIBPAM) + /* if a PAM thread is still running (auth may have succeeded via another method), cancel and clean it up */ + if (cb_data) { + nc_server_ssh_cb_pam_cancel(cb_data->pam_kbdint); + cb_data->pam_kbdint = NULL; + } +#endif + if (rc != 1) { 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..646fb588 --- /dev/null +++ b/src/session_server_ssh_auth_callback.c @@ -0,0 +1,838 @@ +/** + * @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 */ + +#include +#include +#include +#include + +#ifdef HAVE_LIBPAM +# include +#endif + +#include "compat.h" +#include "log_p.h" +#include "session_server_ssh_wrapper.h" + +#ifdef HAVE_LIBPAM + +/** Time [s] to wait for a cancelled PAM thread to terminate before detaching it. */ +#define NC_PAM_THREAD_JOIN_TIMEOUT 1 + +/** + * @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 r, n_prompts = 0; + const char **prompts = NULL; + char *echo = NULL; + + /* parse the PAM messages into prompts */ + r = nc_server_ssh_pam_conv_parse(data->session, n_messages, msg, resp, &n_prompts, &prompts, &echo); + if (r != PAM_SUCCESS) { + return r; + } + if (!n_prompts) { + /* no actual prompts */ + return PAM_SUCCESS; + } + + /* signal main thread that prompts are ready */ + /* LOCK */ + pthread_mutex_lock(&data->lock); + if (data->state == NC_PAM_CANCELLED) { + goto cancelled; + } + data->n_prompts = n_prompts; + 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) { + goto cancelled; + } + + /* state == NC_PAM_ANSWERS_READY: copy answers (freed by PAM on failure) */ + r = nc_server_ssh_pam_conv_fill(data->session, *resp, n_prompts, data->n_answers, + (const char **)data->answers); + /* UNLOCK */ + pthread_mutex_unlock(&data->lock); + + /* free prompts/echo arrays */ + free(prompts); + free(echo); + return r; + +cancelled: + /* UNLOCK */ + pthread_mutex_unlock(&data->lock); + free(prompts); + free(echo); + free(*resp); + *resp = NULL; + return PAM_CONV_ERR; +} + +/** + * @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; + + /* run the PAM sequence (the PAM handle is created and released inside) */ + ret = nc_server_ssh_pam_authenticate(data->session, data->username, &conv); + + /* LOCK */ + pthread_mutex_lock(&data->lock); + data->pam_ret = ret; + data->state = NC_PAM_DONE; + pthread_cond_signal(&data->changed); + /* UNLOCK */ + pthread_mutex_unlock(&data->lock); + + 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) +{ +#ifdef HAVE_PTHREAD_TIMEDJOIN_NP + struct timespec ts_timeout; +#endif + + if (!data) { + return; + } + + /* signal PAM thread to cancel */ + /* LOCK */ + 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_DONE) && (data->state != NC_PAM_CANCELLED)) { + /* PAM thread is still active - mark as cancelled and force it out of any blocking call; + * any lock held at the cancellation point stays locked but the data is never used again */ + data->state = NC_PAM_CANCELLED; + pthread_cancel(data->thread); + } + /* UNLOCK */ + pthread_mutex_unlock(&data->lock); + +#ifdef HAVE_PTHREAD_TIMEDJOIN_NP + /* join with a timeout, never block indefinitely */ + nc_realtime_get(&ts_timeout); + ts_timeout.tv_sec += NC_PAM_THREAD_JOIN_TIMEOUT; + if (pthread_timedjoin_np(data->thread, NULL, &ts_timeout)) { + /* the thread could not be cancelled (misbehaving PAM module stuck at no cancellation point), + * so just detach it and leak its resources, there is nothing more to do */ + pthread_detach(data->thread); + ERR(data->session, "Unable to cancel a PAM thread, leaking its resources."); + return; + } +#else + pthread_join(data->thread, NULL); +#endif + + nc_server_ssh_cb_pam_data_free(data); +} + +/** + * @brief Cancel and free the PAM exchange stored in the callback data, if any. + * + * @param[in] cb_data Callback data. + */ +static void +nc_server_ssh_cb_kbdint_pam_cancel_stored(struct nc_server_ssh_cb_data *cb_data) +{ + if (cb_data->pam_kbdint) { + nc_server_ssh_cb_pam_cancel(cb_data->pam_kbdint); + cb_data->pam_kbdint = NULL; + } +} + +/** + * @brief Finish a completed PAM exchange: join the PAM thread and free its data. + * + * @param[in] cb_data Callback data (pam_kbdint pointer is cleared). + * @param[in] pam_data PAM exchange data, called with its lock held (unlocked here). + * @return SSH_AUTH_SUCCESS if PAM authentication succeeded. + * @return SSH_AUTH_DENIED otherwise. + */ +static int +nc_server_ssh_cb_pam_finish(struct nc_server_ssh_cb_data *cb_data, struct nc_server_ssh_cb_pam_data *pam_data) +{ + int rc; + + rc = pam_data->pam_ret; + /* UNLOCK */ + 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; +} + +/** + * @brief Send PAM prompts to the client as a keyboard-interactive request. + * + * @param[in] cb_data Callback data (pam_kbdint pointer is cleared on error). + * @param[in] pam_data PAM exchange data, called with its lock held (unlocked here). + * @param[in] message SSH message for sending prompts. + * @return SSH_AUTH_INFO if the prompts were sent. + * @return SSH_AUTH_DENIED on error (the PAM exchange is cancelled). + */ +static int +nc_server_ssh_cb_kbdint_pam_send_prompts(struct nc_server_ssh_cb_data *cb_data, + struct nc_server_ssh_cb_pam_data *pam_data, ssh_message message) +{ + int rc, n_prompts; + const char **prompts; + char *echo; + + n_prompts = pam_data->n_prompts; + prompts = pam_data->prompts; + echo = pam_data->echo; + + /* UNLOCK */ + 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 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. + * @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) +{ + struct nc_server_ssh_cb_pam_data *pam_data; + int rc; + + /* check the PAM configuration */ + if (!server_opts.pam_config_name) { + ERR(cb_data->session, "PAM configuration filename not set."); + return SSH_AUTH_DENIED; + } + + /* cancel any in-progress PAM exchange, e.g. the client abandoned the previous one */ + nc_server_ssh_cb_kbdint_pam_cancel_stored(cb_data); + + /* 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 */ + /* LOCK */ + 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) */ + return nc_server_ssh_cb_pam_finish(cb_data, pam_data); + } + + /* state == NC_PAM_PROMPTS_READY: send prompts to client */ + return nc_server_ssh_cb_kbdint_pam_send_prompts(cb_data, pam_data, message); +} + +/** + * @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 = SSH_AUTH_DENIED; + char **answers = NULL; + const char *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; + } + + if (n_answers) { + answers = calloc(n_answers, sizeof *answers); + NC_CHECK_ERRMEM_GOTO(!answers, (rc = SSH_AUTH_DENIED, n_answers = 0), 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 */ + /* LOCK */ + 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) { + return nc_server_ssh_cb_pam_finish(cb_data, pam_data); + } + + /* state == NC_PAM_PROMPTS_READY: send new prompts to client */ + return nc_server_ssh_cb_kbdint_pam_send_prompts(cb_data, pam_data, message); + +cleanup: + if (answers) { + 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) +{ + return nc_server_ssh_kbdint_send_passwd_prompt(session, username, msg) ? SSH_AUTH_DENIED : 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) +{ + int n_answers = ssh_userauth_kbdint_getnanswers(session->ti.libssh.session); + + return nc_server_ssh_kbdint_verify_passwd(session, username, n_answers) ? SSH_AUTH_DENIED : SSH_AUTH_SUCCESS; +} + +#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; + + if (session->flags & NC_SESSION_SSH_AUTHENTICATED) { + ERR(session, "User \"%s\" authenticated, but requested another authentication.", session->username); + return -1; + } + + *local_users_supported = 0; + *auth_client = NULL; + + if (!user) { + nc_server_ssh_auth_attempt_failed(session); + 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; + nc_server_ssh_auth_attempt_failed(session); + 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 */ + nc_server_ssh_auth_attempt_failed(session); + return -1; + } + + /* Find the auth client if local users are supported */ + if (*local_users_supported) { + *auth_client = nc_ssh_find_auth_client(opts, user, session); + + 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); + nc_server_ssh_auth_attempt_failed(session); + 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)) { + 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); + } + + nc_server_ssh_auth_attempt_failed(session); + 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 = 0; + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + return SSH_AUTH_DENIED; + } + + rc = nc_server_ssh_auth_password_check(session, user, password, auth_client, local_users_supported); + + if (rc == 0) { + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_PASSWORD); + } else { + nc_server_ssh_auth_attempt_failed(session); + 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; + + if (nc_server_ssh_cb_auth_common_setup(cb_data, user, &local_users_supported, &auth_client)) { + return SSH_AUTH_DENIED; + } + + ret = nc_server_ssh_auth_pubkey_check(session, pubkey, auth_client, local_users_supported); + + if (ret == 0) { + if (signature_state == SSH_PUBLICKEY_STATE_NONE) { + /* just checking if the public key would be accepted */ + return SSH_AUTH_SUCCESS; + } else if (signature_state == SSH_PUBLICKEY_STATE_VALID) { + return nc_ssh_auth_success(session, &cb_data->auth_state, SSH_AUTH_METHOD_PUBLICKEY); + } else { + VRB(session, "User \"%s\" tried to use an invalid public key signature.", session->username); + nc_server_ssh_auth_attempt_failed(session); + return SSH_AUTH_DENIED; + } + } else { + nc_server_ssh_auth_attempt_failed(session); + return SSH_AUTH_DENIED; + } +} + +/** + * @brief Dispatch callback-based system keyboard-interactive authentication. + * + * @param[in] cb_data Callback data. + * @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 + (void)user; + 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); + } +#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; + enum nc_kbdint_backend backend; + 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)) { +#ifdef HAVE_LIBPAM + /* cancel any in-progress PAM exchange before denying */ + nc_server_ssh_cb_kbdint_pam_cancel_stored(cb_data); +#endif + return SSH_AUTH_DENIED; + } + + /* select the kbdint backend based on the configuration */ + if (nc_server_ssh_kbdint_select_method(session, local_users_supported, auth_client, &backend)) { + /* denied, the reason was already logged */ + nc_server_ssh_auth_attempt_failed(session); +#ifdef HAVE_LIBPAM + /* cancel any in-progress PAM exchange before denying */ + nc_server_ssh_cb_kbdint_pam_cancel_stored(cb_data); +#endif + return SSH_AUTH_DENIED; + } + + if (backend == NC_KBDINT_BACKEND_CUSTOM_CLB) { + /* custom interactive auth callback */ + ret = server_opts.interactive_auth_clb(session, + session->ti.libssh.session, message, server_opts.interactive_auth_data); + } else { + ret = nc_server_ssh_cb_kbdint_system(cb_data, message, user); + } + + /* 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) || (ret == SSH_AUTH_PARTIAL)) { + /* the custom callback may return SSH_AUTH_PARTIAL (libssh doc), treat it as a method + * success — the outcome is recomputed against the configured methods */ + 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); + nc_server_ssh_auth_attempt_failed(session); + 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; + struct nc_ssh_channel_cb_data **chan_ptr; + int rc; + + rc = nc_server_ssh_channel_subsys_check(session, channel, subsystem); + if (rc < 0) { + /* invalid request */ + return 1; + } + if (!rc) { + /* the "netconf" subsystem requested on the first channel */ + return 0; + } + + /* additional channel subsystem request - the channel must still be unclaimed */ + chan_ptr = &cb_data->channels; + while (*chan_ptr && (*chan_ptr != channel_data)) { + chan_ptr = &(*chan_ptr)->next; + } + if (!*chan_ptr) { + /* channel already claimed by an earlier subsystem request (normally caught by + * nc_server_ssh_channel_subsys_check()) */ + return 1; + } + + /* new session is ready as far as SSH is concerned */ + new_session = nc_server_ssh_new_channel_session(session, channel); + if (!new_session) { + return 1; + } + + /* Transfer ownership of channel callbacks to the new session and remove from cb_data to avoid double-free */ + *chan_ptr = channel_data->next; + channel_data->next = NULL; + + 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; + } + +#ifdef HAVE_LIBPAM + /* safety net: cancel a PAM thread left running, e.g. when auth succeeded via another method */ + if (data->pam_kbdint) { + nc_server_ssh_cb_pam_cancel(data->pam_kbdint); + data->pam_kbdint = NULL; + } +#endif + + /* 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..f977463b --- /dev/null +++ b/src/session_server_ssh_auth_message.c @@ -0,0 +1,638 @@ +/** + * @file session_server_ssh_auth_message.c + * @author Michal Vasko + * @brief libnetconf2 SSH authentication with messages + * + * @copyright + * Copyright (c) 2017 - 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 */ + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_LIBPAM +# include +#endif + +#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, r, n_answers, n_prompts = 0; + const char **prompts = NULL, **answers = NULL, *answer; + char *echo = NULL; + struct nc_pam_thread_arg *clb_data = appdata_ptr; + ssh_session libssh_session; + + libssh_session = clb_data->session->ti.libssh.session; + + /* parse the PAM messages into prompts */ + r = nc_server_ssh_pam_conv_parse(clb_data->session, n_messages, msg, resp, &n_prompts, &prompts, &echo); + if (r != PAM_SUCCESS) { + return r; + } + if (!n_prompts) { + /* there are no requests left for the user, only messages with some information for the client were sent */ + return PAM_SUCCESS; + } + + /* print all the keyboard-interactive challenges to the user */ + r = ssh_message_auth_interactive_request(clb_data->msg, NC_PAM_KBDINT_NAME, NC_PAM_KBDINT_INSTRUCTION, + n_prompts, 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; + } + + /* collect the replies */ + if (n_answers) { + answers = calloc(n_answers, sizeof *answers); + NC_CHECK_ERRMEM_GOTO(!answers, r = PAM_BUF_ERR, cleanup); + } + 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); + r = PAM_CONV_ERR; + goto cleanup; + } + answers[i] = answer; + } + + /* give the replies to a PAM module (also checks that the counts match) */ + r = nc_server_ssh_pam_conv_fill(clb_data->session, *resp, n_prompts, n_answers, answers); + +cleanup: + free(prompts); + free(echo); + free(answers); + return r; +} + +/** + * @brief Handles authentication via Linux PAM. + * + * @param[in] session NETCONF session. + * @param[in] username Username of the client to authenticate. + * @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) +{ + 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 its data */ + conv.conv = nc_pam_conv_clb; + conv.appdata_ptr = &clb_data; + + /* run the PAM sequence (the PAM handle is created and released inside) */ + return nc_server_ssh_pam_authenticate(session, username, &conv); +} + +#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 n_answers; + + /* send the password prompt to the client */ + if (nc_server_ssh_kbdint_send_passwd_prompt(session, username, msg)) { + return 1; + } + + /* get the reply */ + n_answers = nc_server_ssh_kbdint_get_nanswers(session, session->ti.libssh.session); + if (n_answers < 0) { + /* timeout or dc */ + return 1; + } + + /* verify the answer against the user's system password hash */ + return nc_server_ssh_kbdint_verify_passwd(session, username, n_answers) ? 1 : 0; +} + +#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; + + rc = nc_server_ssh_auth_password_check(session, session->username, ssh_message_auth_password(msg), auth_client, local_users_supported); + + 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; + + ret = nc_server_ssh_auth_pubkey_check(session, ssh_message_auth_pubkey(msg), auth_client, local_users_supported); + if (!ret) { + /* the key is authorized */ + 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; + } + } + + 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; + enum nc_kbdint_backend backend; + + /* select the kbdint backend based on the configuration */ + if (nc_server_ssh_kbdint_select_method(session, local_users_supported, auth_client, &backend)) { + return 1; + } + + if (backend == NC_KBDINT_BACKEND_CUSTOM_CLB) { + /* custom callback has higher priority */ + r = server_opts.interactive_auth_clb(session, + session->ti.libssh.session, msg, server_opts.interactive_auth_data); + } else { + r = nc_server_ssh_msg_auth_kbdint_system(session, msg); + } + + 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; + int rc; + + rc = nc_server_ssh_channel_subsys_check(session, channel, subsystem); + if (rc < 0) { + return -1; + } + if (!rc) { + /* the "netconf" subsystem requested on the first channel */ + return 0; + } + + /* additional channel subsystem request, new session is ready as far as SSH is concerned */ + new_session = nc_server_ssh_new_channel_session(session, channel); + if (!new_session) { + return -1; + } + + 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; + int first_time = 0; + + /* save the username, do not let the client change it */ + username = ssh_message_auth_user(msg); + assert(username); + + if (!session->username) { + session->username = strdup(username); + NC_CHECK_ERRMEM_RET(!session->username, 1); + first_time = 1; + + /* send the SSH issue banner on the first userauth request */ + nc_server_ssh_send_banner(session, opts); + } 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; + nc_server_ssh_auth_attempt_failed(session); + return 1; + } + + if (local_users_supported) { + auth_client = nc_ssh_find_auth_client(opts, username, session); + + 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(session, "User \"%s\" not known by the server.", username); + ssh_set_auth_methods(session->ti.libssh.session, SSH_AUTH_METHOD_PUBLICKEY); + nc_server_ssh_auth_attempt_failed(session); + ssh_message_reply_default(msg); + return 0; + } + } + + if (first_time) { + /* configure and count accepted auth methods */ + nc_ssh_auth_state_init(session, auth_state, local_users_supported, auth_client); + } + + /* 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); + } + } else if (ret == 1) { + /* failed attempt, msg wasnt yet replied to */ + nc_server_ssh_auth_attempt_failed(session); + 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..09b85806 --- /dev/null +++ b/src/session_server_ssh_wrapper.h @@ -0,0 +1,471 @@ +/** + * @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 */ +#include "session_p.h" + +#ifdef NC_ENABLED_SSH_TLS + # include +#endif + +#ifdef HAVE_LIBPAM +# include + +/** Fixed prompt name sent with every keyboard-interactive request. */ +#define NC_PAM_KBDINT_NAME "Keyboard-Interactive Authentication" + +/** Fixed prompt instruction sent with every keyboard-interactive request. */ +#define NC_PAM_KBDINT_INSTRUCTION "Please enter your authentication token" +#endif +#ifdef HAVE_SHADOW +# include +#endif + +#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. + * - If the PAM thread does not cooperate with a graceful cancel (a PAM module + * blocked outside the conversation), nc_server_ssh_cb_pam_cancel() force-cancels + * it with pthread_cancel() and waits for it only for a bounded time; a thread + * that survives even that is detached and its resources intentionally leaked, + * so the main thread never blocks indefinitely. In that case the DONE + * transition never happens and @p state stays NC_PAM_CANCELLED. + */ +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). */ + + 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). + * + * Never blocks indefinitely: if the thread does not terminate on the graceful + * cancel, it is force-cancelled with pthread_cancel() and the join is bounded + * (when pthread_timedjoin_np() is available); a thread that survives even that + * is detached and its data is intentionally leaked instead. + * + * @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. + * @param[in] session NETCONF session for logging. + * @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, struct nc_session *session); + +/** + * @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); + +/** + * @brief Increase the failed authentication attempt counter and log the attempt. + * + * @param[in] session NETCONF session. + */ +void nc_server_ssh_auth_attempt_failed(struct nc_session *session); + +/** + * @brief Authenticate user with password (retrieves stored hash and compares). + * + * @param[in] session NETCONF session. + * @param[in] user Username attempting authentication. + * @param[in] password Password provided by the user. + * @param[in] auth_client Client configuration (if local users supported). + * @param[in] local_users_supported Flag indicating if local users are used. + * @return 0 on success (password matches), non-zero on failure. + */ +int nc_server_ssh_auth_password_check(struct nc_session *session, const char *user, + const char *password, struct nc_auth_client *auth_client, int local_users_supported); + +/** + * @brief Authenticate a user by verifying a public key against configured or system public keys. + * + * @param[in] session NETCONF session. + * @param[in] pubkey Received libssh public key. + * @param[in] auth_client Client configuration (if local users supported). + * @param[in] local_users_supported Flag indicating if local users are configured. + * @return 0 on success (key matches an authorized key), non-zero on failure/error. + */ +int nc_server_ssh_auth_pubkey_check(struct nc_session *session, ssh_key pubkey, + struct nc_auth_client *auth_client, int local_users_supported); + +/** + * @brief Keyboard-interactive authentication backend. + */ +enum nc_kbdint_backend { + NC_KBDINT_BACKEND_SYSTEM, /**< Authenticate via the system method (PAM or shadow). */ + NC_KBDINT_BACKEND_CUSTOM_CLB /**< Authenticate via the custom keyboard-interactive callback. */ +}; + +/** + * @brief Select the keyboard-interactive authentication backend based on the configuration. + * + * @param[in] session NETCONF session. + * @param[in] local_users_supported Whether local users are supported. + * @param[in] auth_client Configured client's authentication data (may be NULL for system users). + * @param[out] backend Selected authentication backend. + * @return 0 if a backend was selected, non-zero if the request must be denied (the reason was already logged). + */ +int nc_server_ssh_kbdint_select_method(struct nc_session *session, int local_users_supported, + struct nc_auth_client *auth_client, enum nc_kbdint_backend *backend); + +/** + * @brief Check a channel subsystem request against the session state. + * + * @param[in] session NETCONF session. + * @param[in] channel SSH channel the subsystem was requested on. + * @param[in] subsystem Requested subsystem name (expected "netconf"). + * @return 0 if the "netconf" subsystem was requested on the first channel (the flag was set). + * @return 1 if the request is for an additional channel (the caller must create a new session). + * @return -1 on an invalid request (the reason was logged). + */ +int nc_server_ssh_channel_subsys_check(struct nc_session *session, ssh_channel channel, const char *subsystem); + +/** + * @brief Create a new NETCONF session for an additional SSH channel and insert it into the ring of sessions. + * + * @param[in] session Parent (first channel) NETCONF session. + * @param[in] channel SSH channel of the additional subsystem request. + * @return New session on success. + * @return NULL on error (logged). + */ +struct nc_session *nc_server_ssh_new_channel_session(struct nc_session *session, ssh_channel channel); + +#ifdef HAVE_LIBPAM +/** + * @brief Parse PAM conversation messages into keyboard-interactive prompts. + * + * @param[in] session NETCONF session (used for logging). + * @param[in] n_messages Number of PAM messages. + * @param[in] msg PAM module's messages. + * @param[out] resp PAM response array (allocated here, freed by PAM on success). + * @param[out] n_prompts Number of actual prompts (0 if none). + * @param[out] prompts Prompt strings borrowed from @p msg. + * @param[out] echo Echo flags for the prompts. + * @return PAM_SUCCESS on success, PAM_CONV_ERR on bad input, PAM_BUF_ERR on OOM. + */ +int nc_server_ssh_pam_conv_parse(struct nc_session *session, int n_messages, + const struct pam_message **msg, struct pam_response **resp, + int *n_prompts, const char ***prompts, char **echo); + +/** + * @brief Fill a prepared PAM response array with the client's answers. + * + * @param[in] session NETCONF session (used for logging). + * @param[in] resp PAM response array allocated for @p n_prompts responses. + * @param[in] n_prompts Number of prompts given to the client. + * @param[in] n_answers Number of answers received from the client. + * @param[in] answers Answer strings of the client. + * @return PAM_SUCCESS on success, PAM_CONV_ERR if the answer count does not + * match the prompt count, PAM_BUF_ERR on OOM. + */ +int nc_server_ssh_pam_conv_fill(struct nc_session *session, struct pam_response *resp, + int n_prompts, int n_answers, const char **answers); + +/** + * @brief Run the PAM authentication sequence with a prepared conversation. + * + * @param[in] session NETCONF session (used for logging). + * @param[in] username Username to authenticate. + * @param[in] conv PAM conversation prepared by the caller. + * @return PAM_SUCCESS (0) on success, a PAM error code or 1 otherwise. + */ +int nc_server_ssh_pam_authenticate(struct nc_session *session, const char *username, + const struct pam_conv *conv); +#endif /* HAVE_LIBPAM */ + +#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); + +/** + * @brief Send a single "'s password:" keyboard-interactive prompt to the client. + * + * @param[in] session NETCONF session (used for logging). + * @param[in] username Username shown in the prompt. + * @param[in] msg SSH message the interactive request is sent through. + * @return 0 on success, non-zero on failure. + */ +int nc_server_ssh_kbdint_send_passwd_prompt(struct nc_session *session, const char *username, ssh_message msg); + +/** + * @brief Verify the client's single keyboard-interactive password answer against the user's system hash. + * + * @param[in] session NETCONF session. + * @param[in] username Username whose system password hash is fetched. + * @param[in] n_answers Number of answers the client provided (must be exactly 1). + * @return 0 if the password matches, non-zero otherwise. + */ +int nc_server_ssh_kbdint_verify_passwd(struct nc_session *session, const char *username, int n_answers); +#endif /* HAVE_SHADOW */ + +#endif /* _SESSION_SERVER_SSH_WRAPPER_H_ */ diff --git a/src/session_server_tls.c b/src/session_server_tls.c index d3109f52..fad8235e 100644 --- a/src/session_server_tls.c +++ b/src/session_server_tls.c @@ -523,6 +523,7 @@ _nc_server_tls_cert_to_name(struct nc_server_tls_opts *opts, void *cert_chain, c /* do the same for referenced endpoint's ctn entries */ if (opts->referenced_endpt_name) { if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) { + ERR(NULL, "Referenced endpoint \"%s\" not found.", opts->referenced_endpt_name); ERRINT; rc = -1; goto cleanup; @@ -591,6 +592,7 @@ nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_opts *opts) if (opts->referenced_endpt_name) { if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) { + ERR(NULL, "Referenced endpoint \"%s\" not found.", opts->referenced_endpt_name); ERRINT; return -1; }