summaryrefslogtreecommitdiffstats
path: root/client/red_peer.cpp
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 /client/red_peer.cpp
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/
Diffstat (limited to 'client/red_peer.cpp')
-rw-r--r--client/red_peer.cpp401
1 files changed, 386 insertions, 15 deletions
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) {