Add signature verification of PGP/MIME-signed parts with --verify.
authorJameson Graef Rollins <jrollins@finestructure.net>
Thu, 26 May 2011 01:01:17 +0000 (18:01 -0700)
committerCarl Worth <cworth@cworth.org>
Fri, 27 May 2011 23:22:00 +0000 (16:22 -0700)
This is primarily for notmuch-show, although the functionality is
added to show-message.  Once signatures are processed a new
part_sigstatus formatter is emitted, and the entire multipart/signed
part is replaced with the contents of the signed part.

At the moment only a json part_sigstatus formatting function is
available.  Emacs support to follow.

The original work for this patch was done by

  Daniel Kahn Gillmor <dkg@fifthhorseman.net>

whose help with this functionality I greatly appreciate.

Makefile.local
notmuch-client.h
notmuch-gmime-session.c [new file with mode: 0644]
notmuch-reply.c
notmuch-show.c
notmuch.1
notmuch.c
show-message.c

index 8a8832da992722c61d8a58cb7a4d738fae2f401d..f726f1f26e15262df0e8aba55fcdc8bf899e3da3 100644 (file)
@@ -246,6 +246,7 @@ notmuch_client_srcs =               \
        notmuch-show.c          \
        notmuch-tag.c           \
        notmuch-time.c          \
+       notmuch-gmime-session.c \
        query-string.c          \
        show-message.c          \
        json.c                  \
index b278bc774a8a84b19fa7d315f33ecf5b8a1356e7..dc4ed7aafd03bcdbff1178937e02772789a06188 100644 (file)
@@ -67,6 +67,7 @@ typedef struct notmuch_show_format {
     const char *body_start;
     void (*part_start) (GMimeObject *part,
                        int *part_count);
+    void (*part_sigstatus) (const GMimeSignatureValidity* validity);
     void (*part_content) (GMimeObject *part);
     void (*part_end) (GMimeObject *part);
     const char *part_sep;
@@ -80,6 +81,7 @@ typedef struct notmuch_show_params {
     int entire_thread;
     int raw;
     int part;
+    GMimeCipherContext* cryptoctx;
 } notmuch_show_params_t;
 
 /* There's no point in continuing when we've detected that we've done
@@ -233,4 +235,7 @@ notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config,
 notmuch_bool_t
 debugger_is_active (void);
 
+GType
+notmuch_gmime_session_get_type (void);
+
 #endif
diff --git a/notmuch-gmime-session.c b/notmuch-gmime-session.c
new file mode 100644 (file)
index 0000000..d83d9b3
--- /dev/null
@@ -0,0 +1,49 @@
+#include "notmuch-client.h"
+
+/* CRUFTY BOILERPLATE for GMimeSession (dkg thinks this will go away once GMime 2.6 comes out) */
+typedef struct _NotmuchGmimeSession NotmuchGmimeSession;
+typedef struct _NotmuchGmimeSessionClass NotmuchGmimeSessionClass;
+
+struct _NotmuchGmimeSession {
+    GMimeSession parent_object;
+};
+
+struct _NotmuchGmimeSessionClass {
+    GMimeSessionClass parent_class;
+};
+
+static void notmuch_gmime_session_class_init (NotmuchGmimeSessionClass *klass);
+
+static GMimeSessionClass *parent_class = NULL;
+
+GType
+notmuch_gmime_session_get_type (void)
+{
+    static GType type = 0;
+
+    if (!type) {
+       static const GTypeInfo info = {
+           sizeof (NotmuchGmimeSessionClass),
+           NULL, /* base_class_init */
+           NULL, /* base_class_finalize */
+           (GClassInitFunc) notmuch_gmime_session_class_init,
+           NULL, /* class_finalize */
+           NULL, /* class_data */
+           sizeof (NotmuchGmimeSession),
+           0,    /* n_preallocs */
+           NULL, /* object_init */
+           NULL, /* value_table */
+       };
+       type = g_type_register_static (GMIME_TYPE_SESSION, "NotmuchGmimeSession", &info, 0);
+    }
+    return type;
+}
+
+static void
+notmuch_gmime_session_class_init (NotmuchGmimeSessionClass *klass)
+{
+    GMimeSessionClass *session_class = GMIME_SESSION_CLASS (klass);
+    parent_class = g_type_class_ref (GMIME_TYPE_SESSION);
+    session_class->request_passwd = NULL;
+}
+/* END CRUFTY BOILERPLATE */
index 9c35475d0553fa96bb37b141116dfa22c63b1569..99bb15fce57642c777b6184c917a0e0c8ca5adfe 100644 (file)
@@ -32,6 +32,7 @@ static const notmuch_show_format_t format_reply = {
        "", NULL,
            "", NULL, "",
            "",
+               NULL,
                NULL,
                reply_part_content,
                NULL,
@@ -447,6 +448,7 @@ notmuch_reply_format_default(void *ctx, notmuch_config_t *config, notmuch_query_
     const notmuch_show_format_t *format = &format_reply;
     notmuch_show_params_t params;
     params.part = -1;
+    params.cryptoctx = NULL;
 
     for (messages = notmuch_query_search_messages (query);
         notmuch_messages_valid (messages);
index 363cdbfed7760207d30516e89cc1e8d158c043a1..bb54e563bea87378398773ac7ab420203818b505 100644 (file)
@@ -44,6 +44,7 @@ static const notmuch_show_format_t format_text = {
            "\fheader{\n", format_headers_text, "\fheader}\n",
            "\fbody{\n",
                format_part_start_text,
+               NULL,
                format_part_content_text,
                format_part_end_text,
                "",
@@ -64,6 +65,9 @@ static void
 format_part_start_json (unused (GMimeObject *part),
                        int *part_count);
 
+static void
+format_part_sigstatus_json (const GMimeSignatureValidity* validity);
+
 static void
 format_part_content_json (GMimeObject *part);
 
@@ -76,6 +80,7 @@ static const notmuch_show_format_t format_json = {
            ", \"headers\": {", format_headers_json, "}",
            ", \"body\": [",
                format_part_start_json,
+               format_part_sigstatus_json,
                format_part_content_json,
                format_part_end_json,
                ", ",
@@ -97,6 +102,7 @@ static const notmuch_show_format_t format_mbox = {
                 NULL,
                 NULL,
                 NULL,
+                NULL,
                 "",
             "",
         "", "",
@@ -111,6 +117,7 @@ static const notmuch_show_format_t format_raw = {
        "", NULL,
            "", NULL, "",
             "",
+                NULL,
                 NULL,
                 format_part_content_raw,
                 NULL,
@@ -396,6 +403,22 @@ show_part_content (GMimeObject *part, GMimeStream *stream_out)
        g_object_unref(stream_filter);
 }
 
+static const char*
+signerstatustostring (GMimeSignerStatus x)
+{
+    switch (x) {
+    case GMIME_SIGNER_STATUS_NONE:
+       return "none";
+    case GMIME_SIGNER_STATUS_GOOD:
+       return "good";
+    case GMIME_SIGNER_STATUS_BAD:
+       return "bad";
+    case GMIME_SIGNER_STATUS_ERROR:
+       return "error";
+    }
+    return "unknown";
+}
+
 static void
 format_part_start_text (GMimeObject *part, int *part_count)
 {
@@ -472,6 +495,65 @@ format_part_start_json (unused (GMimeObject *part), int *part_count)
     printf ("{\"id\": %d", *part_count);
 }
 
+static void
+format_part_sigstatus_json (const GMimeSignatureValidity* validity)
+{
+    printf (", \"sigstatus\": [");
+
+    if (!validity) {
+       printf ("]");
+       return;
+    }
+
+    const GMimeSigner *signer = g_mime_signature_validity_get_signers (validity);
+    int first = 1;
+    void *ctx_quote = talloc_new (NULL);
+
+    while (signer) {
+       if (first)
+           first = 0;
+       else
+           printf (", ");
+
+       printf ("{");
+
+       /* status */
+       printf ("\"status\": %s", json_quote_str (ctx_quote, signerstatustostring(signer->status)));
+
+       if (signer->status == GMIME_SIGNER_STATUS_GOOD)
+       {
+           if (signer->fingerprint)
+               printf (", \"fingerprint\": %s", json_quote_str (ctx_quote, signer->fingerprint));
+           /* these dates are seconds since the epoch; should we
+            * provide a more human-readable format string? */
+           if (signer->created)
+               printf (", \"created\": %d", (int) signer->created);
+           if (signer->expires)
+               printf (", \"expires\": %d", (int) signer->expires);
+           /* output user id only if validity is FULL or ULTIMATE. */
+           /* note that gmime is using the term "trust" here, which
+            * is WRONG.  It's actually user id "validity". */
+           if ((signer->name) && (signer->trust)) {
+               if ((signer->trust == GMIME_SIGNER_TRUST_FULLY) || (signer->trust == GMIME_SIGNER_TRUST_ULTIMATE))
+                   printf (", \"userid\": %s", json_quote_str (ctx_quote, signer->name));
+           }
+       } else {
+           if (signer->keyid)
+               printf (", \"keyid\": %s", json_quote_str (ctx_quote, signer->keyid));
+       }
+       if (signer->errors != GMIME_SIGNER_ERROR_NONE) {
+           printf (", \"errors\": %x", signer->errors);
+       }
+
+       printf ("}");
+       signer = signer->next;
+    }
+
+    printf ("]");
+
+    talloc_free (ctx_quote);
+}
+
 static void
 format_part_content_json (GMimeObject *part)
 {
@@ -739,6 +821,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     params.entire_thread = 0;
     params.raw = 0;
     params.part = -1;
+    params.cryptoctx = NULL;
 
     for (i = 0; i < argc && argv[i][0] == '-'; i++) {
        if (strcmp (argv[i], "--") == 0) {
@@ -767,6 +850,16 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
            params.part = atoi(argv[i] + sizeof ("--part=") - 1);
        } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
            params.entire_thread = 1;
+       } else if (STRNCMP_LITERAL (argv[i], "--verify") == 0) {
+           if (params.cryptoctx == NULL) {
+               GMimeSession* session = g_object_new(notmuch_gmime_session_get_type(), NULL);
+               if (NULL == (params.cryptoctx = g_mime_gpg_context_new(session, "gpg")))
+                   fprintf (stderr, "Failed to construct gpg context.\n");
+               else
+                   g_mime_gpg_context_set_always_trust((GMimeGpgContext*)params.cryptoctx, FALSE);
+               g_object_unref (session);
+               session = NULL;
+           }
        } else {
            fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
            return 1;
@@ -824,5 +917,8 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
 
+    if (params.cryptoctx)
+       g_object_unref(params.cryptoctx);
+
     return 0;
 }
index a1c7fa8d4e48c1bb613ad06a943900e44d2f10e2..daa9c6fbc042c6cf4b256049ae609dab7d4fb279 100644 (file)
--- a/notmuch.1
+++ b/notmuch.1
@@ -321,6 +321,17 @@ a depth-first walk of the message MIME structure, and are identified
 in the 'json' or 'text' output formats.
 .RE
 
+.RS 4
+.TP 4
+.B \-\-verify
+
+Compute and report the validity of any MIME cryptographic signatures
+found in the selected content (ie. "multipart/signed" parts). Status
+of the signature will be reported (currently only supported with
+--format=json), and the multipart/signed part will be replaced by the
+signed data.
+.RE
+
 A common use of
 .B notmuch show
 is to display a single thread of email messages. For this, use a
index 262d677c5b4d700277c5e4521382cce2de0b7bc0..cd3cb1b502f34e93f0e534e33216195a96962956 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -294,6 +294,14 @@ static command_t commands[] = {
       "\t\tmessage MIME structure, and are identified in the 'json' or\n"
       "\t\t'text' output formats.\n"
       "\n"
+      "\t--verify\n"
+      "\n"
+      "\t\tCompute and report the validity of any MIME cryptographic\n"
+      "\t\tsignatures found in the selected content (ie.\n"
+      "\t\t\"multipart/signed\" parts). Status of the signature will be\n"
+      "\t\treported (currently only supported with --format=json) and\n"
+      "\t\tthe multipart/signed part will be replaced by the signed data.\n"
+      "\n"
       "\n"
       "\tA common use of \"notmuch show\" is to display a single\n"
       "\tthread of email messages. For this, use a search term of\n"
index fbae530180b99b95eb5ac4e788c63a2975043eff..c90f310ca3c45b77b412b2ded67239e9e566b4c8 100644 (file)
@@ -51,9 +51,48 @@ show_message_part (GMimeObject *part,
 
        if (format->part_start)
            format->part_start (part, &(state->part_count));
-       format->part_content (part);
     }
 
+    /* handle PGP/MIME parts */
+    if (GMIME_IS_MULTIPART (part) && params->cryptoctx) {
+       GMimeMultipart *multipart = GMIME_MULTIPART (part);
+       GError* err = NULL;
+
+       if (GMIME_IS_MULTIPART_SIGNED (part))
+       {
+           if ( g_mime_multipart_get_count (multipart) != 2 ) {
+               /* this violates RFC 3156 section 5, so we won't bother with it. */
+               fprintf (stderr,
+                        "Error: %d part(s) for a multipart/signed message (should be exactly 2)\n",
+                        g_mime_multipart_get_count (multipart));
+           } else {
+               /* For some reason the GMimeSignatureValidity returned
+                * here is not a const (inconsistent with that
+                * returned by
+                * g_mime_multipart_encrypted_get_signature_validity,
+                * and therefore needs to be properly disposed of.
+                * Hopefully the API will become more consistent. */
+               GMimeSignatureValidity *sigvalidity = g_mime_multipart_signed_verify (GMIME_MULTIPART_SIGNED (part), params->cryptoctx, &err);
+               if (!sigvalidity) {
+                   fprintf (stderr, "Failed to verify signed part: %s\n", (err ? err->message : "no error explanation given"));
+               }
+               if ((selected || state->in_zone) && format->part_sigstatus)
+                   format->part_sigstatus (sigvalidity);
+               /* extract only data part, and ignore signature part */
+               part = g_mime_multipart_get_part (multipart, 0);
+               if (sigvalidity)
+                   g_mime_signature_validity_free (sigvalidity);
+           }
+       }
+
+       if (err)
+           g_error_free (err);
+    }
+    /* end handle PGP/MIME parts */
+
+    if (selected || state->in_zone)
+       format->part_content (part);
+
     if (GMIME_IS_MULTIPART (part)) {
        GMimeMultipart *multipart = GMIME_MULTIPART (part);
        int i;