diff options
-rw-r--r-- | client/application.cpp | 10 | ||||
-rw-r--r-- | client/application.h | 2 | ||||
-rw-r--r-- | client/platform.h | 2 | ||||
-rw-r--r-- | client/red_channel.cpp | 17 | ||||
-rw-r--r-- | client/red_channel.h | 6 | ||||
-rw-r--r-- | client/red_client.cpp | 44 | ||||
-rw-r--r-- | client/red_client.h | 3 | ||||
-rw-r--r-- | client/red_peer.cpp | 401 | ||||
-rw-r--r-- | client/red_peer.h | 41 | ||||
-rw-r--r-- | client/windows/platform.cpp | 19 | ||||
-rw-r--r-- | client/x11/platform.cpp | 14 | ||||
-rw-r--r-- | common/red.h | 21 | ||||
-rw-r--r-- | server/reds.c | 214 |
13 files changed, 742 insertions, 52 deletions
diff --git a/client/application.cpp b/client/application.cpp index c253cccc..3566adba 100644 --- a/client/application.cpp +++ b/client/application.cpp @@ -49,6 +49,8 @@ #define STICKY_KEY_PIXMAP ALT_IMAGE_RES_ID #define STICKY_KEY_TIMEOUT 750 +#define CA_FILE_NAME "spice_truststore.pem" + #ifdef CAIRO_CANVAS_CACH_IS_SHARED mutex_t cairo_surface_user_data_mutex; #endif @@ -1596,6 +1598,11 @@ bool Application::process_cmd_line(int argc, char** argv) _peer_con_opt[RED_CHANNEL_PLAYBACK] = RedPeer::ConnectionOptions::CON_OP_INVALID; _peer_con_opt[RED_CHANNEL_RECORD] = RedPeer::ConnectionOptions::CON_OP_INVALID; + _host_auth_opt.type_flags = RedPeer::HostAuthOptions::HOST_AUTH_OP_NAME; + + Platform::get_spice_config_dir(_host_auth_opt.CA_file); + _host_auth_opt.CA_file += CA_FILE_NAME; + parser.begin(argc, argv); char* val; @@ -1614,12 +1621,11 @@ bool Application::process_cmd_line(int argc, char** argv) break; } case SPICE_OPT_SPORT: { - if ((port = str_to_port(val)) == -1) { + if ((sport = str_to_port(val)) == -1) { std::cout << "invalid secure port " << val << "\n"; _exit_code = SPICEC_ERROR_CODE_INVALID_ARG; return false; } - sport = port; break; } case SPICE_OPT_FULL_SCREEN: diff --git a/client/application.h b/client/application.h index 38fd30ec..3c0297ff 100644 --- a/client/application.h +++ b/client/application.h @@ -155,6 +155,7 @@ public: void external_show(); void connect(); const PeerConnectionOptMap& get_con_opt_map() {return _peer_con_opt;} + const RedPeer::HostAuthOptions& get_host_auth_opt() { return _host_auth_opt;} uint32_t get_mouse_mode(); const std::vector<int>& get_canvas_types() { return _canvas_types;} @@ -223,6 +224,7 @@ private: private: RedClient _client; PeerConnectionOptMap _peer_con_opt; + RedPeer::HostAuthOptions _host_auth_opt; std::vector<bool> _enabled_channels; std::vector<RedScreen*> _screens; RedScreen* _main_screen; diff --git a/client/platform.h b/client/platform.h index ff89181b..f0ffc0df 100644 --- a/client/platform.h +++ b/client/platform.h @@ -47,6 +47,8 @@ public: static void send_quit_request(); + static void get_spice_config_dir(std::string& path); + enum ThreadPriority { PRIORITY_INVALID, PRIORITY_TIME_CRITICAL, diff --git a/client/red_channel.cpp b/client/red_channel.cpp index 0afe3cef..153055d9 100644 --- a/client/red_channel.cpp +++ b/client/red_channel.cpp @@ -89,6 +89,8 @@ void RedChannelBase::link(uint32_t connection_id, const std::string& password) header.major_version); } + _remote_minor = header.minor_version; + AutoArray<uint8_t> reply_buf(new uint8_t[header.size]); recive(reply_buf.get(), header.size); @@ -155,11 +157,11 @@ void RedChannelBase::link(uint32_t connection_id, const std::string& password) } void RedChannelBase::connect(const ConnectionOptions& options, uint32_t connection_id, - uint32_t ip, std::string password) + const char* host, std::string password) { if (options.allow_unsecure()) { try { - RedPeer::connect_unsecure(ip, options.unsecure_port); + RedPeer::connect_unsecure(host, options.unsecure_port); link(connection_id, password); return; } catch (...) { @@ -170,16 +172,10 @@ void RedChannelBase::connect(const ConnectionOptions& options, uint32_t connecti } } ASSERT(options.allow_secure()); - RedPeer::connect_secure(options, ip); + RedPeer::connect_secure(options, host); link(connection_id, password); } -void RedChannelBase::connect(const ConnectionOptions& options, uint32_t connection_id, - const char* host, std::string password) -{ - connect(options, connection_id, host_by_name(host), password); -} - void RedChannelBase::set_capability(ChannelCaps& caps, uint32_t cap) { uint32_t word_index = cap / 32; @@ -399,7 +395,8 @@ void RedChannel::run() set_state(CONNECTING_STATE); ConnectionOptions con_options(_client.get_connection_options(get_type()), _client.get_port(), - _client.get_sport()); + _client.get_sport(), + _client.get_host_auth_options()); RedChannelBase::connect(con_options, _client.get_connection_id(), _client.get_host(), _client.get_password()); on_connect(); diff --git a/client/red_channel.h b/client/red_channel.h index c2f94bd7..996aaf66 100644 --- a/client/red_channel.h +++ b/client/red_channel.h @@ -55,14 +55,14 @@ public: uint8_t get_type() { return _type;} uint8_t get_id() { return _id;} - void connect(const ConnectionOptions& options, uint32_t connection_id, uint32_t ip, - std::string password); void connect(const ConnectionOptions& options, uint32_t connection_id, const char *host, std::string password); const ChannelCaps& get_common_caps() { return _common_caps;} const ChannelCaps& get_caps() {return _caps;} + uint32_t get_peer_minor() { return _remote_minor;} + protected: void set_common_capability(uint32_t cap); void set_capability(uint32_t cap); @@ -83,6 +83,8 @@ private: ChannelCaps _remote_common_caps; ChannelCaps _remote_caps; + + uint32_t _remote_minor; }; class SendTrigger: public EventSources::Trigger { diff --git a/client/red_client.cpp b/client/red_client.cpp index cf4562b3..df88e7a8 100644 --- a/client/red_client.cpp +++ b/client/red_client.cpp @@ -23,6 +23,16 @@ #include "utils.h" #include "debug.h" +#ifdef __GNUC__ +typedef struct __attribute__ ((__packed__)) OldRedMigrationBegin { +#else +typedef struct __declspec(align(1)) OldRedMigrationBegin { +#endif + uint16_t port; + uint16_t sport; + char host[0]; +} OldRedMigrationBegin; + Migrate::Migrate(RedClient& client) : _client (client) , _running (false) @@ -114,18 +124,19 @@ void Migrate::connect_one(MigChannel& channel, const RedPeer::ConnectionOptions& void Migrate::run() { uint32_t connection_id; + RedPeer::ConnectionOptions::Type conn_type; DBG(0, ""); try { - RedPeer::ConnectionOptions con_opt(_client.get_connection_options(RED_CHANNEL_MAIN), - _port, _port); + conn_type = _client.get_connection_options(RED_CHANNEL_MAIN); + RedPeer::ConnectionOptions con_opt(conn_type, _port, _sport, _auth_options); MigChannels::iterator iter = _channels.begin(); connection_id = _client.get_connection_id(); connect_one(**iter, con_opt, connection_id); + for (++iter; iter != _channels.end(); ++iter) { - con_opt = RedPeer::ConnectionOptions( - _client.get_connection_options((*iter)->get_type()), - _port, _sport); + conn_type = _client.get_connection_options((*iter)->get_type()); + con_opt = RedPeer::ConnectionOptions(conn_type, _port, _sport, _auth_options); connect_one(**iter, con_opt, connection_id); } _connected = true; @@ -157,9 +168,24 @@ void Migrate::start(const RedMigrationBegin* migrate) { DBG(0, ""); abort(); - _host.assign(migrate->host); - _port = migrate->port ? migrate->port : -1; - _sport = migrate->sport ? migrate->sport : -1; + if ((RED_VERSION_MAJOR == 1) && (_client.get_peer_minor() < 1)) { + LOG_INFO("server minor version incompatible for destination authentication" + "(missing dest pubkey in RedMigrationBegin)"); + OldRedMigrationBegin* old_migrate = (OldRedMigrationBegin*)migrate; + _host.assign(old_migrate->host); + _port = old_migrate->port ? old_migrate->port : -1; + _sport = old_migrate->sport ? old_migrate->sport : -1;; + _auth_options = _client.get_host_auth_options(); + } else { + _host.assign(((char*)migrate) + migrate->host_offset); + _port = migrate->port ? migrate->port : -1; + _sport = migrate->sport ? migrate->sport : -1; + _auth_options.type_flags = RedPeer::HostAuthOptions::HOST_AUTH_OP_PUBKEY; + _auth_options.host_pubkey.assign(((uint8_t*)migrate)+ migrate->pub_key_offset, + ((uint8_t*)migrate)+ migrate->pub_key_offset + + migrate->pub_key_size); + } + _password = _client._password; Lock lock(_lock); _running = true; @@ -382,6 +408,8 @@ void RedClient::connect() for (; iter != end; iter++) { _con_opt_map[(*iter).first] = (*iter).second; } + + _host_auth_opt = _application.get_host_auth_opt(); RedChannel::connect(); } diff --git a/client/red_client.h b/client/red_client.h index 04c800cc..d97128e5 100644 --- a/client/red_client.h +++ b/client/red_client.h @@ -76,6 +76,7 @@ private: std::string _host; int _port; int _sport; + RedPeer::HostAuthOptions _auth_options; Thread* _thread; Mutex _lock; Condition _cond; @@ -148,6 +149,7 @@ public: Application& get_application() { return _application;} bool is_auto_display_res() { return _auto_display_res;} RedPeer::ConnectionOptions::Type get_connection_options(uint32_t channel_type); + RedPeer::HostAuthOptions& get_host_auth_options() { return _host_auth_opt;} void get_sync_info(uint8_t channel_type, uint8_t channel_id, SyncInfo& info); void wait_for_channels(int wait_list_size, RedWaitForChannel* wait_list); PixmapCache& get_pixmap_cache() {return _pixmap_cache;} @@ -215,6 +217,7 @@ private: AutoRef<AgentTimer> _agent_timer; PeerConnectionOptMap _con_opt_map; + RedPeer::HostAuthOptions _host_auth_opt; Migrate _migrate; Mutex _channels_lock; typedef std::list<ChannelFactory*> Factorys; diff --git a/client/red_peer.cpp b/client/red_peer.cpp index d0868725..a1dca53c 100644 --- a/client/red_peer.cpp +++ b/client/red_peer.cpp @@ -32,6 +32,8 @@ #define SOCKET_ERROR -1 #define closesocket(sock) ::close(sock) #endif +#include <openssl/x509.h> +#include <openssl/x509v3.h> #include "red.h" #include "red_peer.h" #include "utils.h" @@ -64,10 +66,18 @@ int inet_aton(const char *ip, struct in_addr *in_addr) #define sock_err_message(err) strerror(err) #endif +typedef struct SslVerifyCbData { + RedPeer::HostAuthOptions info; + const char* host_name; + bool all_preverify_ok; +} SslVerifyCbData; + static void ssl_error() { + unsigned long last_error = ERR_peek_last_error(); + ERR_print_errors_fp(stderr); - THROW_ERR(SPICEC_ERROR_CODE_SSL_ERROR, "SSL Error"); + THROW_ERR(SPICEC_ERROR_CODE_SSL_ERROR, "SSL Error:", ERR_error_string(last_error, NULL)); } RedPeer::RedPeer() @@ -122,13 +132,15 @@ uint32_t RedPeer::host_by_name(const char* host) return ntohl(return_value); } -void RedPeer::connect_unsecure(uint32_t ip, int port) +void RedPeer::connect_unsecure(const char* host, int port) { struct sockaddr_in addr; int no_delay; - + uint32_t ip; ASSERT(_ctx == NULL && _ssl == NULL && _peer == INVALID_SOCKET); try { + ip = host_by_name(host); + addr.sin_port = htons(port); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(ip); @@ -162,15 +174,349 @@ void RedPeer::connect_unsecure(uint32_t ip, int port) } } -void RedPeer::connect_unsecure(const char* host, int port) +bool RedPeer::verify_pubkey(X509* cert, const HostAuthOptions::PublicKey& key) +{ + EVP_PKEY* cert_pubkey = NULL; + EVP_PKEY* orig_pubkey = NULL; + BIO* bio = NULL; + uint8_t* c_key = NULL; + int ret = 0; + + if (key.empty()) { + return false; + } + + ASSERT(cert); + + try { + cert_pubkey = X509_get_pubkey(cert); + if (!cert_pubkey) { + THROW("reading public key from certificate failed"); + } + + c_key = new uint8_t[key.size()]; + memcpy(c_key, &key[0], key.size()); + + bio = BIO_new_mem_buf((void*)c_key, key.size()); + if (!bio) { + THROW("creating BIO failed"); + } + + orig_pubkey = d2i_PUBKEY_bio(bio, NULL); + if (!orig_pubkey) { + THROW("reading pubkey from bio failed"); + } + + ret = EVP_PKEY_cmp(orig_pubkey, cert_pubkey); + + BIO_free(bio); + EVP_PKEY_free(orig_pubkey); + EVP_PKEY_free(cert_pubkey); + delete []c_key; + if (ret == 1) { + DBG(0, "public keys match"); + return true; + } else if (ret == 0) { + DBG(0, "public keys mismatch"); + return false; + } else { + DBG(0, "public keys types mismatch"); + return false; + } + } catch (Exception& e) { + LOG_WARN("%s", e.what()); + + if (bio) { + BIO_free(bio); + } + + if (orig_pubkey) { + EVP_PKEY_free(orig_pubkey); + } + + if (cert_pubkey) { + EVP_PKEY_free(cert_pubkey); + } + delete []c_key; + return false; + } +} + +/* From gnutls: compare host_name against certificate, taking account of wildcards. + * return true on success or false on error. + * + * note: cert_name_size is required as X509 certs can contain embedded NULs in + * the strings such as CN or subjectAltName + */ +bool RedPeer::x509_cert_host_name_compare(const char *cert_name, int cert_name_size, + const char *host_name) +{ + /* find the first different character */ + for (; *cert_name && *host_name && (toupper(*cert_name) == toupper(*host_name)); + cert_name++, host_name++, cert_name_size--); + + /* the strings are the same */ + if (cert_name_size == 0 && *host_name == '\0') + return true; + + if (*cert_name == '*') + { + /* a wildcard certificate */ + cert_name++; + cert_name_size--; + + while (true) + { + /* Use a recursive call to allow multiple wildcards */ + if (RedPeer::x509_cert_host_name_compare(cert_name, cert_name_size, host_name)) { + return true; + } + + /* wildcards are only allowed to match a single domain + component or component fragment */ + if (*host_name == '\0' || *host_name == '.') + break; + host_name++; + } + + return false; + } + + return false; +} + +/* + * From gnutls_x509_crt_check_hostname - compares the hostname with certificate's hostname + * + * This function will check if the given certificate's subject matches + * the hostname. This is a basic implementation of the matching + * described in RFC2818 (HTTPS), which takes into account wildcards, + * and the DNSName/IPAddress subject alternative name PKIX extension. + * + */ +bool RedPeer::verify_host_name(X509* cert, const char* host_name) +{ + GENERAL_NAMES* subject_alt_names; + bool found_dns_name = false; + struct in_addr addr; + int addr_len = 0; + bool cn_match = false; + + ASSERT(cert); + + // only IpV4 supported + if (inet_aton(host_name, &addr)) { + addr_len = sizeof(struct in_addr); + } + + /* try matching against: + * 1) a DNS name or IP address as an alternative name (subjectAltName) extension + * in the certificate + * 2) the common name (CN) in the certificate + * + * either of these may be of the form: *.domain.tld + * + * only try (2) if there is no subjectAltName extension of + * type dNSName + */ + + + subject_alt_names = (GENERAL_NAMES*)X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); + + if (subject_alt_names) { + int num_alts = sk_GENERAL_NAME_num(subject_alt_names); + for (int i = 0; i < num_alts; i++) { + const GENERAL_NAME* name = sk_GENERAL_NAME_value(subject_alt_names, i); + if (name->type == GEN_DNS) { + found_dns_name = true; + if (RedPeer::x509_cert_host_name_compare((char *)ASN1_STRING_data(name->d.dNSName), + ASN1_STRING_length(name->d.dNSName), + host_name)) { + DBG(0, "alt name match=%s", ASN1_STRING_data(name->d.dNSName)); + GENERAL_NAMES_free(subject_alt_names); + return true; + } + } else if (name->type == GEN_IPADD) { + int alt_ip_len = ASN1_STRING_length(name->d.iPAddress); + found_dns_name = true; + if ((addr_len == alt_ip_len)&& + !memcmp(ASN1_STRING_data(name->d.iPAddress), &addr, addr_len)) { + DBG(0, "alt name IP match=%s", + inet_ntoa(*((struct in_addr*)ASN1_STRING_data(name->d.dNSName)))); + GENERAL_NAMES_free(subject_alt_names); + return true; + } + } + } + GENERAL_NAMES_free(subject_alt_names); + } + + if (found_dns_name) + { + DBG(0, "SubjectAltName mismatch"); + return false; + } + + /* extracting commonNames */ + X509_NAME* subject = X509_get_subject_name(cert); + if (subject) { + int pos = -1; + X509_NAME_ENTRY* cn_entry; + ASN1_STRING* cn_asn1; + + while ((pos = X509_NAME_get_index_by_NID(subject, NID_commonName, pos)) != -1) { + cn_entry = X509_NAME_get_entry(subject, pos); + if (!cn_entry) { + continue; + } + cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry); + if (!cn_asn1) { + continue; + } + + if (RedPeer::x509_cert_host_name_compare((char*)ASN1_STRING_data(cn_asn1), + ASN1_STRING_length(cn_asn1), + host_name)) { + DBG(0, "common name match=%s", (char*)ASN1_STRING_data(cn_asn1)); + cn_match = true; + break; + } + } + } + + if (!cn_match) { + DBG(0, "common name mismatch"); + } + return cn_match; + +} + +bool RedPeer::verify_subject(X509* cert, const HostAuthOptions::CertFieldValueList& subject) { - connect_unsecure(host_by_name(host), port); + X509_NAME* cert_subject = NULL; + HostAuthOptions::CertFieldValueList::const_iterator subject_iter; + X509_NAME* in_subject; + int ret; + + ASSERT(cert); + + cert_subject = X509_get_subject_name(cert); + if (!cert_subject) { + LOG_WARN("reading certificate subject failed"); + return false; + } + + if (X509_NAME_entry_count(cert_subject) != subject.size()) { + DBG(0, "subject mismatch: #entries cert=%d, input=%d", + X509_NAME_entry_count(cert_subject), subject.size()); + return false; + } + + in_subject = X509_NAME_new(); + if (!in_subject) { + LOG_WARN("failed to allocate X509_NAME"); + return false; + } + + for (subject_iter = subject.begin(); subject_iter != subject.end(); subject_iter++) { + if (!X509_NAME_add_entry_by_txt(in_subject, + subject_iter->first.c_str(), + MBSTRING_UTF8, + (const unsigned char*)subject_iter->second.c_str(), + subject_iter->second.length(), -1, 0)) { + LOG_WARN("failed to add entry %s=%s to X509_NAME", + subject_iter->first.c_str(), subject_iter->second.c_str()); + X509_NAME_free(in_subject); + return false; + } + } + + ret = X509_NAME_cmp(cert_subject, in_subject); + X509_NAME_free(in_subject); + + if (ret == 0) { + DBG(0, "subjects match"); + return true; + } else { + DBG(0, "subjects mismatch"); + return false; + } +} + +int RedPeer::ssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) +{ + int depth; + SSL *ssl; + X509* cert; + SslVerifyCbData* verify_data; + int auth_flags; + + depth = X509_STORE_CTX_get_error_depth(ctx); + + ssl = (SSL*)X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + if (!ssl) { + LOG_WARN("failed to get ssl connection"); + return 0; + } + + verify_data = (SslVerifyCbData*)SSL_get_app_data(ssl); + auth_flags = verify_data->info.type_flags; + + if (depth > 0) { + // if certificate verification failed, we can still authorize the server + // if its public key matches the one we hold in the peer_connect_options. + if (!preverify_ok) { + DBG(0, "openssl verify failed at depth=%d", depth); + verify_data->all_preverify_ok = false; + if (auth_flags & HostAuthOptions::HOST_AUTH_OP_PUBKEY) { + return 1; + } else { + return 0; + } + } else { + return preverify_ok; + } + } + + /* depth == 0 */ + cert = X509_STORE_CTX_get_current_cert(ctx); + if (!cert) { + LOG_WARN("failed to get server certificate"); + return 0; + } + + if (auth_flags & HostAuthOptions::HOST_AUTH_OP_PUBKEY) { + if (verify_pubkey(cert, verify_data->info.host_pubkey)) { + return 1; + } + } + + if (!verify_data->all_preverify_ok || !preverify_ok) { + return 0; + } + + if (auth_flags & HostAuthOptions::HOST_AUTH_OP_NAME) { + if (verify_host_name(cert, verify_data->host_name)) { + return 1; + } + } + + if (auth_flags & HostAuthOptions::HOST_AUTH_OP_SUBJECT) { + if (verify_subject(cert, verify_data->info.host_subject)) { + return 1; + } + } + return 0; } -// todo: use SSL_CTX_set_cipher_list, SSL_CTX_load_verify_location etc. -void RedPeer::connect_secure(const ConnectionOptions& options, uint32_t ip) +// todo: use SSL_CTX_set_cipher_list, etc. +void RedPeer::connect_secure(const ConnectionOptions& options, const char* host) { - connect_unsecure(ip, options.secure_port); + int return_code; + int auth_flags; + SslVerifyCbData auth_data; + + connect_unsecure(host, options.secure_port); ASSERT(_ctx == NULL && _ssl == NULL && _peer != INVALID_SOCKET); try { @@ -179,12 +525,39 @@ void RedPeer::connect_secure(const ConnectionOptions& options, uint32_t ip) #else SSL_METHOD *ssl_method = TLSv1_method(); #endif + auth_data.info = options.host_auth; + auth_data.host_name = host; + auth_data.all_preverify_ok = true; _ctx = SSL_CTX_new(ssl_method); if (_ctx == NULL) { ssl_error(); } + auth_flags = auth_data.info.type_flags; + if ((auth_flags & RedPeer::HostAuthOptions::HOST_AUTH_OP_NAME) || + (auth_flags & RedPeer::HostAuthOptions::HOST_AUTH_OP_SUBJECT)) { + std::string CA_file = auth_data.info.CA_file; + ASSERT(!CA_file.empty()); + + return_code = SSL_CTX_load_verify_locations(_ctx, CA_file.c_str(), NULL); + if (return_code != 1) { + if (auth_flags & RedPeer::HostAuthOptions::HOST_AUTH_OP_PUBKEY) { + LOG_WARN("SSL_CTX_load_verify_locations failed, CA_file=%s. " + "only pubkey authentication is active", CA_file.c_str()); + auth_data.info.type_flags = RedPeer::HostAuthOptions::HOST_AUTH_OP_PUBKEY; + } + else { + LOG_WARN("SSL_CTX_load_verify_locations failed CA_file=%s", CA_file.c_str()); + ssl_error(); + } + } + } + + if (auth_flags) { + SSL_CTX_set_verify(_ctx, SSL_VERIFY_PEER, ssl_verify_callback); + } + _ssl = SSL_new(_ctx); if (!_ssl) { THROW("create ssl failed"); @@ -196,10 +569,13 @@ void RedPeer::connect_secure(const ConnectionOptions& options, uint32_t ip) } SSL_set_bio(_ssl, sbio, sbio); + SSL_set_app_data(_ssl, &auth_data); - int return_code = SSL_connect(_ssl); + return_code = SSL_connect(_ssl); if (return_code <= 0) { - SSL_get_error(_ssl, return_code); + int ssl_error_code = SSL_get_error(_ssl, return_code); + LOG_WARN("failed to connect w/SSL, ssl_error %s", + ERR_error_string(ssl_error_code, NULL)); ssl_error(); } } catch (...) { @@ -209,11 +585,6 @@ void RedPeer::connect_secure(const ConnectionOptions& options, uint32_t ip) } } -void RedPeer::connect_secure(const ConnectionOptions& options, const char* host) -{ - connect_secure(options, host_by_name(host)); -} - void RedPeer::shutdown() { if (_peer != INVALID_SOCKET) { diff --git a/client/red_peer.h b/client/red_peer.h index f78405b5..761aed1d 100644 --- a/client/red_peer.h +++ b/client/red_peer.h @@ -42,6 +42,30 @@ public: class OutMessage; class DisconnectedException {}; + class HostAuthOptions { + public: + + enum Type { + HOST_AUTH_OP_PUBKEY = 1, + HOST_AUTH_OP_NAME = (1 << 1), + HOST_AUTH_OP_SUBJECT = (1 << 2), + }; + + typedef std::vector<uint8_t> PublicKey; + typedef std::pair<std::string, std::string> CertFieldValuePair; + typedef std::list<CertFieldValuePair> CertFieldValueList; + + HostAuthOptions() : type_flags(0) {} + + public: + + int type_flags; + + PublicKey host_pubkey; + CertFieldValueList host_subject; + std::string CA_file; + }; + class ConnectionOptions { public: @@ -52,10 +76,12 @@ public: CON_OP_BOTH, }; - ConnectionOptions(Type in_type, int in_port, int in_sport) + ConnectionOptions(Type in_type, int in_port, int in_sport, + const HostAuthOptions& in_host_auth) : type (in_type) , unsecure_port (in_port) , secure_port (in_sport) + , host_auth (in_host_auth) { } @@ -75,12 +101,10 @@ public: Type type; int unsecure_port; int secure_port; + HostAuthOptions host_auth; // for secure connection }; - void connect_unsecure(uint32_t ip, int port); void connect_unsecure(const char* host, int port); - - void connect_secure(const ConnectionOptions& options, uint32_t ip); void connect_secure(const ConnectionOptions& options, const char* host); void disconnect(); @@ -100,6 +124,15 @@ protected: virtual void on_event() {} virtual int get_socket() { return _peer;} + static bool x509_cert_host_name_compare(const char *cert_name, int cert_name_size, + const char *host_name); + + static bool verify_pubkey(X509* cert, const HostAuthOptions::PublicKey& key); + static bool verify_host_name(X509* cert, const char* host_name); + static bool verify_subject(X509* cert, const HostAuthOptions::CertFieldValueList& subject); + + static int ssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx); + private: void shutdown(); void cleanup(); diff --git a/client/windows/platform.cpp b/client/windows/platform.cpp index 2988827d..8aba5d11 100644 --- a/client/windows/platform.cpp +++ b/client/windows/platform.cpp @@ -17,6 +17,8 @@ #include "common.h" +#include <shlobj.h> + #include "platform.h" #include "win_platform.h" #include "utils.h" @@ -28,6 +30,8 @@ #include "cursor.h" #include "named_pipe.h" +#define SPICE_CONFIG_DIR "spicec\\" + int gdi_handlers = 0; extern HINSTANCE instance; @@ -427,6 +431,21 @@ bool Platform::is_monitors_pos_valid() return true; } +void Platform::get_spice_config_dir(std::string& path) +{ + char app_data_path[MAX_PATH]; + HRESULT res = SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, app_data_path); + if (res != S_OK) { + throw Exception("get user app data dir failed"); + } + + path = app_data_path; + if (strcmp((app_data_path + strlen(app_data_path) - 2), "\\") != 0) { + path += "\\"; + } + path += SPICE_CONFIG_DIR; +} + void Platform::init() { create_message_wind(); diff --git a/client/x11/platform.cpp b/client/x11/platform.cpp index a1c08ca8..cff22891 100644 --- a/client/x11/platform.cpp +++ b/client/x11/platform.cpp @@ -64,6 +64,8 @@ #define USE_XRANDR_1_2 #endif +#define SPICE_CONFIG_DIR ".spicec/" + static Display* x_display = NULL; static bool x_shm_avail = false; static XVisualInfo **vinfo = NULL; @@ -1895,6 +1897,18 @@ bool Platform::is_monitors_pos_valid() return (ScreenCount(x_display) == 1); } +void Platform::get_spice_config_dir(std::string& path) +{ + char* home_dir = getenv("HOME"); + if (!home_dir) { + throw Exception("get home dir failed"); + } + + path = home_dir; + path += "/"; + path += SPICE_CONFIG_DIR; +} + static void root_win_proc(XEvent& event) { #ifdef USE_XRANDR_1_2 diff --git a/common/red.h b/common/red.h index 4b0a3642..cead0667 100644 --- a/common/red.h +++ b/common/red.h @@ -46,7 +46,7 @@ #define RED_MAGIC (*(uint32_t*)"REDQ") #define RED_VERSION_MAJOR 1 -#define RED_VERSION_MINOR 0 +#define RED_VERSION_MINOR 1 // Encryption & Ticketing Parameters #define RED_MAX_PASSWORD_LENGTH 60 @@ -208,10 +208,27 @@ typedef struct ATTR_PACKED RedMultiMediaTime { uint32_t time; } RedMultiMediaTime; +enum { + RED_PUBKEY_TYPE_INVALID, + RED_PUBKEY_TYPE_RSA, + RED_PUBKEY_TYPE_RSA2, + RED_PUBKEY_TYPE_DSA, + RED_PUBKEY_TYPE_DSA1, + RED_PUBKEY_TYPE_DSA2, + RED_PUBKEY_TYPE_DSA3, + RED_PUBKEY_TYPE_DSA4, + RED_PUBKEY_TYPE_DH, + RED_PUBKEY_TYPE_EC, +}; + typedef struct ATTR_PACKED RedMigrationBegin { uint16_t port; uint16_t sport; - char host[0]; + uint32_t host_offset; + uint32_t host_size; + uint16_t pub_key_type; + uint32_t pub_key_offset; + uint32_t pub_key_size; } RedMigrationBegin; enum { diff --git a/server/reds.c b/server/reds.c index 38a65387..ddf1fe46 100644 --- a/server/reds.c +++ b/server/reds.c @@ -60,9 +60,10 @@ static VDIPortInterface *vdagent = NULL; #define MIGRATION_NOTIFY_SPICE_KEY "spice_mig_ext" -#define REDS_MIG_VERSION 1 +#define REDS_MIG_VERSION 3 #define REDS_MIG_CONTINUE 1 #define REDS_MIG_ABORT 2 +#define REDS_MIG_DIFF_VERSION 3 #define REDS_AGENT_WINDOW_SIZE 10 #define REDS_TOKENS_TO_SEND 5 @@ -276,6 +277,7 @@ typedef struct RedsState { uint32_t ping_id; uint32_t net_test_id; int net_test_stage; + int peer_minor_version; } RedsState; uint64_t bitrate_per_sec = ~0; @@ -2846,6 +2848,8 @@ static void reds_handle_read_header_done(void *opaque) return; } + reds->peer_minor_version = header->minor_version; + if (header->size < sizeof(RedLinkMess)) { reds_send_link_error(link, RED_ERR_INVALID_DATA); red_printf("bad size %u", header->size); @@ -4033,12 +4037,20 @@ typedef struct RedsMigSpice { char *host; int port; int sport; + uint16_t cert_pub_key_type; + uint32_t cert_pub_key_len; + uint8_t* cert_pub_key; } RedsMigSpice; typedef struct RedsMigSpiceMessage { uint32_t link_id; } RedsMigSpiceMessage; +typedef struct RedsMigCertPubKeyInfo { + uint16_t type; + uint32_t len; +} RedsMigCertPubKeyInfo; + static int reds_mig_actual_read(RedsMigSpice *s) { for (;;) { @@ -4147,7 +4159,9 @@ static void reds_mig_continue(RedsMigSpice *s) red_printf(""); core->set_file_handlers(core, s->fd, NULL, NULL, NULL); host_len = strlen(s->host) + 1; - if (!(item = new_simple_out_item(RED_MIGRATE_BEGIN, sizeof(RedMigrationBegin) + host_len))) { + item = new_simple_out_item(RED_MIGRATE_BEGIN, + sizeof(RedMigrationBegin) + host_len + s->cert_pub_key_len); + if (!(item)) { red_printf("alloc item failed"); reds_disconnect(); return; @@ -4155,7 +4169,13 @@ static void reds_mig_continue(RedsMigSpice *s) migrate = (RedMigrationBegin *)item->data; migrate->port = s->port; migrate->sport = s->sport; - memcpy(migrate->host, s->host, host_len); + migrate->host_offset = sizeof(RedMigrationBegin); + migrate->host_size = host_len; + migrate->pub_key_type = s->cert_pub_key_type; + migrate->pub_key_offset = sizeof(RedMigrationBegin) + host_len; + migrate->pub_key_size = s->cert_pub_key_len; + memcpy((uint8_t*)(migrate) + migrate->host_offset , s->host, host_len); + memcpy((uint8_t*)(migrate) + migrate->pub_key_offset, s->cert_pub_key, s->cert_pub_key_len); reds_push_pipe_item(&item->base); free(s->local_args); @@ -4220,6 +4240,68 @@ static void reds_mig_send_ticket(RedsMigSpice *s) BIO_free(bio_key); } +static void reds_mig_receive_cert_public_key(RedsMigSpice *s) +{ + s->cert_pub_key = malloc(s->cert_pub_key_len); + if (!s->cert_pub_key) { + red_printf("alloc failed"); + reds_mig_failed(s); + return; + } + + memcpy(s->cert_pub_key, s->read.buf, s->cert_pub_key_len); + + s->read.size = RED_TICKET_PUBKEY_BYTES; + s->read.end_pos = 0; + s->read.handle_data = reds_mig_send_ticket; + + core->set_file_handlers(core, s->fd, reds_mig_read, NULL, s); +} + +static void reds_mig_receive_cert_public_key_info(RedsMigSpice *s) +{ + RedsMigCertPubKeyInfo* pubkey_info = (RedsMigCertPubKeyInfo*)s->read.buf; + s->cert_pub_key_type = pubkey_info->type; + s->cert_pub_key_len = pubkey_info->len; + + if (s->cert_pub_key_len > RECIVE_BUF_SIZE) { + red_printf("certificate public key length exceeds buffer size"); + reds_mig_failed(s); + return; + } + + if (s->cert_pub_key_len) { + s->read.size = s->cert_pub_key_len; + s->read.end_pos = 0; + s->read.handle_data = reds_mig_receive_cert_public_key; + } else { + s->cert_pub_key = NULL; + s->read.size = RED_TICKET_PUBKEY_BYTES; + s->read.end_pos = 0; + s->read.handle_data = reds_mig_send_ticket; + } + + core->set_file_handlers(core, s->fd, reds_mig_read, NULL, s); +} + +static void reds_mig_handle_send_abort_done(RedsMigSpice *s) +{ + reds_mig_failed(s); +} + +static void reds_mig_receive_version(RedsMigSpice *s) +{ + uint32_t* dest_version; + uint32_t resault; + dest_version = (uint32_t*)s->read.buf; + resault = REDS_MIG_ABORT; + memcpy(s->write.buf, &resault, sizeof(resault)); + s->write.length = sizeof(resault); + s->write.now = s->write.buf; + s->write.handle_done = reds_mig_handle_send_abort_done; + core->set_file_handlers(core, s->fd, reds_mig_write, reds_mig_write, s); +} + static void reds_mig_control(RedsMigSpice *spice_migration) { uint32_t *control; @@ -4229,9 +4311,9 @@ static void reds_mig_control(RedsMigSpice *spice_migration) switch (*control) { case REDS_MIG_CONTINUE: - spice_migration->read.size = RED_TICKET_PUBKEY_BYTES; + spice_migration->read.size = sizeof(RedsMigCertPubKeyInfo); spice_migration->read.end_pos = 0; - spice_migration->read.handle_data = reds_mig_send_ticket; + spice_migration->read.handle_data = reds_mig_receive_cert_public_key_info; core->set_file_handlers(core, spice_migration->fd, reds_mig_read, NULL, spice_migration); @@ -4240,6 +4322,15 @@ static void reds_mig_control(RedsMigSpice *spice_migration) red_printf("abort"); reds_mig_failed(spice_migration); break; + case REDS_MIG_DIFF_VERSION: + red_printf("different versions"); + spice_migration->read.size = sizeof(uint32_t); + spice_migration->read.end_pos = 0; + spice_migration->read.handle_data = reds_mig_receive_version; + + core->set_file_handlers(core, spice_migration->fd, reds_mig_read, + NULL, spice_migration); + break; default: red_printf("invalid control"); reds_mig_failed(spice_migration); @@ -4281,6 +4372,12 @@ static void reds_mig_started(void *opaque, const char *in_args) goto error; } + if ((RED_VERSION_MAJOR == 1) && (reds->peer_minor_version < 1)) { + red_printf("minor version mismatch client %u server %u", + reds->peer_minor_version, RED_VERSION_MINOR); + goto error; + } + spice_migration = (RedsMigSpice *)malloc(sizeof(RedsMigSpice)); if (!spice_migration) { red_printf("Could not allocate memory for spice migration structure"); @@ -4483,6 +4580,85 @@ static void reds_mig_write_all(int fd, void *buf, int len, const char *name) } } +static void reds_mig_send_cert_public_key(int fd) +{ + FILE* cert_file; + X509* x509; + EVP_PKEY* pub_key; + unsigned char* pp = NULL; + int length; + BIO* mem_bio; + RedsMigCertPubKeyInfo pub_key_info_msg; + + if (spice_secure_port == -1) { + pub_key_info_msg.type = RED_PUBKEY_TYPE_INVALID; + pub_key_info_msg.len = 0; + reds_mig_write_all(fd, &pub_key_info_msg, sizeof(pub_key_info_msg), "cert public key info"); + return; + } + + cert_file = fopen(ssl_parameters.certs_file, "r"); + if (!cert_file) { + red_error("opening certificate failed"); + } + + x509 = PEM_read_X509_AUX(cert_file, NULL, NULL, NULL); + if (!x509) { + red_error("reading x509 cert failed"); + } + pub_key = X509_get_pubkey(x509); + if (!pub_key) { + red_error("reading public key failed"); + } + + mem_bio = BIO_new(BIO_s_mem()); + i2d_PUBKEY_bio(mem_bio, pub_key); + if (BIO_flush(mem_bio) != 1) { + red_error("bio flush failed"); + } + length = BIO_get_mem_data(mem_bio, &pp); + + switch(pub_key->type) { + case EVP_PKEY_RSA: + pub_key_info_msg.type = RED_PUBKEY_TYPE_RSA; + break; + case EVP_PKEY_RSA2: + pub_key_info_msg.type = RED_PUBKEY_TYPE_RSA2; + break; + case EVP_PKEY_DSA: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DSA; + break; + case EVP_PKEY_DSA1: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DSA1; + break; + case EVP_PKEY_DSA2: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DSA2; + break; + case EVP_PKEY_DSA3: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DSA3; + break; + case EVP_PKEY_DSA4: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DSA4; + break; + case EVP_PKEY_DH: + pub_key_info_msg.type = RED_PUBKEY_TYPE_DH; + break; + case EVP_PKEY_EC: + pub_key_info_msg.type = RED_PUBKEY_TYPE_EC; + break; + default: + red_error("invalid public key type"); + } + pub_key_info_msg.len = length; + reds_mig_write_all(fd, &pub_key_info_msg, sizeof(pub_key_info_msg), "cert public key info"); + reds_mig_write_all(fd, pp, length, "cert public key"); + + BIO_free(mem_bio); + fclose(cert_file); + EVP_PKEY_free(pub_key); + X509_free(x509); +} + static void reds_mig_recv(void *opaque, int fd) { uint32_t ack_message = *(uint32_t *)"ack_"; @@ -4497,16 +4673,36 @@ static void reds_mig_recv(void *opaque, int fd) BUF_MEM *buff; reds_mig_read_all(fd, &version, sizeof(version), "version"); - - if (version != REDS_MIG_VERSION) { + // starting from version 3, if the version of the src is bigger + // than ours, we send our version to the src. + if (version < REDS_MIG_VERSION) { resault = REDS_MIG_ABORT; reds_mig_write_all(fd, &resault, sizeof(resault), "resault"); mig->notifier_done(mig, reds->mig_notifier); return; + } else if (version > REDS_MIG_VERSION) { + uint32_t src_resault; + uint32_t self_version = REDS_MIG_VERSION; + resault = REDS_MIG_DIFF_VERSION; + reds_mig_write_all(fd, &resault, sizeof(resault), "resault"); + reds_mig_write_all(fd, &self_version, sizeof(self_version), "dest-version"); + reds_mig_read_all(fd, &src_resault, sizeof(src_resault), "src resault"); + + if (src_resault == REDS_MIG_ABORT) { + red_printf("abort (response to REDS_MIG_DIFF_VERSION)"); + mig->notifier_done(mig, reds->mig_notifier); + return; + } else if (src_resault != REDS_MIG_CONTINUE) { + red_printf("invalid response to REDS_MIG_DIFF_VERSION"); + mig->notifier_done(mig, reds->mig_notifier); + return; + } + } else { + resault = REDS_MIG_CONTINUE; + reds_mig_write_all(fd, &resault, sizeof(resault), "resault"); } - resault = REDS_MIG_CONTINUE; - reds_mig_write_all(fd, &resault, sizeof(resault), "resault"); + reds_mig_send_cert_public_key(fd); ticketing_info.bn = BN_new(); if (!ticketing_info.bn) { |