pam UPDATE auth using Linux PAM
Added the ability to authenticate via Linux PAM when using keyboard-interactive SSH authentication method. One new API call was added.
diff --git a/src/config.h.in b/src/config.h.in
index 340181e..2beb65f 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -39,6 +39,11 @@
#cmakedefine HAVE_CRYPT
/*
+* Support for older PAM versions
+*/
+#cmakedefine LIBPAM_HAVE_CONFDIR
+
+/*
* Location of installed basic YANG modules on the system
*/
#define NC_YANG_DIR "@YANG_MODULE_DIR@"
diff --git a/src/session.c b/src/session.c
index 98e40b2..443e0f1 100644
--- a/src/session.c
+++ b/src/session.c
@@ -100,16 +100,16 @@
int
nc_gettimespec_mono_add(struct timespec *ts, uint32_t msec)
{
- #ifdef CLOCK_MONOTONIC_RAW
+#ifdef CLOCK_MONOTONIC_RAW
if (clock_gettime(CLOCK_MONOTONIC_RAW, ts)) {
return -1;
}
- #elif defined (CLOCK_MONOTONIC)
+#elif defined (CLOCK_MONOTONIC)
if (clock_gettime(CLOCK_MONOTONIC, ts)) {
return -1;
}
- #else
- /* no monotonic clock available, return realtime */
+#else
+ /* no monotonic clock available, return real time */
if (nc_gettimespec_real_add(ts, 0)) {
return -1;
}
diff --git a/src/session_client.c b/src/session_client.c
index c29e564..5755a79 100644
--- a/src/session_client.c
+++ b/src/session_client.c
@@ -70,7 +70,7 @@
},
#ifdef NC_ENABLED_SSH
.ssh_opts = {
- .auth_pref = {{NC_SSH_AUTH_INTERACTIVE, 3}, {NC_SSH_AUTH_PASSWORD, 2}, {NC_SSH_AUTH_PUBLICKEY, 1}},
+ .auth_pref = {{NC_SSH_AUTH_INTERACTIVE, 1}, {NC_SSH_AUTH_PASSWORD, 2}, {NC_SSH_AUTH_PUBLICKEY, 3}},
.auth_hostkey_check = sshauth_hostkey_check,
.auth_password = sshauth_password,
.auth_interactive = sshauth_interactive,
@@ -163,17 +163,17 @@
e->refcount = 1;
#ifdef NC_ENABLED_SSH
e->ssh_opts.auth_pref[0].type = NC_SSH_AUTH_INTERACTIVE;
- e->ssh_opts.auth_pref[0].value = 3;
+ e->ssh_opts.auth_pref[0].value = 1;
e->ssh_opts.auth_pref[1].type = NC_SSH_AUTH_PASSWORD;
e->ssh_opts.auth_pref[1].value = 2;
e->ssh_opts.auth_pref[2].type = NC_SSH_AUTH_PUBLICKEY;
- e->ssh_opts.auth_pref[2].value = 1;
+ e->ssh_opts.auth_pref[2].value = 3;
e->ssh_opts.auth_hostkey_check = sshauth_hostkey_check;
e->ssh_opts.auth_password = sshauth_password;
e->ssh_opts.auth_interactive = sshauth_interactive;
e->ssh_opts.auth_privkey_passphrase = sshauth_privkey_passphrase;
- /* callhome settings are the same except the inverted auth methods preferences */
+ /* callhome settings are the same */
memcpy(&e->ssh_ch_opts, &e->ssh_opts, sizeof e->ssh_ch_opts);
e->ssh_ch_opts.auth_pref[0].value = 1;
e->ssh_ch_opts.auth_pref[1].value = 2;
diff --git a/src/session_p.h b/src/session_p.h
index 916e235..b1224ef 100644
--- a/src/session_p.h
+++ b/src/session_p.h
@@ -195,6 +195,8 @@
int (*interactive_auth_clb)(const struct nc_session *session, ssh_message msg, void *user_data);
void *interactive_auth_data;
void (*interactive_auth_data_free)(void *data);
+ char *conf_name;
+ char *conf_dir;
#endif
#ifdef NC_ENABLED_TLS
int (*user_verify_clb)(const struct nc_session *session);
@@ -514,6 +516,18 @@
void (*notif_clb)(struct nc_session *session, const struct lyd_node *envp, const struct lyd_node *op);
};
+#ifdef NC_ENABLED_SSH
+
+/**
+ * @brief PAM callback arguments.
+ */
+struct nc_pam_thread_arg {
+ ssh_message msg; /**< libssh message */
+ struct nc_session *session; /**< NETCONF session */
+};
+
+#endif
+
void *nc_realloc(void *ptr, size_t size);
struct passwd *nc_getpwuid(uid_t uid, struct passwd *pwd_buf, char **buf, size_t *buf_size);
diff --git a/src/session_server.c b/src/session_server.c
index 546050c..5ce9fa3 100644
--- a/src/session_server.c
+++ b/src/session_server.c
@@ -776,6 +776,12 @@
}
server_opts.hostkey_data = NULL;
server_opts.hostkey_data_free = NULL;
+
+ /* PAM */
+ free(server_opts.conf_name);
+ free(server_opts.conf_dir);
+ server_opts.conf_name = NULL;
+ server_opts.conf_dir = NULL;
#endif
#ifdef NC_ENABLED_TLS
if (server_opts.server_cert_data && server_opts.server_cert_data_free) {
@@ -1995,7 +2001,7 @@
goto cleanup;
}
server_opts.endpts[server_opts.endpt_count - 1].opts.ssh->auth_methods =
- NC_SSH_AUTH_PUBLICKEY | NC_SSH_AUTH_PASSWORD | NC_SSH_AUTH_INTERACTIVE;
+ NC_SSH_AUTH_PUBLICKEY | NC_SSH_AUTH_PASSWORD;
server_opts.endpts[server_opts.endpt_count - 1].opts.ssh->auth_attempts = 3;
server_opts.endpts[server_opts.endpt_count - 1].opts.ssh->auth_timeout = 30;
break;
@@ -2817,7 +2823,7 @@
ERRMEM;
goto cleanup;
}
- endpt->opts.ssh->auth_methods = NC_SSH_AUTH_PUBLICKEY | NC_SSH_AUTH_PASSWORD | NC_SSH_AUTH_INTERACTIVE;
+ endpt->opts.ssh->auth_methods = NC_SSH_AUTH_PUBLICKEY | NC_SSH_AUTH_PASSWORD;
endpt->opts.ssh->auth_attempts = 3;
endpt->opts.ssh->auth_timeout = 30;
break;
diff --git a/src/session_server.h b/src/session_server.h
index c4228c5..cffcec9 100644
--- a/src/session_server.h
+++ b/src/session_server.h
@@ -580,18 +580,32 @@
*
* @param[in] interactive_auth_clb Callback that should authenticate the user.
* Zero return indicates success, non-zero an error.
- * @param[in] user_data Optional arbitrary user data that will be passed to @p passwd_auth_clb.
+ * @param[in] user_data Optional arbitrary user data that will be passed to @p interactive_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(int (*interactive_auth_clb)(const struct nc_session *session,
const ssh_message msg, void *user_data), void *user_data, void (*free_user_data)(void *user_data));
/**
+ * @brief Set the name and a path to a PAM configuration file.
+ *
+ * @p conf_name has to be set via this function prior to using keyboard-interactive authentication method.
+ *
+ * @param[in] conf_name Name of the configuration file.
+ * @param[in] conf_dir Optional. The absolute path to the directory in which the configuration file
+ * with the name @p conf_name is located. A newer version (>= 1.4) of PAM library is required to be
+ * able to specify the path. If NULL is passed,
+ * then the PAM's system directories will be searched (usually /etc/pam.d/).
+ * @return 0 on success, -1 on error.
+ */
+int nc_server_ssh_set_pam_conf_path(const char *conf_name, const char *conf_dir);
+
+/**
* @brief Set the callback for SSH public key authentication. If none is set, local system users are used.
*
* @param[in] pubkey_auth_clb Callback that should authenticate the user.
* Zero return indicates success, non-zero an error.
- * @param[in] user_data Optional arbitrary user data that will be passed to @p passwd_auth_clb.
+ * @param[in] user_data Optional arbitrary user data that will be passed to @p pubkey_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_pubkey_auth_clb(int (*pubkey_auth_clb)(const struct nc_session *session, ssh_key key,
@@ -642,7 +656,7 @@
* @param[in] endpt_name Exisitng endpoint name.
* @param[in] key_mov Name of the host key that will be moved.
* @param[in] key_after Name of the key that will preceed @p key_mov. NULL if @p key_mov is to be moved at the beginning.
- * @return 0 in success, -1 on error.
+ * @return 0 on success, -1 on error.
*/
int nc_server_ssh_endpt_mov_hostkey(const char *endpt_name, const char *key_mov, const char *key_after);
@@ -652,7 +666,7 @@
* @param[in] endpt_name Exisitng endpoint name.
* @param[in] name Name of an existing host key.
* @param[in] new_name New name of the host key @p name.
- * @return 0 in success, -1 on error.
+ * @return 0 on success, -1 on error.
*/
int nc_server_ssh_endpt_mod_hostkey(const char *endpt_name, const char *name, const char *new_name);
diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c
index 6eeafd5..b9e567b 100644
--- a/src/session_server_ssh.c
+++ b/src/session_server_ssh.c
@@ -27,6 +27,7 @@
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
+#include <security/pam_appl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
@@ -176,6 +177,39 @@
server_opts.interactive_auth_data_free = free_user_data;
}
+API int
+nc_server_ssh_set_pam_conf_path(const char *conf_name, const char *conf_dir)
+{
+ free(server_opts.conf_name);
+ free(server_opts.conf_dir);
+ server_opts.conf_name = NULL;
+ server_opts.conf_dir = NULL;
+
+ if (conf_dir) {
+#ifdef LIBPAM_HAVE_CONFDIR
+ server_opts.conf_dir = strdup(conf_dir);
+ if (!(server_opts.conf_dir)) {
+ ERRMEM;
+ return -1;
+ }
+#else
+ ERR(NULL, "Failed to set PAM config directory because of old version of PAM. "
+ "Put the config file in the system directory (usually /etc/pam.d/).");
+ return -1;
+#endif
+ }
+
+ if (conf_name) {
+ server_opts.conf_name = strdup(conf_name);
+ if (!(server_opts.conf_name)) {
+ ERRMEM;
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
API void
nc_server_ssh_set_pubkey_auth_clb(int (*pubkey_auth_clb)(const struct nc_session *session, ssh_key key, void *user_data),
void *user_data, void (*free_user_data)(void *user_data))
@@ -403,6 +437,11 @@
static int
nc_server_ssh_set_auth_methods(int auth_methods, struct nc_server_ssh_opts *opts)
{
+ if ((auth_methods & NC_SSH_AUTH_INTERACTIVE) && !server_opts.conf_name) {
+ /* path to a configuration file not set */
+ ERR(NULL, "Unable to use Keyboard-Interactive authentication method without setting the name of the PAM configuration file first.");
+ return 1;
+ }
opts->auth_methods = auth_methods;
return 0;
}
@@ -907,33 +946,250 @@
}
}
+/**
+ * @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;
+ * @return PAM_BUF_ERR on memory allocation error;
+ * @return PAM_CONV_ERR otherwise.
+ */
+static int
+nc_pam_conv_clb(int n_messages, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
+{
+ int i, j, t, r = PAM_SUCCESS, n_answers, n_requests = n_messages;
+ const char **prompts = NULL;
+ char *echo = NULL;
+ const char *name = "Keyboard-Interactive Authentication";
+ const char *instruction = "Please enter your authentication token";
+ ssh_message reply = NULL;
+ struct nc_pam_thread_arg *clb_data = appdata_ptr;
+ ssh_session libssh_session;
+ struct timespec ts_timeout;
+ struct nc_server_ssh_opts *opts;
+
+ libssh_session = clb_data->session->ti.libssh.session;
+ opts = clb_data->session->data;
+
+ /* PAM_MAX_NUM_MSG == 32 by default */
+ if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) {
+ ERR(NULL, "Bad number of PAM messages (#%d).", n_messages);
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+
+ /* only accepting these 4 types of messages */
+ for (i = 0; i < n_messages; i++) {
+ t = msg[i]->msg_style;
+ if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) {
+ ERR(NULL, "PAM conversation callback received an unexpected type of message.");
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+ }
+
+ /* display messages with errors and/or some information and count the amount of actual authentication challenges */
+ for (i = 0; i < n_messages; i++) {
+ if (msg[i]->msg_style == PAM_TEXT_INFO) {
+ VRB(NULL, "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(NULL, "PAM conversation callback received an error message (%s).", msg[i]->msg);
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+ }
+
+ /* there are no requests left for the user, only messages with some information for the client were sent */
+ if (n_requests <= 0) {
+ r = PAM_SUCCESS;
+ goto cleanup;
+ }
+
+ /* it is the PAM module's responsibility to release both, this array and the responses themselves */
+ *resp = calloc(n_requests, sizeof **resp);
+ prompts = calloc(n_requests, sizeof *prompts);
+ echo = calloc(n_requests, sizeof *echo);
+ if (!(*resp) || !prompts || !echo) {
+ ERRMEM;
+ r = PAM_BUF_ERR;
+ goto cleanup;
+ }
+
+ /* set the prompts for the user */
+ j = 0;
+ for (i = 0; i < n_messages; i++) {
+ if ((msg[i]->msg_style == PAM_PROMPT_ECHO_ON) || (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF)) {
+ prompts[j++] = msg[i]->msg;
+ }
+ }
+
+ /* iterate over all the messages and adjust the echo array accordingly */
+ j = 0;
+ for (i = 0; i < n_messages; i++) {
+ if (msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
+ echo[j++] = 1;
+ }
+ if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) {
+ /* no need to set to 0 because of calloc */
+ j++;
+ }
+ }
+
+ /* print all the keyboard-interactive challenges to the user */
+ r = ssh_message_auth_interactive_request(clb_data->msg, name, instruction, n_requests, prompts, echo);
+ if (r != SSH_OK) {
+ ERR(NULL, "Failed to send an authentication request.");
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+
+ if (opts->auth_timeout) {
+ nc_gettimespec_mono_add(&ts_timeout, opts->auth_timeout * 1000);
+ }
+
+ /* get user's replies */
+ do {
+ if (!nc_session_is_connected(clb_data->session)) {
+ ERR(NULL, "Communication SSH socket unexpectedly closed.");
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+
+ reply = ssh_message_get(libssh_session);
+ if (reply) {
+ break;
+ }
+
+ usleep(NC_TIMEOUT_STEP);
+ } while ((opts->auth_timeout) && (nc_difftimespec_cur(&ts_timeout) >= 1));
+
+ if (!reply) {
+ ERR(NULL, "Authentication timeout.");
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+
+ /* check if the amount of replies matches the amount of requests */
+ n_answers = ssh_userauth_kbdint_getnanswers(libssh_session);
+ if (n_answers != n_requests) {
+ ERR(NULL, "Expected %d response(s), got %d.", n_requests, n_answers);
+ r = PAM_CONV_ERR;
+ goto cleanup;
+ }
+
+ /* give the replies to a PAM module */
+ for (i = 0; i < n_answers; i++) {
+ (*resp)[i].resp = strdup(ssh_userauth_kbdint_getanswer(libssh_session, i));
+ /* it should be the caller's responsibility to free this, however if mem alloc fails,
+ * it is safer to free the responses here and set them to NULL */
+ if ((*resp)[i].resp == NULL) {
+ for (j = 0; j < i; j++) {
+ free((*resp)[j].resp);
+ (*resp)[j].resp = NULL;
+ }
+ ERRMEM;
+ r = PAM_BUF_ERR;
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ ssh_message_free(reply);
+ free(prompts);
+ free(echo);
+ return r;
+}
+
+/**
+ * @brief Handles authentication via Linux PAM.
+ *
+ * @param[in] session NETCONF session.
+ * @param[in] ssh_msg SSH message with a keyboard-interactive authentication request.
+ * @return PAM_SUCCESS on success;
+ * @return PAM error otherwise.
+ */
+static int
+nc_pam_auth(struct nc_session *session, ssh_message ssh_msg)
+{
+ pam_handle_t *pam_h = NULL;
+ int ret;
+ struct nc_pam_thread_arg clb_data;
+ struct pam_conv conv;
+
+ /* structure holding callback's data */
+ clb_data.msg = ssh_msg;
+ clb_data.session = session;
+
+ /* PAM conversation structure holding the callback and it's data */
+ conv.conv = nc_pam_conv_clb;
+ conv.appdata_ptr = &clb_data;
+
+ /* initialize PAM and see if the given configuration file exists */
+#ifdef LIBPAM_HAVE_CONFDIR
+ /* PAM version >= 1.4 */
+ ret = pam_start_confdir(server_opts.conf_name, session->username, &conv, server_opts.conf_dir, &pam_h);
+#else
+ /* PAM version < 1.4 */
+ ret = pam_start(server_opts.conf_name, session->username, &conv, &pam_h);
+#endif
+ if (ret != PAM_SUCCESS) {
+ ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+ goto cleanup;
+ }
+
+ /* authentication based on the modules listed in the configuration file */
+ ret = pam_authenticate(pam_h, 0);
+ if (ret != PAM_SUCCESS) {
+ if (ret == PAM_ABORT) {
+ ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+ goto cleanup;
+ } else {
+ VRB(NULL, "PAM error occurred (%s).\n", 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(NULL, "PAM error occurred (%s).\n", 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(NULL, "PAM warning occurred (%s).\n", pam_strerror(pam_h, ret));
+ ret = pam_chauthtok(pam_h, PAM_CHANGE_EXPIRED_AUTHTOK);
+ if (ret == PAM_SUCCESS) {
+ VRB(NULL, "The authentication token of user \"%s\" updated successfully.", session->username);
+ } else {
+ ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ /* destroy the PAM context */
+ if (pam_end(pam_h, ret) != PAM_SUCCESS) {
+ ERR(NULL, "PAM error occurred (%s).\n", pam_strerror(pam_h, ret));
+ }
+ return ret;
+}
+
static void
nc_sshcb_auth_kbdint(struct nc_session *session, ssh_message msg)
{
int auth_ret = 1;
- char *pass_hash;
if (server_opts.interactive_auth_clb) {
auth_ret = server_opts.interactive_auth_clb(session, msg, server_opts.interactive_auth_data);
- } else {
- if (!ssh_message_auth_kbdint_is_response(msg)) {
- const char *prompts[] = {"Password: "};
- char echo[] = {0};
-
- ssh_message_auth_interactive_request(msg, "Interactive SSH Authentication", "Type your password:", 1, prompts, echo);
- auth_ret = -1;
- } else {
- if (ssh_userauth_kbdint_getnanswers(session->ti.libssh.session) != 1) {// failed session
- ssh_message_reply_default(msg);
- return;
- }
- pass_hash = auth_password_get_pwd_hash(session->username);// get hashed password
- if (pass_hash) {
- /* Normalize auth_password_compare_pwd result to 0 or 1 */
- auth_ret = !!auth_password_compare_pwd(pass_hash, ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0));
- free(pass_hash);// free hashed password
- }
- }
+ } else if (nc_pam_auth(session, msg) == PAM_SUCCESS) {
+ auth_ret = 0;
}
/* We have already sent a reply */
@@ -1451,6 +1707,7 @@
struct nc_server_ssh_opts *opts;
int libssh_auth_methods = 0, ret;
struct timespec ts_timeout;
+ ssh_message msg;
opts = session->data;
@@ -1487,7 +1744,6 @@
return -1;
}
- ssh_set_message_callback(session->ti.libssh.session, nc_sshcb_msg, session);
/* remember that this session was just set as nc_sshcb_msg() parameter */
session->flags |= NC_SESSION_SSH_MSG_CB;
@@ -1529,10 +1785,12 @@
return -1;
}
- if (ssh_execute_message_callbacks(session->ti.libssh.session) != SSH_OK) {
- ERR(session, "Failed to receive SSH messages on a session (%s).",
- ssh_get_error(session->ti.libssh.session));
- return -1;
+ msg = ssh_message_get(session->ti.libssh.session);
+ if (msg) {
+ if (nc_sshcb_msg(session->ti.libssh.session, msg, (void *) session)) {
+ ssh_message_reply_default(msg);
+ }
+ ssh_message_free(msg);
}
if (session->flags & NC_SESSION_SSH_AUTHENTICATED) {
@@ -1561,6 +1819,9 @@
return 0;
}
+ /* set the message callback after a successful authentication */
+ ssh_set_message_callback(session->ti.libssh.session, nc_sshcb_msg, session);
+
/* open channel */
ret = nc_open_netconf_channel(session, timeout);
if (ret < 1) {