mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-21 18:38:08 +01:00
Add signature verification of PGP/MIME-signed parts with --verify.
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.
This commit is contained in:
parent
627d752501
commit
8b18efe171
8 changed files with 212 additions and 1 deletions
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
49
notmuch-gmime-session.c
Normal file
49
notmuch-gmime-session.c
Normal file
|
@ -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 */
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
11
notmuch.1
11
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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue