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}