summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYonit Halperin <yhalperi@redhat.com>2009-12-30 16:07:14 +0200
committerYaniv Kamay <ykamay@redhat.com>2010-01-06 16:06:46 +0200
commite38a61900711169d66b1fa7e117b04d49106a1da (patch)
tree15765c0bb0bcf7b8133203340350c09902cec541
parent54a8e5027093baa1c847b43f2fb08bea93e0ed67 (diff)
downloadspice-e38a61900711169d66b1fa7e117b04d49106a1da.tar.gz
spice-e38a61900711169d66b1fa7e117b04d49106a1da.tar.xz
spice-e38a61900711169d66b1fa7e117b04d49106a1da.zip
server,client: server authentication for secured channels. #527411 #549673.
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.cpp44
-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, 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) {