summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYonit Halperin <yhalperi@redhat.com>2010-01-10 09:48:38 +0200
committerYaniv Kamay <ykamay@redhat.com>2010-01-11 19:10:54 +0200
commit3eae1c80d97ce6d1e0eee69f7454973abdb94ef6 (patch)
treef8e88491e6ea9d4be7feb7e03ed1349325d7983a
parentdcf326cfd523c135bd0be8f9a4bc2da6c78b2d23 (diff)
downloadspice-3eae1c80d97ce6d1e0eee69f7454973abdb94ef6.tar.gz
spice-3eae1c80d97ce6d1e0eee69f7454973abdb94ef6.tar.xz
spice-3eae1c80d97ce6d1e0eee69f7454973abdb94ef6.zip
server,client: server authentication for secured channels.
3 available mechanisms: by public key, by host name, and by certificate subject name. In the former method, chain of trust verification is not performed. The CA certificate files are looked for under <spice-config-dir>/spice_truststore.pem windows <spice-config-dir>=%APPDATA%\spicec\ linux <spice-config-dir>=$HOME/.spicec/
-rw-r--r--client/application.cpp10
-rw-r--r--client/application.h2
-rw-r--r--client/platform.h2
-rw-r--r--client/red_channel.cpp17
-rw-r--r--client/red_channel.h6
-rw-r--r--client/red_client.cpp43
-rw-r--r--client/red_client.h3
-rw-r--r--client/red_peer.cpp401
-rw-r--r--client/red_peer.h41
-rw-r--r--client/windows/platform.cpp19
-rw-r--r--client/x11/platform.cpp14
-rw-r--r--common/red.h21
-rw-r--r--server/reds.c214
13 files changed, 741 insertions, 52 deletions
diff --git a/client/application.cpp b/client/application.cpp
index ac5877c4..d4fe59f6 100644
--- a/client/application.cpp
+++ b/client/application.cpp
@@ -51,6 +51,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
@@ -1818,6 +1820,11 @@ bool Application::process_cmd_line(int argc, char** argv)
_peer_con_opt[i] = 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;
@@ -1836,12 +1843,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 5fa93358..e1c702db 100644
--- a/client/application.h
+++ b/client/application.h
@@ -187,6 +187,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;}
@@ -280,6 +281,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 1b4ef435..87a44bb5 100644
--- a/client/platform.h
+++ b/client/platform.h
@@ -48,6 +48,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 c7dce015..25e4e04d 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().c_str(),
_client.get_password().c_str());
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 e040f63f..57ec3502 100644
--- a/client/red_client.cpp
+++ b/client/red_client.cpp
@@ -23,6 +23,15 @@
#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;
class MouseModeEvent: public Event {
public:
@@ -156,18 +165,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;
@@ -199,9 +209,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() < 2)) {
+ 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;
@@ -433,6 +458,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 a98d2d2f..7cc700eb 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;
@@ -153,6 +154,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;}
@@ -222,6 +224,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 79409ed1..0fd60242 100644
--- a/client/red_peer.cpp
+++ b/client/red_peer.cpp
@@ -16,16 +16,26 @@
*/
#include "common.h"
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
#include "red.h"
#include "red_peer.h"
#include "utils.h"
#include "debug.h"
#include "platform_utils.h"
+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()
@@ -80,13 +90,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);
@@ -120,15 +132,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)
{
- connect_unsecure(host_by_name(host), port);
+ /* 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;
}
-// todo: use SSL_CTX_set_cipher_list, SSL_CTX_load_verify_location etc.
-void RedPeer::connect_secure(const ConnectionOptions& options, uint32_t ip)
+/*
+ * 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)
{
- connect_unsecure(ip, options.secure_port);
+ 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)
+{
+ 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, etc.
+void RedPeer::connect_secure(const ConnectionOptions& options, const char* host)
+{
+ int return_code;
+ int auth_flags;
+ SslVerifyCbData auth_data;
+
+ connect_unsecure(host, options.secure_port);
ASSERT(_ctx == NULL && _ssl == NULL && _peer != INVALID_SOCKET);
try {
@@ -137,12 +483,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");
@@ -154,10 +527,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 (...) {
@@ -167,11 +543,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 2a53eae8..8a4060df 100644
--- a/client/red_peer.h
+++ b/client/red_peer.h
@@ -37,6 +37,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:
@@ -47,10 +71,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)
{
}
@@ -70,12 +96,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();
@@ -95,6 +119,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 97c6dfc4..f332fac0 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;
@@ -433,6 +437,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 77025b6c..4a1f9b23 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;
@@ -1911,6 +1913,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 dfd85126..22c0ec65 100644
--- a/common/red.h
+++ b/common/red.h
@@ -46,7 +46,7 @@
#define RED_MAGIC (*(uint32_t*)"REDQ")
#define RED_VERSION_MAJOR (~(uint32_t)0 - 1)
-#define RED_VERSION_MINOR 1
+#define RED_VERSION_MINOR 2
// Encryption & Ticketing Parameters
#define RED_MAX_PASSWORD_LENGTH 60
@@ -209,10 +209,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 8a2700c6..fb26901a 100644
--- a/server/reds.c
+++ b/server/reds.c
@@ -61,9 +61,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
@@ -278,6 +279,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;
@@ -2908,6 +2910,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);
@@ -4175,12 +4179,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 (;;) {
@@ -4289,7 +4301,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;
@@ -4297,7 +4311,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);
@@ -4362,6 +4382,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;
@@ -4371,9 +4453,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);
@@ -4382,6 +4464,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);
@@ -4423,6 +4514,12 @@ static void reds_mig_started(void *opaque, const char *in_args)
goto error;
}
+ if ((RED_VERSION_MAJOR == 1) && (reds->peer_minor_version < 2)) {
+ 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");
@@ -4625,6 +4722,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_";
@@ -4639,16 +4815,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) {