[balsa] Display S/MIME and TLS certificate chains



commit 9538ac0add6e28e50ccaf97331a16442b54e6754
Author: Peter Bloomfield <PeterBloomfield bellsouth net>
Date:   Thu Nov 14 19:54:12 2019 -0500

    Display S/MIME and TLS certificate chains
    
    The patch addresses the following:
    
    S/MIME signatures:
    Currently, Balsa shows the certificate data and the issuer's name, serial
    and chain id.  It is not possible to view the entire certification chain.
    The patch adds a button to the issuer section of a S/MIME signature,
    which opens a new dialogue, showing the certification tree in the
    upper and the details of the selected certificate in the lower part.
    I.e. it is now possible to inspect/verify the whole tree, up to the
    root certificate.  If you have any S/MIME signed messages in a mailbox,
    you can simply test this feature.
    
    TLS:
    When opening an encrypted SMTP, POP3 or IMAP connection, balsa shows
    a dialogue with the untrusted certificate, asking whether the user
    accepts or rejects it.  With the patch, if the untrusted certificate is
    not self-signed and the issuer certificate(s) can be loaded, the whole
    chain is displayed as above for inspection.  For testing, you could
    temporarily disable the trust for your provider's root certificate,
    which should pop up the modified dialogue.
    
    * libbalsa/Makefile.am, libbalsa/meson.build: add new source and header file
    * libbalsa/libbalsa-gpgme-widgets.[ch]: add button to S/MIME
      signature widget; implement button callback for displaying the
      certificate chain
    * libbalsa/libbalsa.c: replace printf() by g_debug()
      calls in ask_idle() and libbalsa_ask(); use new api for
      creating certificate (chain) widget; delete stuff shifted to
      x509-cert-widget.[ch]
    * libbalsa/x509-cert-widget.[ch]: implement two functions for
      creating a certificate (chain) widget either from the S/MIME
      certificate fingerprint, or from a GTlsCertificate.  If the
      passed certificate is self-signed or if the issuer cannot
      be determined, the function returns a widget containing the
      certificate information.  Otherwise, the returned widget is a
      vertical GtkBox, containing the certificate chain tree view in
      the upper and a GtkStack in the lower part.  The latter displays
      the certificate selected in the tree view.
    * src/save-restore.c: add certificate chain dialogue to the
      geometry manager

 ChangeLog                         |  47 ++++
 libbalsa/Makefile.am              |   4 +-
 libbalsa/libbalsa-gpgme-widgets.c |  43 ++-
 libbalsa/libbalsa-gpgme-widgets.h |   3 +
 libbalsa/libbalsa.c               | 188 +-------------
 libbalsa/meson.build              |   5 +-
 libbalsa/x509-cert-widget.c       | 532 ++++++++++++++++++++++++++++++++++++++
 libbalsa/x509-cert-widget.h       |  58 +++++
 src/save-restore.c                |   1 +
 9 files changed, 703 insertions(+), 178 deletions(-)
---
diff --git a/ChangeLog b/ChangeLog
index ca175abbc..adc14db01 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,50 @@
+2019-11-14  Albrecht Dreß  <albrecht dress arcor de>
+
+       Display S/MIME and TLS certificate chains
+
+       The patch addresses the following:
+
+       S/MIME signatures:
+       Currently, Balsa shows the certificate data and the issuer's
+       name, serial and chain id.  It is not possible to view the entire
+       certification chain.
+       The patch adds a button to the issuer section of a S/MIME
+       signature, which opens a new dialogue, showing the certification
+       tree in the upper and the details of the selected certificate in
+       the lower part.  I.e. it is now possible to inspect/verify the
+       whole tree, up to the root certificate.  If you have any S/MIME
+       signed messages in a mailbox, you can simply test this feature.
+
+       TLS:
+       When opening an encrypted SMTP, POP3 or IMAP connection, balsa
+       shows a dialogue with the untrusted certificate, asking whether
+       the user accepts or rejects it.
+       With the patch, if the untrusted certificate is not self-signed
+       and the issuer certificate(s) can be loaded, the whole chain
+       is displayed as above for inspection.  For testing, you
+       could temporarily disable the trust for your provider's root
+       certificate, which should pop up the modified dialogue.
+
+       * libbalsa/Makefile.am, libbalsa/meson.build: add new source and header file
+       * libbalsa/libbalsa-gpgme-widgets.[ch]: add button to S/MIME
+       signature widget; implement button callback for displaying the
+       certificate chain
+       * libbalsa/libbalsa.c: replace printf() by g_debug()
+       calls in ask_idle() and libbalsa_ask(); use new api for
+       creating certificate (chain) widget; delete stuff shifted to
+       x509-cert-widget.[ch]
+       * libbalsa/x509-cert-widget.[ch]: implement two functions for
+       creating a certificate (chain) widget either from the S/MIME
+       certificate fingerprint, or from a GTlsCertificate.  If the
+       passed certificate is self-signed or if the issuer cannot
+       be determined, the function returns a widget containing the
+       certificate information.  Otherwise, the returned widget is a
+       vertical GtkBox, containing the certificate chain tree view in
+       the upper and a GtkStack in the lower part.  The latter displays
+       the certificate selected in the tree view.
+       * src/save-restore.c: add certificate chain dialogue to the
+       geometry manager
+
 2019-11-13  Peter Bloomfield  <pbloomfield bellsouth net>
 
        meson.build: Use dependency()'s capabilities
diff --git a/libbalsa/Makefile.am b/libbalsa/Makefile.am
index 7094d0e00..845cd1990 100644
--- a/libbalsa/Makefile.am
+++ b/libbalsa/Makefile.am
@@ -134,7 +134,9 @@ libbalsa_a_SOURCES =                \
        url.c                   \
        url.h                   \
        geometry-manager.c      \
-       geometry-manager.h
+       geometry-manager.h      \
+       x509-cert-widget.c      \
+       x509-cert-widget.h
 
 
 EXTRA_DIST =                           \
diff --git a/libbalsa/libbalsa-gpgme-widgets.c b/libbalsa/libbalsa-gpgme-widgets.c
index 88d678b93..70112e80b 100644
--- a/libbalsa/libbalsa-gpgme-widgets.c
+++ b/libbalsa/libbalsa-gpgme-widgets.c
@@ -25,6 +25,7 @@
 #include <glib/gi18n.h>
 
 #include "geometry-manager.h"
+#include "x509-cert-widget.h"
 #include "rfc3156.h"
 
 
@@ -66,6 +67,8 @@ static gchar *create_purpose_str(gboolean can_sign,
        G_GNUC_WARN_UNUSED_RESULT;
 static gchar *create_subkey_type_str(gpgme_subkey_t subkey)
        G_GNUC_WARN_UNUSED_RESULT;
+static void smime_show_chain(GtkWidget              *button,
+                                                        gpointer G_GNUC_UNUSED  user_data);
 
 
 /* documentation: see header file */
@@ -158,7 +161,15 @@ libbalsa_gpgme_key(const gpgme_key_t     key,
                        issuer_row = create_key_grid_row(GTK_GRID(issuer_grid), issuer_row, _("Serial 
number:"), key->issuer_serial, FALSE);
                }
                if (key->chain_id != NULL) {
-                       (void) create_key_grid_row(GTK_GRID(issuer_grid), issuer_row, _("Chain ID:"), 
key->chain_id, FALSE);
+                       GtkWidget *chain_btn;
+
+                       issuer_row = create_key_grid_row(GTK_GRID(issuer_grid), issuer_row, _("Chain ID:"), 
key->chain_id, FALSE);
+
+                       /* add button to show the full chain - copy the fingerprint as the key may be 
unref'ed... */
+                       chain_btn = gtk_button_new_with_label(_("view certificate chain…"));
+                       g_object_set_data_full(G_OBJECT(chain_btn), "certid", g_strdup(fingerprint), g_free);
+                       g_signal_connect(chain_btn, "clicked", G_CALLBACK(smime_show_chain), NULL);
+                       gtk_grid_attach(GTK_GRID(issuer_grid), chain_btn, 0, issuer_row, 2, 1);
                }
        }
 
@@ -731,3 +742,33 @@ create_subkey_type_str(gpgme_subkey_t subkey)
 
        return g_string_free(type_str, FALSE);
 }
+
+
+/** \brief Show the S/MIME certificate chain
+ *
+ * \param button button triggering the dialogue
+ * \param user_data callback user data, unused
+ *
+ * Show a modal dialogue with the certification chain of the certificate identified by the fingerprint 
attached as "certid" to
+ * the passed button.
+ */
+static void
+smime_show_chain(GtkWidget *button, gpointer G_GNUC_UNUSED user_data)
+{
+       GtkWidget *vbox;
+       GtkWidget *dialog;
+       GtkWidget *chain;
+
+       dialog = gtk_dialog_new_with_buttons("Certificate Chain",
+               GTK_WINDOW(gtk_widget_get_toplevel(button)),
+               GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(),
+               _("_Close"), GTK_RESPONSE_CLOSE, NULL);
+       geometry_manager_attach(GTK_WINDOW(dialog), "CertChain");
+       chain = x509_cert_chain_smime(g_object_get_data(G_OBJECT(button), "certid"));
+       vbox = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+       gtk_box_pack_start(GTK_BOX(vbox), chain, TRUE, TRUE, 6);
+
+       gtk_widget_show_all(vbox);
+       gtk_dialog_run(GTK_DIALOG(dialog));
+       gtk_widget_destroy(dialog);
+}
diff --git a/libbalsa/libbalsa-gpgme-widgets.h b/libbalsa/libbalsa-gpgme-widgets.h
index 70a5ceb0f..cc0ff8ebf 100644
--- a/libbalsa/libbalsa-gpgme-widgets.h
+++ b/libbalsa/libbalsa-gpgme-widgets.h
@@ -56,6 +56,9 @@ typedef enum {
  * Create a widget containing most information about the key, including all UID's, all requested subkeys and 
the issuer (S/MIME
  * only).  Note that no information about the OpenPGP signatures of the UID's are included, as it is 
expensive to retrieve all
  * signatures of a key.
+ *
+ * If a S/MIME issuer certificate is available, the widget includes a button for displaying the certificate 
chain in a modal
+ * dialogue.
  */
 GtkWidget *libbalsa_gpgme_key(const gpgme_key_t     key,
                                                          const gchar          *fingerprint,
diff --git a/libbalsa/libbalsa.c b/libbalsa/libbalsa.c
index 5dca8eda7..e7bcb8ccd 100644
--- a/libbalsa/libbalsa.c
+++ b/libbalsa/libbalsa.c
@@ -43,15 +43,9 @@
 #include <gtksourceview/gtksource.h>
 #endif
 
-#if HAVE_GCR
-#define GCR_API_SUBJECT_TO_CHANGE
-#include <gcr/gcr.h>
-#else
-#include <gnutls/x509.h>
-#endif
-
 #include "misc.h"
 #include "missing.h"
+#include "x509-cert-widget.h"
 #include <glib/gi18n.h>
 
 static GThread *main_thread_id;
@@ -268,11 +262,11 @@ static gboolean
 ask_idle(gpointer data)
 {
     AskData* ad = (AskData*)data;
-    printf("ask_idle: ENTER %p\n", data);
+    g_debug("ask_idle: ENTER %p", data);
     ad->res = (ad->cb)(ad->arg);
     ad->done = TRUE;
     g_cond_signal(&ad->condvar);
-    printf("ask_idle: LEAVE %p\n", data);
+    g_debug("ask_idle: LEAVE %p", data);
     return FALSE;
 }
 
@@ -287,11 +281,11 @@ libbalsa_ask(gboolean (*cb)(void *arg), void *arg)
 
     if (!libbalsa_am_i_subthread()) {
         int ret;
-        printf("Main thread asks the following question.\n");
+        g_debug("main thread asks the following question");
         ret = cb(arg);
         return ret;
     }
-    printf("Side thread asks the following question.\n");
+    g_debug("side thread asks the following question");
     g_mutex_init(&ad.lock);
     g_cond_init(&ad.condvar);
     ad.cb  = cb;
@@ -406,7 +400,6 @@ struct AskCertData {
     const char *explanation;
 };
 
-#if HAVE_GCR
 
 static int
 ask_cert_real(void *data)
@@ -416,10 +409,15 @@ ask_cert_real(void *data)
     GtkWidget *cert_widget;
     GString *str;
     unsigned i;
-    GByteArray *cert_der;
-    GcrCertificate *gcr_cert;
     GtkWidget *label;
 
+    /* never accept if the certificate is broken, resulting in a NULL widget */
+    cert_widget = x509_cert_chain_tls(acd->certificate); // x509_cert_widget_from_cert(acd->certificate);
+    if (cert_widget == NULL) {
+       // FIXME - message?
+       return CERT_ACCEPT_NO;
+    }
+
     dialog = gtk_dialog_new_with_buttons(_("SSL/TLS certificate"),
                                          NULL, /* FIXME: NULL parent */
                                          GTK_DIALOG_MODAL |
@@ -429,11 +427,6 @@ ask_cert_real(void *data)
                                          _("_Reject"), GTK_RESPONSE_CANCEL,
                                          NULL);
     gtk_window_set_role(GTK_WINDOW(dialog), "tls_cert_dialog");
-    g_object_get(acd->certificate, "certificate", &cert_der, NULL);
-    gcr_cert = gcr_simple_certificate_new(cert_der->data, cert_der->len);
-    g_byte_array_unref(cert_der);
-    cert_widget = GTK_WIDGET(gcr_certificate_widget_new(gcr_cert));
-    g_object_unref(gcr_cert);
 
     str = g_string_new("");
     g_string_printf(str, _("<big><b>Authenticity of this certificate "
@@ -448,7 +441,7 @@ ask_cert_real(void *data)
     gtk_widget_show(label);
     gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
                        cert_widget, TRUE, TRUE, 1);
-    gtk_widget_show(cert_widget);
+    gtk_widget_show_all(cert_widget);
 
     switch(gtk_dialog_run(GTK_DIALOG(dialog))) {
     case 0:
@@ -466,161 +459,6 @@ ask_cert_real(void *data)
     return i;
 }
 
-#else
-
-static gnutls_x509_crt_t G_GNUC_WARN_UNUSED_RESULT
-get_gnutls_cert(GTlsCertificate *cert)
-{
-       gnutls_x509_crt_t res_crt;
-    int gnutls_res;
-
-    gnutls_res = gnutls_x509_crt_init(&res_crt);
-    if (gnutls_res == GNUTLS_E_SUCCESS) {
-       GByteArray *cert_der;
-
-        g_object_get(cert, "certificate", &cert_der, NULL);
-       if (cert_der != NULL) {
-               gnutls_datum_t data;
-
-               data.data = cert_der->data;
-               data.size = cert_der->len;
-               gnutls_res = gnutls_x509_crt_import(res_crt, &data, GNUTLS_X509_FMT_DER);
-               if (gnutls_res != GNUTLS_E_SUCCESS) {
-                       gnutls_x509_crt_deinit(res_crt);
-                       res_crt = NULL;
-               }
-               g_byte_array_unref(cert_der);
-       }
-    } else {
-       res_crt = NULL;
-    }
-    return res_crt;
-}
-
-static gchar * G_GNUC_WARN_UNUSED_RESULT
-gnutls_get_dn(gnutls_x509_crt_t cert, int (*load_fn)(gnutls_x509_crt_t cert, char *buf, size_t *buf_size))
-{
-    size_t buf_size;
-    gchar *str_buf;
-
-    buf_size = 0U;
-    (void) load_fn(cert, NULL, &buf_size);
-    str_buf = g_malloc0(buf_size + 1U);
-    if (load_fn(cert, str_buf, &buf_size) != GNUTLS_E_SUCCESS) {
-       g_free(str_buf);
-       str_buf = NULL;
-    } else {
-       libbalsa_utf8_sanitize(&str_buf, TRUE, NULL);
-    }
-    return str_buf;
-}
-
-static gchar * G_GNUC_WARN_UNUSED_RESULT
-x509_fingerprint(gnutls_x509_crt_t cert)
-{
-    size_t buf_size;
-    guint8 sha1_buf[20];
-    gchar *str_buf;
-    gint n;
-
-    buf_size = 20U;
-    g_message("%d", gnutls_x509_crt_get_fingerprint(cert, GNUTLS_DIG_SHA1, sha1_buf, &buf_size));
-    str_buf = g_malloc0(61U);
-    for (n = 0; n < 20; n++) {
-       sprintf(&str_buf[3 * n], "%02x:", sha1_buf[n]);
-    }
-    str_buf[59] = '\0';
-    return str_buf;
-}
-
-static int
-ask_cert_real(void *data)
-{
-    struct AskCertData *acd = (struct AskCertData*)data;
-    gnutls_x509_crt_t cert;
-    gchar *name, *c, *valid_from, *valid_until;
-    GtkWidget* dialog, *label;
-    unsigned i;
-    GString* str;
-
-    cert = get_gnutls_cert(acd->certificate);
-    if (cert == NULL) {
-       g_warning("%s: unable to create gnutls cert", __func__);
-       return CERT_ACCEPT_NO;
-    }
-
-    str = g_string_new("");
-    g_string_printf(str, _("Authenticity of this certificate "
-                           "could not be verified.\n"
-                           "<b>Reason:</b> %s\n"
-                           "<b>This certificate belongs to:</b>\n"),
-                    acd->explanation);
-
-    name = gnutls_get_dn(cert, gnutls_x509_crt_get_dn);
-    g_string_append(str, name);
-    g_free(name);
-
-    g_string_append(str, _("\n<b>This certificate was issued by:</b>\n"));
-    name = gnutls_get_dn(cert, gnutls_x509_crt_get_issuer_dn);
-    g_string_append_printf(str, "%s\n", name);
-    g_free(name);
-
-    name = x509_fingerprint(cert);
-    valid_from  = libbalsa_date_to_utf8(gnutls_x509_crt_get_activation_time(cert), "%x %X");
-    valid_until = libbalsa_date_to_utf8(gnutls_x509_crt_get_expiration_time(cert), "%x %X");
-    g_string_append_printf(str, _("<b>This certificate is valid</b>\n"
-                                                         "from %s\n"
-                                                         "to %s\n"
-                                         "<b>Fingerprint:</b> %s"),
-                               valid_from, valid_until, name);
-    g_free(name);
-    g_free(valid_from);
-    g_free(valid_until);
-    gnutls_x509_crt_deinit(cert);
-
-    /* This string uses markup, so we must replace "&" with "&amp;" */
-    c = str->str;
-    while ((c = strchr(c, '&'))) {
-        gssize pos;
-
-        pos = (c - str->str) + 1;
-        g_string_insert(str, pos, "amp;");
-        c = str->str + pos;
-    }
-
-    dialog = gtk_dialog_new_with_buttons(_("SSL/TLS certificate"),
-                                         NULL, /* FIXME: NULL parent */
-                                         GTK_DIALOG_MODAL |
-                                         libbalsa_dialog_flags(),
-                                         _("_Accept Once"), 0,
-                                         _("Accept & _Save"), 1,
-                                         _("_Reject"), GTK_RESPONSE_CANCEL, 
-                                         NULL);
-    gtk_window_set_role(GTK_WINDOW(dialog), "tls_cert_dialog");
-    label = gtk_label_new(str->str);
-    g_string_free(str, TRUE);
-    gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
-    gtk_box_pack_start(GTK_BOX
-                       (gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
-                       label, TRUE, TRUE, 1);
-    gtk_widget_show(label);
-
-    switch(gtk_dialog_run(GTK_DIALOG(dialog))) {
-    case 0: i = CERT_ACCEPT_SESSION; break;
-    case 1: i = CERT_ACCEPT_PERMANENT; break;
-    case GTK_RESPONSE_CANCEL:
-    default: i=CERT_ACCEPT_NO; break;
-    }
-    gtk_widget_destroy(dialog);
-    /* Process some events to let the window disappear:
-     * not really necessary but helps with debugging. */
-   while(gtk_events_pending()) 
-        gtk_main_iteration_do(FALSE);
-    g_debug("%s returns %u", __func__, i);
-    return i;
-}
-
-#endif /* HAVE_GCR */
 
 static int
 libbalsa_ask_for_cert_acceptance(GTlsCertificate      *cert,
diff --git a/libbalsa/meson.build b/libbalsa/meson.build
index 471d97d4d..c628b9935 100644
--- a/libbalsa/meson.build
+++ b/libbalsa/meson.build
@@ -131,7 +131,10 @@ libbalsa_a_sources = [
   'url.c',
   'url.h',
   'geometry-manager.c',
-  'geometry-manager.h']
+  'geometry-manager.h',
+  'x509-cert-widget.c',
+  'x509-cert-widget.h'
+]
 
 libimap_include = include_directories('imap')
 
diff --git a/libbalsa/x509-cert-widget.c b/libbalsa/x509-cert-widget.c
new file mode 100644
index 000000000..56d63a3ab
--- /dev/null
+++ b/libbalsa/x509-cert-widget.c
@@ -0,0 +1,532 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/*
+ * Balsa E-Mail Client
+ *
+ * X509 certificate (TLS, S/MIME) widgets
+ * Copyright (C) 2019 Albrecht Dreß <albrecht dress arcor de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#if HAVE_GCR
+#define GCR_API_SUBJECT_TO_CHANGE
+#include <gcr/gcr.h>
+#else
+#include <gnutls/x509.h>
+#include <gnutls/crypto.h>
+#include "libbalsa.h"
+#include "misc.h"
+#endif
+
+#include <glib/gi18n.h>
+
+#include "libbalsa-gpgme.h"
+#include "libbalsa-gpgme-keys.h"
+#include "x509-cert-widget.h"
+
+
+/* stuff for displaying a certificate chain */
+enum {
+    CERT_NAME_COLUMN = 0,
+       CERT_WIDGET_COLUMN,
+       CERT_COLUMNS
+};
+
+
+typedef struct {
+       gchar *label;
+       GtkWidget *widget;
+} cert_data_t;
+
+
+static GtkWidget *x509_cert_widget_from_buffer(const guchar  *buffer,
+                                                                                          size_t         
buflen,
+                                                                                          gchar        
**subject);
+static cert_data_t *cert_data_tls(GTlsCertificate *cert);
+static cert_data_t *cert_data_smime(gpgme_ctx_t   ctx,
+                                                                       gchar       **fingerprint);
+static void cert_data_free(cert_data_t *cert_data);
+static GtkWidget *create_chain_widget(GList *cert_list);
+static void cert_selected_cb(GtkTreeView       *tree_view,
+                                                        GtkTreePath       *path,
+                                                        GtkTreeViewColumn *column,
+                                                        GtkStack          *stack);
+
+
+/* documentation - see header file */
+GtkWidget *
+x509_cert_chain_tls(GTlsCertificate *cert)
+{
+       GList *chain;
+       GTlsCertificate *issuer;
+       GtkWidget *widget;
+
+       g_return_val_if_fail(G_IS_TLS_CERTIFICATE(cert), NULL);
+
+       chain = g_list_prepend(NULL, cert_data_tls(cert));
+       issuer = g_tls_certificate_get_issuer(cert);
+       while (issuer != NULL) {
+               GTlsCertificate *parent;
+
+               chain = g_list_prepend(chain, cert_data_tls(issuer));
+
+               /* get parent - note: this is part of the source certificate, so *don't* unref it */
+               parent = g_tls_certificate_get_issuer(issuer);
+               issuer = parent;
+       }
+
+       if (chain->next != NULL) {
+               widget = create_chain_widget(chain);
+       } else {
+               widget = ((cert_data_t *) chain->data)->widget;
+       }
+       g_list_free_full(chain, (GDestroyNotify) cert_data_free);
+       return widget;
+}
+
+
+/* documentation - see header file */
+GtkWidget *
+x509_cert_chain_smime(const gchar *fingerprint)
+{
+       gpgme_ctx_t ctx;
+       GtkWidget *widget = NULL;
+
+       g_return_val_if_fail(fingerprint != NULL, NULL);
+
+       ctx = libbalsa_gpgme_new_with_proto(GPGME_PROTOCOL_CMS, NULL, NULL, NULL);
+       if (ctx != NULL) {
+               GList *chain = NULL;
+               gchar *keyid;
+
+               keyid = g_strdup(fingerprint);
+               while (keyid != NULL) {
+                       chain = g_list_prepend(chain, cert_data_smime(ctx, &keyid));
+               }
+               gpgme_release(ctx);
+               if (chain->next != NULL) {
+                       widget = create_chain_widget(chain);
+               } else {
+                       widget = ((cert_data_t *) chain->data)->widget;
+               }
+               g_list_free_full(chain, (GDestroyNotify) cert_data_free);
+       }
+
+       return widget;
+}
+
+
+/** \brief Create the certificate chain widget
+ *
+ * \param cert_list list of \ref cert_data_t items, starting with the root certificate
+ * \return a newly created certificate chain widget
+ *
+ * The certificate chain widget is a VBox, containing the certificate chain tree view in the upper and a 
GtkStack in the lower part.
+ * The latter displays the certificate selected in the tree view.
+ */
+static GtkWidget *
+create_chain_widget(GList *cert_list)
+{
+       GtkWidget *vbox;
+       GtkWidget *scrolledwin;
+       GtkTreeStore *store;
+       GtkTreeIter iter;
+       GtkTreeIter parent;
+       GtkWidget *tree_view;
+       GtkCellRenderer *renderer;
+       GtkTreeViewColumn *column;
+       GtkTreePath *path;
+       GtkWidget *stack;
+       GList *p;
+       gboolean is_root;
+
+       vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+       scrolledwin = gtk_scrolled_window_new(NULL, NULL);
+       gtk_box_pack_start(GTK_BOX(vbox), scrolledwin, TRUE, TRUE, 6U);
+
+       store = gtk_tree_store_new(CERT_COLUMNS, G_TYPE_STRING, GTK_TYPE_WIDGET);
+       tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
+       gtk_container_add(GTK_CONTAINER(scrolledwin), tree_view);
+       renderer = gtk_cell_renderer_text_new();
+       column = gtk_tree_view_column_new_with_attributes(NULL, renderer, "text", CERT_NAME_COLUMN, NULL);
+       gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
+       gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(tree_view), FALSE);
+       gtk_tree_view_set_level_indentation(GTK_TREE_VIEW(tree_view), 12);
+       gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), FALSE);
+       gtk_tree_view_set_activate_on_single_click(GTK_TREE_VIEW(tree_view), TRUE);
+
+       stack = gtk_stack_new();
+       gtk_box_pack_start(GTK_BOX(vbox), stack, TRUE, TRUE, 6U);
+       g_signal_connect(tree_view, "row-activated", G_CALLBACK(cert_selected_cb), stack);
+
+       is_root = TRUE;
+       for (p = cert_list; p != NULL; p = p->next) {
+               cert_data_t *cert = (cert_data_t *) p->data;
+
+               gtk_widget_show(cert->widget);
+               gtk_tree_store_append(store, &iter, is_root ? NULL : &parent);
+               parent = iter;
+               gtk_tree_store_set(store, &iter,
+                       CERT_NAME_COLUMN, cert->label,
+                       CERT_WIDGET_COLUMN, cert->widget,
+                       -1);
+               gtk_stack_add_named(GTK_STACK(stack), cert->widget, cert->label);
+               gtk_stack_set_visible_child(GTK_STACK(stack), cert->widget);
+               is_root = FALSE;
+       }
+
+       gtk_tree_view_expand_all(GTK_TREE_VIEW(tree_view));
+       gtk_tree_selection_select_iter(gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view)), &iter);
+       path = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter);
+       gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree_view), path, NULL, FALSE, 0.0, 0.0);
+       gtk_tree_path_free(path);
+       g_object_unref(store);
+
+       return vbox;
+}
+
+
+/** \brief Certificate selection callback
+ *
+ * \param tree_view tree view
+ * \param path selected tree view path
+ * \param column selected tree view column, unused
+ * \param stack target stack
+ *
+ * Switch the passed GtkStack to display the widget indicated by the \ref CERT_WIDGET_COLUMN of the selected 
tree view path.
+ */
+static void
+cert_selected_cb(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn G_GNUC_UNUSED *column, 
GtkStack *stack)
+{
+       GtkTreeModel *model = gtk_tree_view_get_model(tree_view);
+       GtkTreeIter iter;
+
+       if (gtk_tree_model_get_iter(model, &iter, path)) {
+               GtkWidget *widget;
+
+               gtk_tree_model_get(model, &iter, CERT_WIDGET_COLUMN, &widget, -1);
+               gtk_stack_set_visible_child(stack, widget);
+       }
+}
+
+
+/** \brief Get subject and widget for a S/MIME certificate
+ *
+ * \param ctx properly initialised GpgME context
+ * \param fingerprint fingerprint of the S/MIME certificate to be loaded, replaced by the parent certificate 
fingerprint
+ * \return a newly allocated certificate data item, containing subject string and widget
+ *
+ * If the certificate can be loaded using the passed fingerprint, the widget in the returned struct is a 
display widget as created
+ * by calling x509_cert_widget_from_buffer(), and the subject is the sanitised primary uid.  The passed 
fingerprint is replaced by
+ * the fingerprint of the issuer certificate.  The caller shall free this value if necessary.
+ *
+ * Otherwise, the widget is just a GtkLabel saying that the certificate is not available, the label contains 
the fingerprint string,
+ * and the passed fingerprint is freed and returned as NULL.
+ */
+static cert_data_t *
+cert_data_smime(gpgme_ctx_t ctx, gchar **fingerprint)
+{
+       gpgme_key_t key;
+       GError *error = NULL;
+       cert_data_t *result;
+
+       result = g_new0(cert_data_t, 1U);
+       key = libbalsa_gpgme_load_key(ctx, *fingerprint, &error);
+       if (key != NULL) {
+               gpgme_data_t keybuf;
+               gchar *key_data;
+               size_t key_bytes;
+
+               result->label = libbalsa_cert_subject_readable(key->uids->uid);
+               gpgme_data_new(&keybuf);
+               (void) gpgme_op_export(ctx, *fingerprint, 0, keybuf);
+               key_data = gpgme_data_release_and_get_mem(keybuf, &key_bytes);
+               result->widget = x509_cert_widget_from_buffer((const guchar *) key_data, key_bytes, NULL);
+               gpgme_free(key_data);
+
+               /* check if we reached the root */
+               if ((key->chain_id != NULL) && (g_ascii_strcasecmp(*fingerprint, key->chain_id) != 0)) {
+                       g_free(*fingerprint);
+                       *fingerprint = g_strdup(key->chain_id);
+               } else {
+                       g_free(*fingerprint);
+                       *fingerprint = NULL;
+               }
+               gpgme_key_unref(key);
+       } else {
+               gchar *errbuf;
+
+               result->label = g_strdup_printf(_("fingerprint %s"), *fingerprint);
+               errbuf = g_strdup_printf(_("cannot load key with fingerprint %s: %s"), *fingerprint, 
error->message);
+               g_error_free(error);
+               result->widget = gtk_label_new(errbuf);
+               g_free(errbuf);
+               g_free(*fingerprint);
+               *fingerprint = NULL;
+       }
+
+       return result;
+}
+
+
+/** \brief Free a certificate data item
+ *
+ * \param cert_data certificate data
+ *
+ * This function is used as callback for g_list_free_full() from x509_cert_chain_tls() and 
x509_cert_chain_smime() when cleaning up
+ * the certificate chain data.  It frees cert_data_t::label and the passed data item itself, but \em not 
cert_data::widget, as it
+ * is consumed in the returned widget of the aforementioned functions.
+ */
+static void
+cert_data_free(cert_data_t *cert_data)
+{
+       g_free(cert_data->label);
+       g_free(cert_data);
+}
+
+
+#if HAVE_GCR
+
+/** \brief Get subject and widget for a TLS certificate
+ *
+ * \param cert TLS certificate
+ * \return a newly allocated certificate data item, containing subject string and widget
+ */
+static cert_data_t *
+cert_data_tls(GTlsCertificate *cert)
+{
+       GByteArray *der_data;
+    GcrCertificate *gcr_cert;
+       cert_data_t *result;
+
+       result = g_new0(cert_data_t, 1U);
+       g_object_get(cert, "certificate", &der_data, NULL);
+       gcr_cert = gcr_simple_certificate_new(der_data->data, der_data->len);
+       g_byte_array_unref(der_data);
+       result->label = gcr_certificate_get_subject_name(gcr_cert);
+       result->widget = GTK_WIDGET(gcr_certificate_widget_new(gcr_cert));
+    g_object_unref(gcr_cert);
+       return result;
+}
+
+
+/** \brief Create a certificate widget from a DER buffer
+ *
+ * \param buffer DER data buffer
+ * \param buflen number of bytes in the DER data buffer
+ * \param subject unused, required for a unified API when GCR is not available
+ * \return a new (GCR) certificate widget
+ */
+static GtkWidget *
+x509_cert_widget_from_buffer(const guchar *buffer, size_t buflen, gchar G_GNUC_UNUSED **subject)
+{
+    GcrCertificate *gcr_cert;
+    GtkWidget *widget;
+
+       g_return_val_if_fail((buffer != NULL) && (buflen > 0U), NULL);
+
+       gcr_cert = gcr_simple_certificate_new(buffer, buflen);
+    widget = GTK_WIDGET(gcr_certificate_widget_new(gcr_cert));
+    g_object_unref(gcr_cert);
+    return widget;
+}
+
+#else
+
+/** \brief Create a GnuTLS certificate item from DER data
+ *
+ * \param buffer DER data buffer
+ * \param buflen number of bytes in the DER data buffer
+ * \return a new GnuTLS certificate item
+ */
+static gnutls_x509_crt_t G_GNUC_WARN_UNUSED_RESULT
+get_gnutls_cert(const guchar *buffer, guint buflen)
+{
+       gnutls_x509_crt_t res_crt;
+    int gnutls_res;
+
+    gnutls_res = gnutls_x509_crt_init(&res_crt);
+    if (gnutls_res == GNUTLS_E_SUCCESS) {
+       gnutls_datum_t data;
+
+       data.data = (unsigned char *) buffer;
+       data.size = buflen;
+       gnutls_res = gnutls_x509_crt_import(res_crt, &data, GNUTLS_X509_FMT_DER);
+       if (gnutls_res != GNUTLS_E_SUCCESS) {
+               gnutls_x509_crt_deinit(res_crt);
+               res_crt = NULL;
+       }
+    } else {
+       res_crt = NULL;
+    }
+
+    if (res_crt == NULL) {
+       g_warning("GnuTLS: %d: %s", gnutls_res, gnutls_strerror(gnutls_res));
+    }
+
+    return res_crt;
+}
+
+
+/** \brief Get a DN from a GnuTLS certificate
+ *
+ * \param cert GnuTLS certificate
+ * \param load_fn function pointer for loading the DN
+ * \return a newly allocated, utf8-clean string containing the loaded DN
+ */
+static gchar * G_GNUC_WARN_UNUSED_RESULT
+gnutls_get_dn(gnutls_x509_crt_t cert, int (*load_fn)(gnutls_x509_crt_t cert, char *buf, size_t *buf_size))
+{
+    size_t buf_size;
+    gchar *str_buf;
+
+    buf_size = 0U;
+    (void) load_fn(cert, NULL, &buf_size);
+    str_buf = g_malloc0(buf_size + 1U);
+    if (load_fn(cert, str_buf, &buf_size) != GNUTLS_E_SUCCESS) {
+       g_free(str_buf);
+       str_buf = NULL;
+    } else {
+       libbalsa_utf8_sanitize(&str_buf, TRUE, NULL);
+    }
+    return str_buf;
+}
+
+
+/** \brief Get the certificate fingerprint
+ *
+ * \param cert GnuTLS certificate
+ * \param algo fingerprint hash algorithm
+ * \return a newly allocated string containing the fingerprint as a series of colon-separated hex bytes
+ */
+static gchar * G_GNUC_WARN_UNUSED_RESULT
+x509_fingerprint(gnutls_x509_crt_t cert, gnutls_digest_algorithm_t algo)
+{
+    size_t buf_size;
+    guint8 *sha_buf;
+    gchar *str_buf;
+    size_t n;
+
+    buf_size = gnutls_hash_get_len(algo);
+    sha_buf = g_malloc(buf_size);
+    gnutls_x509_crt_get_fingerprint(cert, algo, sha_buf, &buf_size);
+    str_buf = g_malloc0((3 * buf_size) + 1U);
+    for (n = 0U; n < buf_size; n++) {
+       sprintf(&str_buf[3U * n], "%02x:", sha_buf[n]);
+    }
+    g_free(sha_buf);
+    str_buf[(3 * buf_size) - 1U] = '\0';
+    return str_buf;
+}
+
+
+/** \brief Create a certificate widget from a DER buffer
+ *
+ * \param buffer DER data buffer
+ * \param buflen number of bytes in the DER data buffer
+ * \param subject if not NULL, filled with a newly allocated string containing the certificate's subject
+ * \return a new (GCR) certificate widget
+ *
+ * The Widget is basically a label, containing the certificate's dn, the issuer dn, the SHA1 and SHA256 
fingerprints, and the
+ * validity period.
+ */
+static GtkWidget *
+x509_cert_widget_from_buffer(const guchar *buffer, size_t buflen, gchar **subject)
+{
+    gnutls_x509_crt_t cert;
+    GtkWidget *widget;
+    GString *str;
+    gchar *name;
+    gchar *valid_from;
+    gchar *valid_until;
+    gchar *sha256_fp;
+    gchar *c;
+
+       cert = get_gnutls_cert(buffer, buflen);
+    if (cert == NULL) {
+       g_warning("%s: unable to create gnutls cert", __func__);
+       return NULL;
+    }
+
+    str = g_string_new("<b>This certificate belongs to:</b>\n");
+
+    name = gnutls_get_dn(cert, gnutls_x509_crt_get_dn);
+    g_string_append(str, name);
+    if (subject != NULL) {
+       *subject = name;
+    } else {
+       g_free(name);
+    }
+
+    g_string_append(str, _("\n<b>This certificate was issued by:</b>\n"));
+    name = gnutls_get_dn(cert, gnutls_x509_crt_get_issuer_dn);
+    g_string_append_printf(str, "%s\n", name);
+    g_free(name);
+
+    name = x509_fingerprint(cert, GNUTLS_DIG_SHA1);
+    sha256_fp = x509_fingerprint(cert, GNUTLS_DIG_SHA256);
+    valid_from  = libbalsa_date_to_utf8(gnutls_x509_crt_get_activation_time(cert), "%x %X");
+    valid_until = libbalsa_date_to_utf8(gnutls_x509_crt_get_expiration_time(cert), "%x %X");
+    g_string_append_printf(str, _("<b>This certificate is valid</b>\n"
+                                                         "from %s\n"
+                                                         "to %s\n"
+                                         "<b>Fingerprint:</b>\nSHA1: %s\nSHA256: %s"),
+                               valid_from, valid_until, name, sha256_fp);
+    g_free(sha256_fp);
+    g_free(name);
+    g_free(valid_from);
+    g_free(valid_until);
+    gnutls_x509_crt_deinit(cert);
+
+    /* This string uses markup, so we must replace "&" with "&amp;" */
+    c = str->str;
+    while ((c = strchr(c, '&'))) {
+        gssize pos;
+
+        pos = (c - str->str) + 1;
+        g_string_insert(str, pos, "amp;");
+        c = str->str + pos;
+    }
+    widget = gtk_label_new(str->str);
+    gtk_label_set_use_markup(GTK_LABEL(widget), TRUE);
+    gtk_label_set_ellipsize(GTK_LABEL(widget), PANGO_ELLIPSIZE_END);
+    g_string_free(str, TRUE);
+    return widget;
+}
+
+
+/** \brief Get subject and widget for a TLS certificate
+ *
+ * \param cert TLS certificate
+ * \return a newly allocated certificate data item, containing subject string and widget
+ */
+static cert_data_t *
+cert_data_tls(GTlsCertificate *cert)
+{
+       GByteArray *der_data;
+       cert_data_t *result;
+
+       result = g_new0(cert_data_t, 1U);
+       g_object_get(cert, "certificate", &der_data, NULL);
+       result->widget = x509_cert_widget_from_buffer(der_data->data, der_data->len, &result->label);
+       g_byte_array_unref(der_data);
+       return result;
+}
+
+#endif
diff --git a/libbalsa/x509-cert-widget.h b/libbalsa/x509-cert-widget.h
new file mode 100644
index 000000000..cb04a4e1f
--- /dev/null
+++ b/libbalsa/x509-cert-widget.h
@@ -0,0 +1,58 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/*
+ * Balsa E-Mail Client
+ *
+ * X509 certificate (TLS, S/MIME) widgets
+ * Copyright (C) 2019 Albrecht Dreß <albrecht dress arcor de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBBALSA_X509_CERT_WIDGET_H_
+#define LIBBALSA_X509_CERT_WIDGET_H_
+
+
+#include <gtk/gtk.h>
+
+
+G_BEGIN_DECLS
+
+
+/** \brief Create a certificate chain widget for a TLS certificate
+ *
+ * \param cert TLS certificate
+ * \return a new widget on success or NULL on error
+ *
+ * If the passed certificate is self-signed or if the issuer cannot be determined, the function returns a 
widget containing the
+ * certificate information.  Otherwise, the returned widget is a vertical GtkBox, containing the certificate 
chain tree view in the
+ * upper and a GtkStack in the lower part.  The latter displays the certificate selected in the tree view.
+ */
+GtkWidget *x509_cert_chain_tls(GTlsCertificate *cert);
+
+/** \brief Create a certificate chain widget for a S/MIME certificate
+ *
+ * \param fingerprint fingerprint of a S/MIME certificate
+ * \return a new widget on success or NULL on error
+ *
+ * If S/MIME certificate identified by the fingerprint is self-signed or if the issuer cannot be determined, 
the function returns a
+ * widget containing the certificate information.  Otherwise, the returned widget is a vertical GtkBox, 
containing the certificate
+ * chain tree view in the upper and a GtkStack in the lower part.  The latter displays the certificate 
selected in the tree view.
+ */
+GtkWidget *x509_cert_chain_smime(const gchar *fingerprint);
+
+
+G_END_DECLS
+
+
+#endif         /* LIBBALSA_X509_CERT_WIDGET_H_ */
diff --git a/src/save-restore.c b/src/save-restore.c
index 1cf0b7a95..596dac439 100644
--- a/src/save-restore.c
+++ b/src/save-restore.c
@@ -730,6 +730,7 @@ config_global_load(void)
 #ifdef ENABLE_AUTOCRYPT
     geometry_manager_init("AutocryptDB", 300, 200, FALSE);
 #endif  /* ENABLE_AUTOCRYPT */
+    geometry_manager_init("CertChain", 300, 200, FALSE);
 
     /* FIXME: PKGW: why comment this out? Breaks my Transfer context menu. */
     if (balsa_app.mblist_width < 100)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]