diff --git a/src/common-ssh/common-ssh/key.h b/src/common-ssh/common-ssh/key.h index 1754deae..897555a3 100644 --- a/src/common-ssh/common-ssh/key.h +++ b/src/common-ssh/common-ssh/key.h @@ -22,6 +22,9 @@ #include "config.h" +#include +#include + #include /** @@ -166,5 +169,52 @@ void guac_common_ssh_key_free(guac_common_ssh_key* key); int guac_common_ssh_key_sign(guac_common_ssh_key* key, const char* data, int length, unsigned char* sig); +/** + * Verifies the host key for the given hostname/port combination against + * one or more known_hosts entries. The known_host entries can either be a + * single host_key, provided by the client, or a set of known_hosts entries + * provided in the /etc/guacamole/ssh_known_hosts file. Failure to correctly + * load the known_hosts entries will result in a connection abort and a returned + * error code. A return code of zero indiciates that either no known_hosts entries + * were provided, or that the verification succeeded (match). Negative values + * indicate internal libssh2 error codes; positive values indicate a failure + * during verification of the host key against the known hosts. + * + * @param session + * A pointer to the LIBSSH2_SESSION structure of the SSH connection already + * in progress. + * + * @param client + * The current guac_client instance for which the known_hosts checking is + * being performed. + * + * @param host_key + * The known host entry provided by the client. If this is non-null and not + * empty, it will be the only host key loaded and used for verification. If + * this is null or empty an attempt will be made to read the + * /etc/guacamole/ssh_known_hosts file and load entries from it. + * + * @param hostname + * The hostname or IP of the server that is being verified. + * + * @param port + * The port number of the server being verified. + * + * @param remote_hostkey + * The host key of the remote system being verified. + * + * @param remote_hostkey_len + * The length of the remote host key being verified + * + * @return + * The status of the known_hosts check. This will be zero if no entries + * are provided or if the match succeeds, negative to indicate internal + * libssh2 errors, or positive to indicate failures during host key + * checking. + */ +int guac_common_ssh_verify_host_key(LIBSSH2_SESSION* session, guac_client* client, + const char* host_key, const char* hostname, int port, const char* remote_hostkey, + const size_t remote_hostkey_len); + #endif diff --git a/src/common-ssh/common-ssh/ssh.h b/src/common-ssh/common-ssh/ssh.h index 8026549d..672e7767 100644 --- a/src/common-ssh/common-ssh/ssh.h +++ b/src/common-ssh/common-ssh/ssh.h @@ -98,7 +98,8 @@ void guac_common_ssh_uninit(); * if the connection or authentication were not successful. */ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, - const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive); + const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive, + const char* host_key); /** * Disconnects and destroys the given SSH session, freeing all associated diff --git a/src/common-ssh/key.c b/src/common-ssh/key.c index a05d696d..f835e4cc 100644 --- a/src/common-ssh/key.c +++ b/src/common-ssh/key.c @@ -35,6 +35,7 @@ #include #include +#include guac_common_ssh_key* guac_common_ssh_key_alloc(char* data, int length, char* passphrase) { @@ -245,3 +246,86 @@ int guac_common_ssh_key_sign(guac_common_ssh_key* key, const char* data, } +int guac_common_ssh_verify_host_key(LIBSSH2_SESSION* session, guac_client* client, + const char* host_key, const char* hostname, int port, const char* remote_hostkey, + const size_t remote_hostkey_len) { + + LIBSSH2_KNOWNHOSTS* ssh_known_hosts = libssh2_knownhost_init(session); + int known_hosts = 0; + + /* Add host key provided from settings */ + if (host_key && strcmp(host_key, "") != 0) { + + known_hosts = libssh2_knownhost_readline(ssh_known_hosts, host_key, strlen(host_key), + LIBSSH2_KNOWNHOST_FILE_OPENSSH); + + /* readline function returns 0 on success, so we increment to indicate a valid entry */ + if (known_hosts == 0) + known_hosts++; + + } + + /* Otherwise, we look for a ssh_known_hosts file within GUACAMOLE_HOME and read that in. */ + else { + + const char *guac_known_hosts = "/etc/guacamole/ssh_known_hosts"; + if (access(guac_known_hosts, F_OK) != -1) + known_hosts = libssh2_knownhost_readfile(ssh_known_hosts, guac_known_hosts, LIBSSH2_KNOWNHOST_FILE_OPENSSH); + + } + + /* If there's an error provided, abort connection and return that. */ + if (known_hosts < 0) { + + char* errmsg; + int errval = libssh2_session_last_error(session, &errmsg, NULL, 0); + guac_client_log(client, GUAC_LOG_ERROR, + "Error %d trying to load SSH host keys: %s", errval, errmsg); + + libssh2_knownhost_free(ssh_known_hosts); + return known_hosts; + + } + + /* No host keys were loaded, so we bail out checking and continue the connection. */ + else if (known_hosts == 0) { + guac_client_log(client, GUAC_LOG_WARNING, + "No known host keys provided, host identity will not be verified."); + libssh2_knownhost_free(ssh_known_hosts); + return known_hosts; + } + + + /* Check remote host key against known hosts */ + int kh_check = libssh2_knownhost_checkp(ssh_known_hosts, hostname, port, + remote_hostkey, remote_hostkey_len, + LIBSSH2_KNOWNHOST_TYPE_PLAIN| + LIBSSH2_KNOWNHOST_KEYENC_RAW, + NULL); + + /* Deal with the return of the host key check */ + switch (kh_check) { + case LIBSSH2_KNOWNHOST_CHECK_MATCH: + guac_client_log(client, GUAC_LOG_DEBUG, + "Host key match found for %s", hostname); + break; + case LIBSSH2_KNOWNHOST_CHECK_NOTFOUND: + guac_client_log(client, GUAC_LOG_ERROR, + "Host key not found for %s.", hostname); + break; + case LIBSSH2_KNOWNHOST_CHECK_MISMATCH: + guac_client_log(client, GUAC_LOG_ERROR, + "Host key does not match known hosts entry for %s", hostname); + break; + case LIBSSH2_KNOWNHOST_CHECK_FAILURE: + default: + guac_client_log(client, GUAC_LOG_ERROR, + "Host %s could not be checked against known hosts.", + hostname); + } + + /* Return the check value */ + libssh2_knownhost_free(ssh_known_hosts); + return kh_check; + +} diff --git a/src/common-ssh/ssh.c b/src/common-ssh/ssh.c index 5ea6feab..9dde5111 100644 --- a/src/common-ssh/ssh.c +++ b/src/common-ssh/ssh.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -414,7 +415,8 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) } guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, - const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive) { + const char* hostname, const char* port, guac_common_ssh_user* user, int keepalive, + const char* host_key) { int retval; @@ -518,6 +520,42 @@ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, return NULL; } + /* Get host key of remote system we're connecting to */ + size_t remote_hostkey_len; + const char *remote_hostkey = libssh2_session_hostkey(session, &remote_hostkey_len, NULL); + + /* Failure to retrieve a host key means we should abort */ + if (!remote_hostkey) { + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Failed to get host key for %s", hostname); + free(common_session); + close(fd); + return NULL; + } + + /* SSH known host key checking. */ + int known_host_check = guac_common_ssh_verify_host_key(session, client, host_key, + hostname, atoi(port), remote_hostkey, + remote_hostkey_len); + + /* Abort on any error codes */ + if (known_host_check != 0) { + char* err_msg; + libssh2_session_last_error(session, &err_msg, NULL, 0); + + if (known_host_check < 0) + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Error occurred attempting to check host key: %s", err_msg); + + if (known_host_check > 0) + guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, + "Host key did not match any provided known host keys. %s", err_msg); + + free(common_session); + close(fd); + return NULL; + } + /* Store basic session data */ common_session->client = client; common_session->user = user; @@ -560,4 +598,3 @@ void guac_common_ssh_destroy_session(guac_common_ssh_session* session) { free(session); } - diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 45082bb0..4d484320 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -974,7 +974,8 @@ void* guac_rdp_client_thread(void* data) { /* Attempt SSH connection */ rdp_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, - settings->sftp_port, rdp_client->sftp_user, settings->sftp_server_alive_interval); + settings->sftp_port, rdp_client->sftp_user, settings->sftp_server_alive_interval, + settings->sftp_host_key); /* Fail if SSH connection does not succeed */ if (rdp_client->sftp_session == NULL) { diff --git a/src/protocols/rdp/rdp_settings.c b/src/protocols/rdp/rdp_settings.c index 35f7f202..e1358303 100644 --- a/src/protocols/rdp/rdp_settings.c +++ b/src/protocols/rdp/rdp_settings.c @@ -81,6 +81,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { #ifdef ENABLE_COMMON_SSH "enable-sftp", "sftp-hostname", + "sftp-host-key", "sftp-port", "sftp-username", "sftp-password", @@ -355,6 +356,11 @@ enum RDP_ARGS_IDX { */ IDX_SFTP_HOSTNAME, + /** + * The public SSH host key of the SFTP server. Optional. + */ + IDX_SFTP_HOST_KEY, + /** * The port of the SSH server to connect to for SFTP. If blank, the default * SSH port of "22" will be used. @@ -822,6 +828,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_SFTP_HOSTNAME, settings->hostname); + /* The public SSH host key. */ + settings->sftp_host_key = + guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_SFTP_HOST_KEY, NULL); + /* Port for SFTP connection */ settings->sftp_port = guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -999,6 +1010,7 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { /* Free SFTP settings */ free(settings->sftp_directory); free(settings->sftp_root_directory); + free(settings->sftp_host_key); free(settings->sftp_hostname); free(settings->sftp_passphrase); free(settings->sftp_password); diff --git a/src/protocols/rdp/rdp_settings.h b/src/protocols/rdp/rdp_settings.h index ad71ca06..4f3839e6 100644 --- a/src/protocols/rdp/rdp_settings.h +++ b/src/protocols/rdp/rdp_settings.h @@ -342,6 +342,11 @@ typedef struct guac_rdp_settings { */ char* sftp_hostname; + /** + * The public SSH host key. + */ + char* sftp_host_key; + /** * The port of the SSH server to connect to for SFTP. */ diff --git a/src/protocols/ssh/settings.c b/src/protocols/ssh/settings.c index 2ce97903..7cfe404f 100644 --- a/src/protocols/ssh/settings.c +++ b/src/protocols/ssh/settings.c @@ -31,6 +31,7 @@ /* Client plugin arguments */ const char* GUAC_SSH_CLIENT_ARGS[] = { "hostname", + "host-key", "port", "username", "password", @@ -68,6 +69,11 @@ enum SSH_ARGS_IDX { */ IDX_HOSTNAME, + /** + * The Base64-encoded public SSH host key. Optional. + */ + IDX_HOST_KEY, + /** * The port to connect to. Optional. */ @@ -247,6 +253,10 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_HOSTNAME, ""); + settings->host_key = + guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_HOST_KEY, NULL); + settings->username = guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_USERNAME, NULL); @@ -384,6 +394,7 @@ void guac_ssh_settings_free(guac_ssh_settings* settings) { /* Free network connection information */ free(settings->hostname); + free(settings->host_key); free(settings->port); /* Free credentials */ @@ -417,4 +428,3 @@ void guac_ssh_settings_free(guac_ssh_settings* settings) { free(settings); } - diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h index 393cfc0c..761239c7 100644 --- a/src/protocols/ssh/settings.h +++ b/src/protocols/ssh/settings.h @@ -70,6 +70,11 @@ typedef struct guac_ssh_settings { */ char* hostname; + /** + * The public SSH host key. + */ + char* host_key; + /** * The port of the SSH server to connect to. */ diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index a614f813..f98f2228 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -235,7 +235,8 @@ void* ssh_client_thread(void* data) { /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, - settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval); + settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, + settings->host_key); if (ssh_client->session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; @@ -275,7 +276,8 @@ void* ssh_client_thread(void* data) { guac_client_log(client, GUAC_LOG_DEBUG, "Reconnecting for SFTP..."); ssh_client->sftp_session = guac_common_ssh_create_session(client, settings->hostname, - settings->port, ssh_client->user, settings->server_alive_interval); + settings->port, ssh_client->user, settings->server_alive_interval, + settings->host_key); if (ssh_client->sftp_session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; diff --git a/src/protocols/vnc/settings.c b/src/protocols/vnc/settings.c index 509921a5..587864a4 100644 --- a/src/protocols/vnc/settings.c +++ b/src/protocols/vnc/settings.c @@ -60,6 +60,7 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { #ifdef ENABLE_COMMON_SSH "enable-sftp", "sftp-hostname", + "sftp-host-key", "sftp-port", "sftp-username", "sftp-password", @@ -208,6 +209,11 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_USERNAME, + /** + * The public SSH host key to identify the SFTP server. + */ + IDX_SFTP_HOST_KEY, + /** * The password to provide when authenticating with the SSH server for * SFTP (if not using a private key). @@ -411,6 +417,11 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_SFTP_HOSTNAME, settings->hostname); + /* The public SSH host key. */ + settings->sftp_host_key = + guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_SFTP_HOST_KEY, NULL); + /* Port for SFTP connection */ settings->sftp_port = guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -504,6 +515,7 @@ void guac_vnc_settings_free(guac_vnc_settings* settings) { /* Free SFTP settings */ free(settings->sftp_directory); free(settings->sftp_root_directory); + free(settings->sftp_host_key); free(settings->sftp_hostname); free(settings->sftp_passphrase); free(settings->sftp_password); diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h index 85e6478c..3e2ebd5e 100644 --- a/src/protocols/vnc/settings.h +++ b/src/protocols/vnc/settings.h @@ -138,6 +138,11 @@ typedef struct guac_vnc_settings { */ char* sftp_hostname; + /** + * The public SSH host key. + */ + char* sftp_host_key; + /** * The port of the SSH server to connect to for SFTP. */ diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index db1e2180..d9f9dbbb 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -261,7 +261,8 @@ void* guac_vnc_client_thread(void* data) { /* Attempt SSH connection */ vnc_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, - settings->sftp_port, vnc_client->sftp_user, settings->sftp_server_alive_interval); + settings->sftp_port, vnc_client->sftp_user, settings->sftp_server_alive_interval, + settings->sftp_host_key); /* Fail if SSH connection does not succeed */ if (vnc_client->sftp_session == NULL) {