From 248003f3f1b631bed332e09b68ff1c1312e1db43 Mon Sep 17 00:00:00 2001 From: Michael Catanzaro Date: Fri, 7 Aug 2020 18:37:35 -0500 Subject: [PATCH] gnutls: download missing intermediate certificates at runtime This is slow. Don't misconfigure your server by failing to include required certificates if you don't want your website to be slow. Note that since we're already on a dedicated handshake thread if called from GTlsConnection, we can do sync I/O throughout, no worries. Fixes #96 --- tls/gnutls/gtlsdatabase-gnutls.c | 137 +++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tls/gnutls/gtlsdatabase-gnutls.c b/tls/gnutls/gtlsdatabase-gnutls.c index 3825a351..85b771fb 100644 --- a/tls/gnutls/gtlsdatabase-gnutls.c +++ b/tls/gnutls/gtlsdatabase-gnutls.c @@ -66,6 +66,9 @@ typedef struct * string handles. This array is populated on demand. */ GHashTable *handles; + + /* Unowned. This is only set temporarily, during certificate verification. */ + GCancellable *verify_chain_cancellable; } GTlsDatabaseGnutlsPrivate; static void g_tls_database_gnutls_initable_interface_init (GInitableIface *iface); @@ -220,6 +223,8 @@ g_tls_database_gnutls_finalize (GObject *object) GTlsDatabaseGnutls *self = G_TLS_DATABASE_GNUTLS (object); GTlsDatabaseGnutlsPrivate *priv = g_tls_database_gnutls_get_instance_private (self); + g_assert (!priv->verify_chain_cancellable); + g_clear_pointer (&priv->subjects, g_hash_table_destroy); g_clear_pointer (&priv->issuers, g_hash_table_destroy); g_clear_pointer (&priv->complete, g_hash_table_destroy); @@ -496,10 +501,13 @@ g_tls_database_gnutls_verify_chain (GTlsDatabase *database, if (g_cancellable_set_error_if_cancelled (cancellable, error)) return G_TLS_CERTIFICATE_GENERIC_ERROR; + g_assert (!priv->verify_chain_cancellable); + priv->verify_chain_cancellable = cancellable; gnutls_chain = convert_certificate_chain_to_gnutls (G_TLS_CERTIFICATE_GNUTLS (chain)); gerr = gnutls_x509_trust_list_verify_crt (priv->trust_list, gnutls_chain->chain, gnutls_chain->length, 0, &gnutls_result, NULL); + priv->verify_chain_cancellable = NULL; if (gerr != 0 || g_cancellable_set_error_if_cancelled (cancellable, error)) { @@ -575,6 +583,131 @@ g_tls_database_gnutls_populate_trust_list (GTlsDatabaseGnutls *self, return gerr >= 0; } +#if GNUTLS_VERSION_MAJOR > 3 || GNUTLS_VERSION_MAJOR == 3 && GNUTLS_VERSION_MINOR >= 7 +static int +issuer_missing_cb (gnutls_x509_trust_list_t tlist, + const gnutls_x509_crt_t crt, + gnutls_x509_crt_t **issuers, + guint *issuers_size) +{ + GTlsDatabaseGnutls *self = gnutls_x509_trust_list_get_ptr (tlist); + GTlsDatabaseGnutlsPrivate *priv = g_tls_database_gnutls_get_instance_private (self); + gnutls_datum_t datum; + GFile *file = NULL; + GFileInputStream *istream = NULL; + char *aia = NULL; + char *scheme = NULL; + int gerr; + int ret = -1; + guchar buffer[2048]; + gssize n_read; + GByteArray *der = NULL; + GError *error = NULL; + + /* The server sent an incomplete certificate chain, but we may be able to + * download the missing certificate to allow verification to proceed. See + * Authority Information Access, RFC 5280 ยง4.2.2.1. Also see: + * https://blogs.gnome.org/mcatanzaro/2015/01/30/mozilla-is-responsible-for-the-redhat-corpmerchandise-com-fiasco/ + */ + + for (int i = 0; ; i++) + { + gerr = gnutls_x509_crt_get_authority_info_access (crt, i, GNUTLS_IA_CAISSUERS_URI, &datum, NULL); + if (gerr == GNUTLS_E_UNKNOWN_ALGORITHM) + continue; + + if (gerr == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + return -1; + + if (gerr < 0) + { + g_warning ("Failed to read Authority Information Access from certificate: %s", gnutls_strerror (gerr)); + return -1; + } + + /* Success */ + break; + } + g_assert (gerr == GNUTLS_E_SUCCESS); + + aia = g_malloc0 (datum.size + 1); + memcpy (aia, datum.data, datum.size); + + if (!g_uri_is_valid (aia, G_URI_FLAGS_NONE, &error)) + { + g_warning ("Authority Information Access URI %s is not a valid URI: %s", aia, error->message); + goto out; + } + + /* We support only HTTP. Notably, HTTPS is not supported because (a) it is + * not specified by RFC 5280, and (b) since we have no way to break a + * recursive loop if the connection to retrieve the certificate itself also + * requires a missing certificate. We could easily support FTP, but we don't, + * because that's silly. Also note that we don't support "certs-only" CMS + * messages, we only support directly retrieving a DER certificate. Finally, + * we don't support the case where accessLocation is a directoryName, so no + * private DAP or LDAP. + */ + scheme = g_uri_parse_scheme (aia); + if (!scheme || strcmp (scheme, "http") != 0) + { + g_warning ("Authority Information Access URI %s uses unsupported URI scheme '%s'", scheme, aia); + goto out; + } + + file = g_file_new_for_uri (aia); + istream = g_file_read (file, priv->verify_chain_cancellable, &error); + if (!istream) + { + g_warning ("Failed to download missing issuer certificate from Authority Information Access URI %s: failed g_file_read (do you need to install gvfs?): %s", + aia, error->message); + goto out; + } + + der = g_byte_array_sized_new (sizeof (buffer)); + do + { + n_read = g_input_stream_read (G_INPUT_STREAM (istream), buffer, sizeof (buffer), + priv->verify_chain_cancellable, &error); + if (n_read == -1) + { + g_warning ("Failed to download missing issuer certificate from Authority Information Access URI %s: failed g_input_stream_read: %s", + aia, error->message); + goto out; + } + g_byte_array_append (der, buffer, n_read); + } while (n_read > 0); + + gnutls_free (datum.data); + datum.size = der->len; + datum.data = (unsigned char *)g_byte_array_free (der, FALSE); + der = NULL; + + gerr = gnutls_x509_crt_list_import2 (issuers, issuers_size, &datum, GNUTLS_X509_FMT_DER, 0); + if (gerr < 0) + { + g_warning ("Failed to download missing issuer certificate from Authority Information Access URI %s: failed gnutls_x509_crt_import: %s", + aia, gnutls_strerror (gerr)); + goto out; + } + + ret = 0; + +out: + if (error) + g_error_free (error); + if (file) + g_object_unref (file); + if (istream) + g_object_unref (istream); + if (der) + g_byte_array_unref (der); + gnutls_free (datum.data); + g_free (aia); + return ret; +} +#endif + static void g_tls_database_gnutls_class_init (GTlsDatabaseGnutlsClass *klass) { @@ -610,6 +743,10 @@ g_tls_database_gnutls_initable_init (GInitable *initable, return FALSE; gnutls_x509_trust_list_init (&trust_list, 0); +#if GNUTLS_VERSION_MAJOR > 3 || GNUTLS_VERSION_MAJOR == 3 && GNUTLS_VERSION_MINOR >= 7 + gnutls_x509_trust_list_set_getissuer_function (trust_list, issuer_missing_cb); + gnutls_x509_trust_list_set_ptr (trust_list, self); +#endif g_assert (G_TLS_DATABASE_GNUTLS_GET_CLASS (self)->populate_trust_list); if (!G_TLS_DATABASE_GNUTLS_GET_CLASS (self)->populate_trust_list (self, trust_list, error)) -- GitLab