613 lines
19 KiB
C
613 lines
19 KiB
C
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
#include "common-ssh/key.h"
|
|
#include "common-ssh/ssh.h"
|
|
#include "common-ssh/user.h"
|
|
|
|
#include <guacamole/client.h>
|
|
#include <libssh2.h>
|
|
|
|
#ifdef LIBSSH2_USES_GCRYPT
|
|
#include <gcrypt.h>
|
|
#endif
|
|
|
|
#include <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
|
|
#include <errno.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <pthread.h>
|
|
#include <pwd.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef LIBSSH2_USES_GCRYPT
|
|
GCRY_THREAD_OPTION_PTHREAD_IMPL;
|
|
#endif
|
|
|
|
#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
|
|
/**
|
|
* Array of mutexes, used by OpenSSL.
|
|
*/
|
|
static pthread_mutex_t* guac_common_ssh_openssl_locks = NULL;
|
|
|
|
/**
|
|
* Called by OpenSSL when locking or unlocking the Nth mutex.
|
|
*
|
|
* @param mode
|
|
* A bitmask denoting the action to be taken on the Nth lock, such as
|
|
* CRYPTO_LOCK or CRYPTO_UNLOCK.
|
|
*
|
|
* @param n
|
|
* The index of the lock to lock or unlock.
|
|
*
|
|
* @param file
|
|
* The filename of the function setting the lock, for debugging purposes.
|
|
*
|
|
* @param line
|
|
* The line number of the function setting the lock, for debugging
|
|
* purposes.
|
|
*/
|
|
static void guac_common_ssh_openssl_locking_callback(int mode, int n,
|
|
const char* file, int line){
|
|
|
|
/* Lock given mutex upon request */
|
|
if (mode & CRYPTO_LOCK)
|
|
pthread_mutex_lock(&(guac_common_ssh_openssl_locks[n]));
|
|
|
|
/* Unlock given mutex upon request */
|
|
else if (mode & CRYPTO_UNLOCK)
|
|
pthread_mutex_unlock(&(guac_common_ssh_openssl_locks[n]));
|
|
|
|
}
|
|
|
|
/**
|
|
* Called by OpenSSL when determining the current thread ID.
|
|
*
|
|
* @return
|
|
* An ID which uniquely identifies the current thread.
|
|
*/
|
|
static unsigned long guac_common_ssh_openssl_id_callback() {
|
|
return (unsigned long) pthread_self();
|
|
}
|
|
|
|
/**
|
|
* Creates the given number of mutexes, such that OpenSSL will have at least
|
|
* this number of mutexes at its disposal.
|
|
*
|
|
* @param count
|
|
* The number of mutexes (locks) to create.
|
|
*/
|
|
static void guac_common_ssh_openssl_init_locks(int count) {
|
|
|
|
int i;
|
|
|
|
/* Allocate required number of locks */
|
|
guac_common_ssh_openssl_locks =
|
|
malloc(sizeof(pthread_mutex_t) * count);
|
|
|
|
/* Initialize each lock */
|
|
for (i=0; i < count; i++)
|
|
pthread_mutex_init(&(guac_common_ssh_openssl_locks[i]), NULL);
|
|
|
|
}
|
|
|
|
/**
|
|
* Frees the given number of mutexes.
|
|
*
|
|
* @param count
|
|
* The number of mutexes (locks) to free.
|
|
*/
|
|
static void guac_common_ssh_openssl_free_locks(int count) {
|
|
|
|
int i;
|
|
|
|
/* SSL lock array was not initialized */
|
|
if (guac_common_ssh_openssl_locks == NULL)
|
|
return;
|
|
|
|
/* Free all locks */
|
|
for (i=0; i < count; i++)
|
|
pthread_mutex_destroy(&(guac_common_ssh_openssl_locks[i]));
|
|
|
|
/* Free lock array */
|
|
free(guac_common_ssh_openssl_locks);
|
|
|
|
}
|
|
#endif
|
|
|
|
int guac_common_ssh_init(guac_client* client) {
|
|
|
|
#ifdef LIBSSH2_USES_GCRYPT
|
|
/* Init threadsafety in libgcrypt */
|
|
gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread);
|
|
if (!gcry_check_version(GCRYPT_VERSION)) {
|
|
guac_client_log(client, GUAC_LOG_ERROR, "libgcrypt version mismatch.");
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
|
|
/* Init threadsafety in OpenSSL */
|
|
guac_common_ssh_openssl_init_locks(CRYPTO_num_locks());
|
|
CRYPTO_set_id_callback(guac_common_ssh_openssl_id_callback);
|
|
CRYPTO_set_locking_callback(guac_common_ssh_openssl_locking_callback);
|
|
#endif
|
|
|
|
/* Init OpenSSL */
|
|
SSL_library_init();
|
|
ERR_load_crypto_strings();
|
|
|
|
/* Init libssh2 */
|
|
libssh2_init(0);
|
|
|
|
/* Success */
|
|
return 0;
|
|
|
|
}
|
|
|
|
void guac_common_ssh_uninit() {
|
|
#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
|
|
guac_common_ssh_openssl_free_locks(CRYPTO_num_locks());
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Callback invoked by libssh2 when libssh2_userauth_publickkey() is invoked.
|
|
* This callback must sign the given data, returning the signature as newly-
|
|
* allocated buffer space.
|
|
*
|
|
* @param session
|
|
* The SSH session for which the signature is being generated.
|
|
*
|
|
* @param sig
|
|
* A pointer to the buffer space containing the signature. This callback
|
|
* MUST allocate and assign this space.
|
|
*
|
|
* @param sig_len
|
|
* The length of the signature within the allocated buffer space, in bytes.
|
|
* This value must be set to the size of the signature after the signing
|
|
* operation completes.
|
|
*
|
|
* @param data
|
|
* The arbitrary data that must be signed.
|
|
*
|
|
* @param data_len
|
|
* The length of the arbitrary data to be signed, in bytes.
|
|
*
|
|
* @param abstract
|
|
* The value of the abstract parameter provided with the corresponding call
|
|
* to libssh2_userauth_publickey().
|
|
*
|
|
* @return
|
|
* Zero on success, non-zero if the signing operation failed.
|
|
*/
|
|
static int guac_common_ssh_sign_callback(LIBSSH2_SESSION* session,
|
|
unsigned char** sig, size_t* sig_len,
|
|
const unsigned char* data, size_t data_len, void **abstract) {
|
|
|
|
guac_common_ssh_key* key = (guac_common_ssh_key*) abstract;
|
|
int length;
|
|
|
|
/* Allocate space for signature */
|
|
*sig = malloc(4096);
|
|
|
|
/* Sign with key */
|
|
length = guac_common_ssh_key_sign(key, (const char*) data, data_len, *sig);
|
|
if (length < 0)
|
|
return 1;
|
|
|
|
*sig_len = length;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Callback for the keyboard-interactive authentication method. Currently
|
|
* supports just one prompt for the password. This callback is invoked as
|
|
* needed to fullfill a call to libssh2_userauth_keyboard_interactive().
|
|
*
|
|
* @param name
|
|
* An arbitrary name which should be printed to the terminal for the
|
|
* benefit of the user. This is currently ignored.
|
|
*
|
|
* @param name_len
|
|
* The length of the name string, in bytes.
|
|
*
|
|
* @param instruction
|
|
* Arbitrary instructions which should be printed to the terminal for the
|
|
* benefit of the user. This is currently ignored.
|
|
*
|
|
* @param instruction_len
|
|
* The length of the instruction string, in bytes.
|
|
*
|
|
* @param num_prompts
|
|
* The number of keyboard-interactive prompts for which responses are
|
|
* requested. This callback currently only supports one prompt, and assumes
|
|
* that this prompt is requesting the password.
|
|
*
|
|
* @param prompts
|
|
* An array of all keyboard-interactive prompts for which responses are
|
|
* requested.
|
|
*
|
|
* @param responses
|
|
* A parallel array into which all prompt responses should be stored. Each
|
|
* entry within this array corresponds to the entry in the prompts array
|
|
* with the same index.
|
|
*
|
|
* @param abstract
|
|
* The value of the abstract parameter provided when the SSH session was
|
|
* created with libssh2_session_init_ex().
|
|
*/
|
|
static void guac_common_ssh_kbd_callback(const char *name, int name_len,
|
|
const char *instruction, int instruction_len, int num_prompts,
|
|
const LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts,
|
|
LIBSSH2_USERAUTH_KBDINT_RESPONSE *responses,
|
|
void **abstract) {
|
|
|
|
guac_common_ssh_session* common_session =
|
|
(guac_common_ssh_session*) *abstract;
|
|
|
|
guac_client* client = common_session->client;
|
|
|
|
/* Send password if only one prompt */
|
|
if (num_prompts == 1) {
|
|
char* password = common_session->user->password;
|
|
responses[0].text = strdup(password);
|
|
responses[0].length = strlen(password);
|
|
}
|
|
|
|
/* If more than one prompt, a single password is not enough */
|
|
else
|
|
guac_client_log(client, GUAC_LOG_WARNING,
|
|
"Unsupported number of keyboard-interactive prompts: %i",
|
|
num_prompts);
|
|
|
|
}
|
|
|
|
/**
|
|
* Authenticates the user associated with the given session over SSH. All
|
|
* required credentials must already be present within the user object
|
|
* associated with the given session.
|
|
*
|
|
* @param session
|
|
* The session associated with the user to be authenticated.
|
|
*
|
|
* @return
|
|
* Zero if authentication succeeds, or non-zero if authentication has
|
|
* failed.
|
|
*/
|
|
static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) {
|
|
|
|
guac_client* client = common_session->client;
|
|
guac_common_ssh_user* user = common_session->user;
|
|
LIBSSH2_SESSION* session = common_session->session;
|
|
|
|
/* Get user credentials */
|
|
guac_common_ssh_key* key = user->private_key;
|
|
|
|
/* Validate username provided */
|
|
if (user->username == NULL) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"SSH authentication requires a username.");
|
|
return 1;
|
|
}
|
|
|
|
/* Get list of supported authentication methods */
|
|
char* user_authlist = libssh2_userauth_list(session, user->username,
|
|
strlen(user->username));
|
|
|
|
/* If auth list is NULL, then authentication has succeeded with NONE */
|
|
if (user_authlist == NULL) {
|
|
guac_client_log(client, GUAC_LOG_DEBUG,
|
|
"SSH NONE authentication succeeded.");
|
|
return 0;
|
|
}
|
|
|
|
guac_client_log(client, GUAC_LOG_DEBUG,
|
|
"Supported authentication methods: %s", user_authlist);
|
|
|
|
/* Authenticate with private key, if provided */
|
|
if (key != NULL) {
|
|
|
|
/* Check if public key auth is supported on the server */
|
|
if (strstr(user_authlist, "publickey") == NULL) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"Public key authentication is not supported by "
|
|
"the SSH server");
|
|
return 1;
|
|
}
|
|
|
|
/* Attempt public key auth */
|
|
if (libssh2_userauth_publickey(session, user->username,
|
|
(unsigned char*) key->public_key, key->public_key_length,
|
|
guac_common_ssh_sign_callback, (void**) key)) {
|
|
|
|
/* Abort on failure */
|
|
char* error_message;
|
|
libssh2_session_last_error(session, &error_message, NULL, 0);
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"Public key authentication failed: %s", error_message);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
/* Private key authentication succeeded */
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* Attempt authentication with username + password. */
|
|
if (user->password == NULL && common_session->credential_handler)
|
|
user->password = common_session->credential_handler(client, "Password: ");
|
|
|
|
/* Authenticate with password, if provided */
|
|
if (user->password != NULL) {
|
|
|
|
/* Check if password auth is supported on the server */
|
|
if (strstr(user_authlist, "password") != NULL) {
|
|
|
|
/* Attempt password authentication */
|
|
if (libssh2_userauth_password(session, user->username, user->password)) {
|
|
|
|
/* Abort on failure */
|
|
char* error_message;
|
|
libssh2_session_last_error(session, &error_message, NULL, 0);
|
|
guac_client_abort(client,
|
|
GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"Password authentication failed: %s", error_message);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Password authentication succeeded */
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* Check if keyboard-interactive auth is supported on the server */
|
|
if (strstr(user_authlist, "keyboard-interactive") != NULL) {
|
|
|
|
/* Attempt keyboard-interactive auth using provided password */
|
|
if (libssh2_userauth_keyboard_interactive(session, user->username,
|
|
&guac_common_ssh_kbd_callback)) {
|
|
|
|
/* Abort on failure */
|
|
char* error_message;
|
|
libssh2_session_last_error(session, &error_message, NULL, 0);
|
|
guac_client_abort(client,
|
|
GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"Keyboard-interactive authentication failed: %s",
|
|
error_message);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Keyboard-interactive authentication succeeded */
|
|
return 0;
|
|
|
|
}
|
|
|
|
/* No known authentication types available */
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"Password and keyboard-interactive authentication are not "
|
|
"supported by the SSH server");
|
|
return 1;
|
|
|
|
}
|
|
|
|
/* No credentials provided */
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
|
|
"SSH authentication requires either a private key or a password.");
|
|
return 1;
|
|
|
|
}
|
|
|
|
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* host_key,
|
|
guac_ssh_credential_handler* credential_handler) {
|
|
|
|
int retval;
|
|
|
|
int fd;
|
|
struct addrinfo* addresses;
|
|
struct addrinfo* current_address;
|
|
|
|
char connected_address[1024];
|
|
char connected_port[64];
|
|
|
|
struct addrinfo hints = {
|
|
.ai_family = AF_UNSPEC,
|
|
.ai_socktype = SOCK_STREAM,
|
|
.ai_protocol = IPPROTO_TCP
|
|
};
|
|
|
|
/* Get addresses connection */
|
|
if ((retval = getaddrinfo(hostname, port, &hints, &addresses))) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
|
|
"Error parsing given address or port: %s",
|
|
gai_strerror(retval));
|
|
return NULL;
|
|
}
|
|
|
|
/* Attempt connection to each address until success */
|
|
current_address = addresses;
|
|
while (current_address != NULL) {
|
|
|
|
/* Resolve hostname */
|
|
if ((retval = getnameinfo(current_address->ai_addr,
|
|
current_address->ai_addrlen,
|
|
connected_address, sizeof(connected_address),
|
|
connected_port, sizeof(connected_port),
|
|
NI_NUMERICHOST | NI_NUMERICSERV)))
|
|
guac_client_log(client, GUAC_LOG_DEBUG,
|
|
"Unable to resolve host: %s", gai_strerror(retval));
|
|
|
|
/* Get socket */
|
|
fd = socket(current_address->ai_family, SOCK_STREAM, 0);
|
|
if (fd < 0) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
|
|
"Unable to create socket: %s", strerror(errno));
|
|
freeaddrinfo(addresses);
|
|
return NULL;
|
|
}
|
|
|
|
/* Connect */
|
|
if (connect(fd, current_address->ai_addr,
|
|
current_address->ai_addrlen) == 0) {
|
|
|
|
guac_client_log(client, GUAC_LOG_DEBUG,
|
|
"Successfully connected to host %s, port %s",
|
|
connected_address, connected_port);
|
|
|
|
/* Done if successful connect */
|
|
break;
|
|
|
|
}
|
|
|
|
/* Otherwise log information regarding bind failure */
|
|
guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to "
|
|
"host %s, port %s: %s",
|
|
connected_address, connected_port, strerror(errno));
|
|
|
|
close(fd);
|
|
current_address = current_address->ai_next;
|
|
|
|
}
|
|
|
|
/* Free addrinfo */
|
|
freeaddrinfo(addresses);
|
|
|
|
/* If unable to connect to anything, fail */
|
|
if (current_address == NULL) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
|
|
"Unable to connect to any addresses.");
|
|
return NULL;
|
|
}
|
|
|
|
/* Allocate new session */
|
|
guac_common_ssh_session* common_session =
|
|
malloc(sizeof(guac_common_ssh_session));
|
|
|
|
/* Open SSH session */
|
|
LIBSSH2_SESSION* session = libssh2_session_init_ex(NULL, NULL,
|
|
NULL, common_session);
|
|
if (session == NULL) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
|
|
"Session allocation failed.");
|
|
free(common_session);
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
|
|
/* Perform handshake */
|
|
if (libssh2_session_handshake(session, fd)) {
|
|
guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
|
|
"SSH handshake failed.");
|
|
free(common_session);
|
|
close(fd);
|
|
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;
|
|
common_session->session = session;
|
|
common_session->fd = fd;
|
|
common_session->credential_handler = credential_handler;
|
|
|
|
/* Attempt authentication */
|
|
if (guac_common_ssh_authenticate(common_session)) {
|
|
free(common_session);
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
|
|
/* Warn if keepalive below minimum value */
|
|
if (keepalive < 0) {
|
|
keepalive = 0;
|
|
guac_client_log(client, GUAC_LOG_WARNING, "negative keepalive intervals "
|
|
"are converted to 0, disabling keepalive.");
|
|
}
|
|
else if (keepalive == 1) {
|
|
guac_client_log(client, GUAC_LOG_WARNING, "keepalive interval will "
|
|
"be rounded up to minimum value of 2.");
|
|
}
|
|
|
|
/* Configure session keepalive */
|
|
libssh2_keepalive_config(common_session->session, 1, keepalive);
|
|
|
|
/* Return created session */
|
|
return common_session;
|
|
|
|
}
|
|
|
|
void guac_common_ssh_destroy_session(guac_common_ssh_session* session) {
|
|
|
|
/* Disconnect and clean up libssh2 */
|
|
libssh2_session_disconnect(session->session, "Bye");
|
|
libssh2_session_free(session->session);
|
|
|
|
/* Free all other data */
|
|
free(session);
|
|
|
|
}
|