summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Fergeau <cfergeau@redhat.com>2014-05-28 12:45:59 +0200
committerChristophe Fergeau <cfergeau@redhat.com>2014-08-13 15:32:06 +0200
commit07814eedb7c002ced35a6699db5f617dccb5498c (patch)
tree50931c66785c88ecf9c4fe0517ab27235a0b3577
parentb26c5dbc389de696a91a2ccbd40e81773fe7e63b (diff)
GNUTLS
-rw-r--r--gtk/spice-channel-priv.h1
-rw-r--r--gtk/spice-channel.c324
2 files changed, 302 insertions, 23 deletions
diff --git a/gtk/spice-channel-priv.h b/gtk/spice-channel-priv.h
index 59f94af..cac2c4e 100644
--- a/gtk/spice-channel-priv.h
+++ b/gtk/spice-channel-priv.h
@@ -82,6 +82,7 @@ struct _SpiceChannelPrivate {
SSL *ssl;
SpiceOpenSSLVerify *sslverify;
gnutls_session_t tls_session;
+ gnutls_certificate_credentials_t tls_ca;
GSocket *sock;
GSocketConnection *conn;
GInputStream *in;
diff --git a/gtk/spice-channel.c b/gtk/spice-channel.c
index 8f5c480..c5d3661 100644
--- a/gtk/spice-channel.c
+++ b/gtk/spice-channel.c
@@ -32,6 +32,7 @@
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
+#include <gnutls/x509.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
@@ -2234,12 +2235,16 @@ static int spice_channel_load_ca(SpiceChannel *channel)
guint size;
const gchar *ca_file;
int rc;
+ gnutls_datum_t ca_data;
g_return_val_if_fail(c->ctx != NULL, 0);
-/* FIXME: reimplement with GNUTLS,
- * cf gnutls_certificate_set_x509_trust_file
- * gnutls_certificate_set_x509_trust_mem */
+ rc = gnutls_certificate_allocate_credentials (&channel->priv->tls_ca);
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_warning("Failed to allocate gnutls CA credentials");
+ return 0;
+ }
+
lookup = X509_STORE_add_lookup(c->ctx->cert_store, &spice_x509_mem_lookup);
ca_file = spice_session_get_ca_file(c->session);
spice_session_get_ca(c->session, &ca, &size);
@@ -2265,10 +2270,24 @@ static int spice_channel_load_ca(SpiceChannel *channel)
}
sk_X509_INFO_pop_free(inf, X509_INFO_free);
+ ca_data.data = ca;
+ ca_data.size = size;
+ rc = gnutls_certificate_set_x509_trust_mem (channel->priv->tls_ca,
+ &ca_data,
+ GNUTLS_X509_FMT_PEM);
+ if (rc != 1)
+ g_warning("loading ca certs from memory failed");
+ else
+ count++;
}
if (ca_file != NULL) {
rc = SSL_CTX_load_verify_locations(c->ctx, ca_file, NULL);
+ // missing error handling is on purpose, transitional
+ rc = gnutls_certificate_set_x509_trust_file (channel->priv->tls_ca,
+ ca_file,
+ GNUTLS_X509_FMT_PEM);
+ //FIXME: may be more than 1 cert in a single file ?
if (rc != 1)
g_warning("loading ca certs from %s failed", ca_file);
else
@@ -2276,12 +2295,27 @@ static int spice_channel_load_ca(SpiceChannel *channel)
}
if (count == 0) {
+ const char ca_bundle[] = "/etc/ssl/certs/ca-bundle.crt";
rc = SSL_CTX_set_default_verify_paths(c->ctx);
+ // missing error handling is on purpose, transitional
+ rc = gnutls_certificate_set_x509_trust_file (channel->priv->tls_ca,
+ ca_bundle,
+ GNUTLS_X509_FMT_PEM);
+ //FIXME: can most likely be bigger than 1 ??
if (rc != 1)
g_warning("loading ca certs from default location failed");
else
count++;
}
+ if (count != 0) {
+ rc = gnutls_credentials_set(c->tls_session, GNUTLS_CRD_CERTIFICATE,
+ c->tls_ca);
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_critical("gnutls_credentials_set failed: %s",
+ gnutls_strerror(rc));
+ return 0;
+ }
+ }
return count;
}
@@ -2398,6 +2432,236 @@ spice_channel_gnutls_eagain_set_giocondition(gnutls_session_t session,
return FALSE;
}
+static char *
+spice_channel_gnutls_certificate_status_to_string(unsigned int status)
+{
+ const char *status_strings[] = {
+ NULL,
+ "GNUTLS_CERT_INVALID",
+ NULL,
+ NULL,
+ NULL,
+ "GNUTLS_CERT_REVOKED",
+ "GNUTLS_CERT_SIGNER_NOT_FOUND",
+ "GNUTLS_CERT_SIGNER_NOT_CA",
+ "GNUTLS_CERT_INSECURE_ALGORITHM",
+ "GNUTLS_CERT_NOT_ACTIVATED",
+ "GNUTLS_CERT_EXPIRED",
+ "GNUTLS_CERT_SIGNATURE_FAILURE",
+ "GNUTLS_CERT_REVOCATION_DATA_SUPERSEDED",
+ NULL,
+ "GNUTLS_CERT_UNEXPECTED_OWNER",
+ "GNUTLS_CERT_REVOCATION_DATA_ISSUED_IN_FUTURE",
+ "GNUTLS_CERT_SIGNER_CONSTRAINTS_FAILURE",
+ "GNUTLS_CERT_MISMATCH",
+ };
+ unsigned int i;
+ GPtrArray *status_strv;
+ char *status_str = NULL;
+
+ if (status == 0) {
+ goto end;
+ }
+
+ status_strv = g_ptr_array_new();
+ for (i = 0; i < G_N_ELEMENTS(status_strings); i++) {
+ if (status & (1 << i)) {
+ if (status_strings[i] != NULL) {
+ g_ptr_array_insert(status_strv, -1, (gpointer)status_strings[i]);
+ } else {
+ g_warning("Unexpected gnutls_certificate_status_t bit set: %d", i);
+ }
+ }
+ }
+ if (status_strv->len == 0) {
+ goto end;
+ }
+ g_ptr_array_insert(status_strv, -1, NULL);
+ status_str = g_strjoinv(", ", (GStrv)status_strv->pdata);
+end:
+ g_ptr_array_free(status_strv, TRUE);
+
+ return status_str;
+}
+
+static void append_string(GArray *strv, const char *start, gsize len)
+{
+ char *str;
+ str = g_strndup(start, len);
+ g_strchug(str);
+ g_array_append_val(strv, str);
+}
+
+static GStrv spice_channel_gnutls_split_rfc4514(const char *str_rfc4514,
+ gsize len)
+{
+ const char *it = str_rfc4514;
+ const char *start = str_rfc4514;
+ GArray *strv = g_array_new(TRUE, FALSE, sizeof(char*));
+
+ if (len == -1) {
+ len = strlen (str_rfc4514);
+ }
+ while (it - str_rfc4514 < len) {
+ if (*it == ',') {
+ append_string(strv, start, it-start);
+ start = it + 1;
+ } else if (*it == '\\') {
+ /* skip escaped character */
+ it++;
+ if (it - str_rfc4514 >= len) {
+ /* invalid string */
+ break;
+ }
+ }
+ it++;
+ }
+
+ append_string(strv, start, it-start);
+
+ unsigned int i;
+ g_print("STRV:\n");
+ for (i = 0; i < g_strv_length((GStrv)strv->data); i++) {
+ g_print("->%s\n", ((GStrv)strv->data)[i]);
+ }
+
+ return (GStrv)g_array_free(strv, FALSE);
+}
+
+
+/* Comparison of --spice-host-subject string and the one returned by
+ * gnutls_x509_crt_get_dn2() is less obvious than it could as the
+ * string passed to --spice-host-subject can contain spaces which needs
+ * to be ignored (this is the format used by
+ * openssl x509 -noout -text -in $server for example). Since the string
+ * returned by GNUTLS does not have these spaces, we need something more
+ * sophisticated than strcmp.
+ */
+static gboolean spice_channel_gnutls_compare_rfc4514(gnutls_datum_t *rhs_datum,
+ gnutls_datum_t *lhs_datum)
+{
+ GStrv rhs;
+ GStrv lhs;
+ gsize rhs_len;
+ gsize lhs_len;
+ unsigned int i;
+ gboolean is_equal = FALSE;
+
+ rhs = spice_channel_gnutls_split_rfc4514((const char *)rhs_datum->data,
+ rhs_datum->size);
+ lhs = spice_channel_gnutls_split_rfc4514((const char *)lhs_datum->data,
+ lhs_datum->size);
+
+ if ((rhs == NULL) || (lhs == NULL)) {
+ is_equal = (rhs == lhs);
+ goto end;
+ }
+
+ rhs_len = g_strv_length(rhs);
+ lhs_len = g_strv_length(lhs);
+
+ if (lhs_len != rhs_len) {
+ goto end;
+ }
+
+ for (i = 0; i < rhs_len; i++) {
+ if (g_strcmp0(rhs[i], lhs[i]) != 0) {
+ goto end;
+ }
+ }
+
+ is_equal = TRUE;
+
+end:
+ g_strfreev(rhs);
+ g_strfreev(lhs);
+
+ return is_equal;
+}
+
+static gboolean
+spice_channel_gnutls_verify_hostname (SpiceChannel *channel)
+{
+ int rc;
+ const char *hostname;
+ gnutls_x509_crt_t cert = NULL;
+ gboolean verified = FALSE;
+ unsigned certslen = 0;
+ const gnutls_datum_t *const certs = gnutls_certificate_get_peers(channel->priv->tls_session,
+ &certslen);
+ gnutls_datum_t cert_subject = { 0, };
+
+ if (certs == NULL || certslen == 0) {
+ g_warning("error: could not obtain peer certificate");
+ goto end;
+ }
+
+ hostname = spice_session_get_host(channel->priv->session);
+ /* FIXME: correct name to pass there??
+ * especially wrt host subject overrides? */
+ /* FIXME move back to before beginning of handshake as this
+ * can be sent to server */
+ rc = gnutls_server_name_set(channel->priv->tls_session,
+ GNUTLS_NAME_DNS,
+ hostname, strlen(hostname));
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_warning("gnutls_server_name_set: %s", gnutls_strerror(rc));
+ goto end;
+ }
+
+ rc = gnutls_x509_crt_init(&cert);
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_warning("error: gnutls_x509_crt_init: %s\n",
+ gnutls_strerror(rc));
+ goto end;
+ }
+
+ rc = gnutls_x509_crt_import(cert, certs, GNUTLS_X509_FMT_DER);
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_warning("error: gnutls_x509_crt_import: %s\n",
+ gnutls_strerror(rc));
+ goto end;
+ }
+
+ rc = gnutls_x509_crt_check_hostname(cert, hostname);
+ if (rc == 0) {
+ const char *subject;
+ gnutls_datum_t subject_datum;
+
+ subject = spice_session_get_cert_subject(channel->priv->session);
+ if (subject == NULL) {
+ g_warning("error: host name does not match certificate\n");
+ goto end;
+ }
+
+ rc = gnutls_x509_crt_get_dn2(cert, &cert_subject);
+ if (rc != 0) {
+ g_warning("gnutls_x509_crt_get_dn2 failed: %s",
+ gnutls_strerror(rc));
+ goto end;
+ }
+ g_warning("got subject!! %d %s", cert_subject.size, (char *)cert_subject.data);
+ subject_datum.data = (unsigned char *)subject;
+ subject_datum.size = strlen(subject);
+ if (!spice_channel_gnutls_compare_rfc4514(&cert_subject, &subject_datum)) {
+ g_warning("Cert subjects do not match: '%s' != '%s'",
+ cert_subject.data, subject);
+ goto end;
+ }
+ }
+ g_warning("verification was fine");
+ verified = TRUE;
+
+end:
+ if (cert_subject.data != NULL) {
+ gnutls_free(cert_subject.data);
+ }
+ if (cert != NULL) {
+ gnutls_x509_crt_deinit(cert);
+ }
+
+ return verified;
+}
/* coroutine context */
static void *spice_channel_coroutine(void *data)
@@ -2468,6 +2732,8 @@ reconnect:
SSL_CTX_set_options(c->ctx, ssl_options);
+ /* FIXME: if spice_channel_load_ca is not called, the connection
+ * will have no trusted CAs */
/* FIXME: could be done as soon as the options are set on SpiceSession
* since with GNUTLS the truststore is global */
verify = spice_session_get_verify(c->session);
@@ -2509,15 +2775,6 @@ reconnect:
}
}
- rc = gnutls_credentials_set(c->tls_session, GNUTLS_CRD_CERTIFICATE,
- cred);
- if (rc != GNUTLS_E_SUCCESS) {
- g_critical("gnutls_credentials_set failed: %s",
- gnutls_strerror(rc));
- emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
- goto cleanup;
- }
-
c->ssl = SSL_new(c->ctx);
if (c->ssl == NULL) {
g_critical("SSL_new failed");
@@ -2540,17 +2797,6 @@ reconnect:
gnutls_transport_set_pull_function(c->tls_session,
spice_channel_gnutls_read);
- /* FIXME: correct name to pass there??
- * especially wrt host subject overrides? */
- rc = gnutls_server_name_set(c->tls_session, GNUTLS_NAME_DNS,
- spice_session_get_host(c->session),
- strlen(spice_session_get_host(c->session)));
- if (rc != GNUTLS_E_SUCCESS) {
- g_critical("gnutls_server_name_set: %s", gnutls_strerror(rc));
- emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
- goto cleanup;
- }
-
{
guint8 *pubkey;
guint pubkey_len;
@@ -2580,6 +2826,38 @@ handshake_resume:
goto cleanup;
}
/* FIXME: certificate verification */
+ unsigned int status = -1;
+ rc = gnutls_certificate_verify_peers2(c->tls_session, &status);
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_warning("gnutls_certificate_verify_peers2 failed: %s",
+ gnutls_strerror(rc));
+ emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
+ goto cleanup;
+ }
+ if (status != 0) {
+ char *status_str = spice_channel_gnutls_certificate_status_to_string(status);
+ g_warning("certificate validation failed (0x%x): %s", status, status_str);
+ g_free(status_str);
+ emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
+ goto cleanup;
+ }
+
+ if (!spice_channel_gnutls_verify_hostname (channel)) {
+ g_critical("spice_channel_gnutls_verify_hostname failed");
+ emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
+ goto cleanup;
+ }
+ /* FIXME: correct name to pass there??
+ * especially wrt host subject overrides? */
+ rc = gnutls_server_name_set(c->tls_session, GNUTLS_NAME_DNS,
+ spice_session_get_host(c->session),
+ strlen(spice_session_get_host(c->session)));
+ if (rc != GNUTLS_E_SUCCESS) {
+ g_critical("gnutls_server_name_set: %s", gnutls_strerror(rc));
+ emit_main_context(channel, SPICE_CHANNEL_EVENT, SPICE_CHANNEL_ERROR_TLS);
+ goto cleanup;
+ }
+
#if 0
ssl_reconnect:
rc = SSL_connect(c->ssl);