cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

ssh.c (19569B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20#include "common-ssh/key.h"
     21#include "common-ssh/ssh.h"
     22#include "common-ssh/user.h"
     23
     24#include <guacamole/client.h>
     25#include <guacamole/fips.h>
     26#include <guacamole/mem.h>
     27#include <guacamole/string.h>
     28#include <libssh2.h>
     29
     30#ifdef LIBSSH2_USES_GCRYPT
     31#include <gcrypt.h>
     32#endif
     33
     34#include <openssl/err.h>
     35#include <openssl/ssl.h>
     36
     37#include <errno.h>
     38#include <netdb.h>
     39#include <netinet/in.h>
     40#include <pthread.h>
     41#include <pwd.h>
     42#include <stddef.h>
     43#include <stdlib.h>
     44#include <string.h>
     45#include <sys/socket.h>
     46#include <unistd.h>
     47
     48#ifdef LIBSSH2_USES_GCRYPT
     49GCRY_THREAD_OPTION_PTHREAD_IMPL;
     50#endif
     51
     52/**
     53 * A list of all key exchange algorithms that are both FIPS-compliant, and
     54 * OpenSSL-supported. Note that "ext-info-c" is also included. While not a key
     55 * exchange algorithm per se, it must be in the list to ensure that the server
     56 * will send a SSH_MSG_EXT_INFO response, which is required to perform RSA key
     57 * upgrades.
     58 */
     59#define FIPS_COMPLIANT_KEX_ALGORITHMS "diffie-hellman-group-exchange-sha256,ext-info-c"
     60
     61/**
     62 * A list of ciphers that are both FIPS-compliant, and OpenSSL-supported.
     63 */
     64#define FIPS_COMPLIANT_CIPHERS "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,aes192-cbc,aes256-cbc"
     65
     66#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
     67/**
     68 * Array of mutexes, used by OpenSSL.
     69 */
     70static pthread_mutex_t* guac_common_ssh_openssl_locks = NULL;
     71
     72/**
     73 * Called by OpenSSL when locking or unlocking the Nth mutex.
     74 *
     75 * @param mode
     76 *     A bitmask denoting the action to be taken on the Nth lock, such as
     77 *     CRYPTO_LOCK or CRYPTO_UNLOCK.
     78 *
     79 * @param n
     80 *     The index of the lock to lock or unlock.
     81 *
     82 * @param file
     83 *     The filename of the function setting the lock, for debugging purposes.
     84 *
     85 * @param line
     86 *     The line number of the function setting the lock, for debugging
     87 *     purposes.
     88 */
     89static void guac_common_ssh_openssl_locking_callback(int mode, int n,
     90        const char* file, int line){
     91
     92    /* Lock given mutex upon request */
     93    if (mode & CRYPTO_LOCK)
     94        pthread_mutex_lock(&(guac_common_ssh_openssl_locks[n]));
     95
     96    /* Unlock given mutex upon request */
     97    else if (mode & CRYPTO_UNLOCK)
     98        pthread_mutex_unlock(&(guac_common_ssh_openssl_locks[n]));
     99
    100}
    101
    102/**
    103 * Called by OpenSSL when determining the current thread ID.
    104 *
    105 * @return
    106 *     An ID which uniquely identifies the current thread.
    107 */
    108static unsigned long guac_common_ssh_openssl_id_callback() {
    109    return (unsigned long) pthread_self();
    110}
    111
    112/**
    113 * Creates the given number of mutexes, such that OpenSSL will have at least
    114 * this number of mutexes at its disposal.
    115 *
    116 * @param count
    117 *     The number of mutexes (locks) to create.
    118 */
    119static void guac_common_ssh_openssl_init_locks(int count) {
    120
    121    int i;
    122
    123    /* Allocate required number of locks */
    124    guac_common_ssh_openssl_locks =
    125        guac_mem_alloc(sizeof(pthread_mutex_t), count);
    126
    127    /* Initialize each lock */
    128    for (i=0; i < count; i++)
    129        pthread_mutex_init(&(guac_common_ssh_openssl_locks[i]), NULL);
    130
    131}
    132
    133/**
    134 * Frees the given number of mutexes.
    135 *
    136 * @param count
    137 *     The number of mutexes (locks) to free.
    138 */
    139static void guac_common_ssh_openssl_free_locks(int count) {
    140
    141    int i;
    142
    143    /* SSL lock array was not initialized */
    144    if (guac_common_ssh_openssl_locks == NULL)
    145        return;
    146
    147    /* Free all locks */
    148    for (i=0; i < count; i++)
    149        pthread_mutex_destroy(&(guac_common_ssh_openssl_locks[i]));
    150
    151    /* Free lock array */
    152    guac_mem_free(guac_common_ssh_openssl_locks);
    153
    154}
    155#endif
    156
    157int guac_common_ssh_init(guac_client* client) {
    158
    159#ifdef LIBSSH2_USES_GCRYPT
    160    
    161    if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
    162    
    163        /* Init threadsafety in libgcrypt */
    164        gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread);
    165        
    166        /* Initialize GCrypt */
    167        if (!gcry_check_version(GCRYPT_VERSION)) {
    168            guac_client_log(client, GUAC_LOG_ERROR, "libgcrypt version mismatch.");
    169            return 1;
    170        }
    171
    172        /* Mark initialization as completed. */
    173        gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
    174    
    175    }
    176#endif
    177
    178#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
    179    /* Init threadsafety in OpenSSL */
    180    guac_common_ssh_openssl_init_locks(CRYPTO_num_locks());
    181    CRYPTO_set_id_callback(guac_common_ssh_openssl_id_callback);
    182    CRYPTO_set_locking_callback(guac_common_ssh_openssl_locking_callback);
    183#endif
    184
    185    /* Init OpenSSL */
    186    SSL_library_init();
    187    ERR_load_crypto_strings();
    188
    189    /* Init libssh2 */
    190    libssh2_init(0);
    191
    192    /* Success */
    193    return 0;
    194
    195}
    196
    197void guac_common_ssh_uninit() {
    198#ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS
    199    guac_common_ssh_openssl_free_locks(CRYPTO_num_locks());
    200#endif
    201}
    202
    203/**
    204 * Callback for the keyboard-interactive authentication method. Currently
    205 * supports just one prompt for the password. This callback is invoked as
    206 * needed to fullfill a call to libssh2_userauth_keyboard_interactive().
    207 *
    208 * @param name
    209 *     An arbitrary name which should be printed to the terminal for the
    210 *     benefit of the user. This is currently ignored.
    211 *
    212 * @param name_len
    213 *     The length of the name string, in bytes.
    214 *
    215 * @param instruction
    216 *     Arbitrary instructions which should be printed to the terminal for the
    217 *     benefit of the user. This is currently ignored.
    218 *
    219 * @param instruction_len
    220 *     The length of the instruction string, in bytes.
    221 *
    222 * @param num_prompts
    223 *     The number of keyboard-interactive prompts for which responses are
    224 *     requested. This callback currently only supports one prompt, and assumes
    225 *     that this prompt is requesting the password.
    226 *
    227 * @param prompts
    228 *     An array of all keyboard-interactive prompts for which responses are
    229 *     requested.
    230 *
    231 * @param responses
    232 *     A parallel array into which all prompt responses should be stored. Each
    233 *     entry within this array corresponds to the entry in the prompts array
    234 *     with the same index.
    235 *
    236 * @param abstract
    237 *     The value of the abstract parameter provided when the SSH session was
    238 *     created with libssh2_session_init_ex().
    239 */
    240static void guac_common_ssh_kbd_callback(const char *name, int name_len,
    241        const char *instruction, int instruction_len, int num_prompts,
    242        const LIBSSH2_USERAUTH_KBDINT_PROMPT *prompts,
    243        LIBSSH2_USERAUTH_KBDINT_RESPONSE *responses,
    244        void **abstract) {
    245
    246    guac_common_ssh_session* common_session =
    247        (guac_common_ssh_session*) *abstract;
    248
    249    guac_client* client = common_session->client;
    250
    251    /* Send password if only one prompt */
    252    if (num_prompts == 1) {
    253        char* password = common_session->user->password;
    254        responses[0].text = guac_strdup(password);
    255        responses[0].length = strlen(password);
    256    }
    257
    258    /* If more than one prompt, a single password is not enough */
    259    else
    260        guac_client_log(client, GUAC_LOG_WARNING,
    261                "Unsupported number of keyboard-interactive prompts: %i",
    262                num_prompts);
    263
    264}
    265
    266/**
    267 * Authenticates the user associated with the given session over SSH. All
    268 * required credentials must already be present within the user object
    269 * associated with the given session.
    270 *
    271 * @param session
    272 *     The session associated with the user to be authenticated.
    273 *
    274 * @return
    275 *     Zero if authentication succeeds, or non-zero if authentication has
    276 *     failed.
    277 */
    278static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) {
    279
    280    guac_client* client = common_session->client;
    281    guac_common_ssh_user* user = common_session->user;
    282    LIBSSH2_SESSION* session = common_session->session;
    283
    284    /* Get user credentials */
    285    guac_common_ssh_key* key = user->private_key;
    286
    287    /* Validate username provided */
    288    if (user->username == NULL) {
    289        guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    290                "SSH authentication requires a username.");
    291        return 1;
    292    }
    293
    294    /* Get list of supported authentication methods */
    295    size_t username_len = strlen(user->username);
    296    char* user_authlist = libssh2_userauth_list(session, user->username,
    297            username_len);
    298
    299    /* If auth list is NULL, then authentication has succeeded with NONE */
    300    if (user_authlist == NULL) {
    301        guac_client_log(client, GUAC_LOG_DEBUG,
    302            "SSH NONE authentication succeeded.");
    303        return 0;
    304    }
    305
    306    guac_client_log(client, GUAC_LOG_DEBUG,
    307            "Supported authentication methods: %s", user_authlist);
    308
    309    /* Authenticate with private key, if provided */
    310    if (key != NULL) {
    311
    312        /* Check if public key auth is supported on the server */
    313        if (strstr(user_authlist, "publickey") == NULL) {
    314            guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    315                    "Public key authentication is not supported by "
    316                    "the SSH server");
    317            return 1;
    318        }
    319
    320        /* Attempt public key auth */
    321        if (libssh2_userauth_publickey_frommemory(session, user->username,
    322                    username_len, NULL, 0, key->private_key,
    323                    key->private_key_length, key->passphrase)) {
    324
    325            /* Abort on failure */
    326            char* error_message;
    327            libssh2_session_last_error(session, &error_message, NULL, 0);
    328            guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    329                    "Public key authentication failed: %s", error_message);
    330
    331            return 1;
    332
    333        }
    334
    335        /* Private key authentication succeeded */
    336        return 0;
    337
    338    }
    339
    340    /* Attempt authentication with username + password. */
    341    if (user->password == NULL && common_session->credential_handler)
    342            user->password = common_session->credential_handler(client, "Password: ");
    343    
    344    /* Authenticate with password, if provided */
    345    if (user->password != NULL) {
    346
    347        /* Check if password auth is supported on the server */
    348        if (strstr(user_authlist, "password") != NULL) {
    349
    350            /* Attempt password authentication */
    351            if (libssh2_userauth_password(session, user->username, user->password)) {
    352
    353                /* Abort on failure */
    354                char* error_message;
    355                libssh2_session_last_error(session, &error_message, NULL, 0);
    356                guac_client_abort(client,
    357                        GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    358                        "Password authentication failed: %s", error_message);
    359
    360                return 1;
    361            }
    362
    363            /* Password authentication succeeded */
    364            return 0;
    365
    366        }
    367
    368        /* Check if keyboard-interactive auth is supported on the server */
    369        if (strstr(user_authlist, "keyboard-interactive") != NULL) {
    370
    371            /* Attempt keyboard-interactive auth using provided password */
    372            if (libssh2_userauth_keyboard_interactive(session, user->username,
    373                        &guac_common_ssh_kbd_callback)) {
    374
    375                /* Abort on failure */
    376                char* error_message;
    377                libssh2_session_last_error(session, &error_message, NULL, 0);
    378                guac_client_abort(client,
    379                        GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    380                        "Keyboard-interactive authentication failed: %s",
    381                        error_message);
    382
    383                return 1;
    384            }
    385
    386            /* Keyboard-interactive authentication succeeded */
    387            return 0;
    388
    389        }
    390
    391        /* No known authentication types available */
    392        guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    393                "Password and keyboard-interactive authentication are not "
    394                "supported by the SSH server");
    395        return 1;
    396
    397    }
    398
    399    /* No credentials provided */
    400    guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED,
    401            "SSH authentication requires either a private key or a password.");
    402    return 1;
    403
    404}
    405
    406guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client,
    407        const char* hostname, const char* port, guac_common_ssh_user* user,
    408        int keepalive, const char* host_key,
    409        guac_ssh_credential_handler* credential_handler) {
    410
    411    int retval;
    412
    413    int fd;
    414    struct addrinfo* addresses;
    415    struct addrinfo* current_address;
    416
    417    char connected_address[1024];
    418    char connected_port[64];
    419
    420    struct addrinfo hints = {
    421        .ai_family   = AF_UNSPEC,
    422        .ai_socktype = SOCK_STREAM,
    423        .ai_protocol = IPPROTO_TCP
    424    };
    425
    426    /* Get addresses connection */
    427    if ((retval = getaddrinfo(hostname, port, &hints, &addresses))) {
    428        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    429                "Error parsing given address or port: %s",
    430                gai_strerror(retval));
    431        return NULL;
    432    }
    433
    434    /* Attempt connection to each address until success */
    435    current_address = addresses;
    436    while (current_address != NULL) {
    437
    438        /* Resolve hostname */
    439        if ((retval = getnameinfo(current_address->ai_addr,
    440                current_address->ai_addrlen,
    441                connected_address, sizeof(connected_address),
    442                connected_port, sizeof(connected_port),
    443                NI_NUMERICHOST | NI_NUMERICSERV)))
    444            guac_client_log(client, GUAC_LOG_DEBUG,
    445                    "Unable to resolve host: %s", gai_strerror(retval));
    446
    447        /* Get socket */
    448        fd = socket(current_address->ai_family, SOCK_STREAM, 0);
    449        if (fd < 0) {
    450            guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    451                    "Unable to create socket: %s", strerror(errno));
    452            freeaddrinfo(addresses);
    453            return NULL;
    454        }
    455
    456        /* Connect */
    457        if (connect(fd, current_address->ai_addr,
    458                        current_address->ai_addrlen) == 0) {
    459
    460            guac_client_log(client, GUAC_LOG_DEBUG,
    461                    "Successfully connected to host %s, port %s",
    462                    connected_address, connected_port);
    463
    464            /* Done if successful connect */
    465            break;
    466
    467        }
    468
    469        /* Otherwise log information regarding bind failure */
    470        guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to "
    471                "host %s, port %s: %s",
    472                connected_address, connected_port, strerror(errno));
    473
    474        close(fd);
    475        current_address = current_address->ai_next;
    476
    477    }
    478
    479    /* Free addrinfo */
    480    freeaddrinfo(addresses);
    481
    482    /* If unable to connect to anything, fail */
    483    if (current_address == NULL) {
    484        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND,
    485                "Unable to connect to any addresses.");
    486        return NULL;
    487    }
    488
    489    /* Allocate new session */
    490    guac_common_ssh_session* common_session =
    491        guac_mem_alloc(sizeof(guac_common_ssh_session));
    492
    493    /* Open SSH session */
    494    LIBSSH2_SESSION* session = libssh2_session_init_ex(NULL, NULL,
    495            NULL, common_session);
    496    if (session == NULL) {
    497        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    498                "Session allocation failed.");
    499        guac_mem_free(common_session);
    500        close(fd);
    501        return NULL;
    502    }
    503
    504    /*
    505     * If FIPS mode is enabled, prefer only FIPS-compatible algorithms and
    506     * ciphers that are also supported by libssh2. For more info, see:
    507     * https://csrc.nist.gov/CSRC/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2906.pdf
    508     */
    509    if (guac_fips_enabled()) {
    510        libssh2_session_method_pref(session, LIBSSH2_METHOD_KEX, FIPS_COMPLIANT_KEX_ALGORITHMS);
    511        libssh2_session_method_pref(session, LIBSSH2_METHOD_CRYPT_CS, FIPS_COMPLIANT_CIPHERS);
    512        libssh2_session_method_pref(session, LIBSSH2_METHOD_CRYPT_SC, FIPS_COMPLIANT_CIPHERS);
    513    }
    514
    515    /* Perform handshake */
    516    if (libssh2_session_handshake(session, fd)) {
    517        guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR,
    518                "SSH handshake failed.");
    519        guac_mem_free(common_session);
    520        close(fd);
    521        return NULL;
    522    }
    523
    524    /* Get host key of remote system we're connecting to */
    525    size_t remote_hostkey_len;
    526    const char *remote_hostkey = libssh2_session_hostkey(session, &remote_hostkey_len, NULL);
    527
    528    /* Failure to retrieve a host key means we should abort */
    529    if (!remote_hostkey) {
    530        guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    531            "Failed to get host key for %s", hostname);
    532        guac_mem_free(common_session);
    533        close(fd);
    534        return NULL;
    535    }
    536
    537    /* SSH known host key checking. */
    538    int known_host_check = guac_common_ssh_verify_host_key(session, client, host_key,
    539                                                           hostname, atoi(port), remote_hostkey,
    540                                                           remote_hostkey_len);
    541
    542    /* Abort on any error codes */
    543    if (known_host_check != 0) {
    544        char* err_msg;
    545        libssh2_session_last_error(session, &err_msg, NULL, 0);
    546
    547        if (known_host_check < 0)
    548            guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    549                "Error occurred attempting to check host key: %s", err_msg);
    550
    551        if (known_host_check > 0)
    552            guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR,
    553                "Host key did not match any provided known host keys. %s", err_msg);
    554
    555        guac_mem_free(common_session);
    556        close(fd);
    557        return NULL;
    558    }
    559
    560    /* Store basic session data */
    561    common_session->client = client;
    562    common_session->user = user;
    563    common_session->session = session;
    564    common_session->fd = fd;
    565    common_session->credential_handler = credential_handler;
    566
    567    /* Attempt authentication */
    568    if (guac_common_ssh_authenticate(common_session)) {
    569        guac_mem_free(common_session);
    570        close(fd);
    571        return NULL;
    572    }
    573
    574    /* Warn if keepalive below minimum value */
    575    if (keepalive < 0) {
    576        keepalive = 0;
    577        guac_client_log(client, GUAC_LOG_WARNING, "negative keepalive intervals "
    578            "are converted to 0, disabling keepalive.");
    579    }
    580    else if (keepalive == 1) {
    581        guac_client_log(client, GUAC_LOG_WARNING, "keepalive interval will "
    582            "be rounded up to minimum value of 2.");
    583    }
    584
    585    /* Configure session keepalive */
    586    libssh2_keepalive_config(common_session->session, 1, keepalive);
    587
    588    /* Return created session */
    589    return common_session;
    590
    591}
    592
    593void guac_common_ssh_destroy_session(guac_common_ssh_session* session) {
    594
    595    /* Disconnect and clean up libssh2 */
    596    libssh2_session_disconnect(session->session, "Bye");
    597    libssh2_session_free(session->session);
    598
    599    /* Free all other data */
    600    guac_mem_free(session);
    601
    602}