diff options
author | Christophe Fergeau <cfergeau@redhat.com> | 2014-05-28 12:45:59 +0200 |
---|---|---|
committer | Christophe Fergeau <cfergeau@redhat.com> | 2014-08-13 15:32:06 +0200 |
commit | 07814eedb7c002ced35a6699db5f617dccb5498c (patch) | |
tree | 50931c66785c88ecf9c4fe0517ab27235a0b3577 | |
parent | b26c5dbc389de696a91a2ccbd40e81773fe7e63b (diff) |
GNUTLS
-rw-r--r-- | gtk/spice-channel-priv.h | 1 | ||||
-rw-r--r-- | gtk/spice-channel.c | 324 |
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); |