cli/show: add information about which headers were protected

The header-mask member of the per-message crypto object allows a
clever UI frontend to mark whether a header was protected (or not).
And if it was protected, it contains enough information to show useful
detail to an interested user.  For example, an MUA could offer a "show
what this message's Subject looked like on the wire" feature in expert
mode.

As before, we only handle Subject for now, but we might be able to
handle other headers in the future.

Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>

Amended by db: tweaked schemata notation.
This commit is contained in:
Daniel Kahn Gillmor 2019-05-27 18:14:16 -04:00 committed by David Bremner
parent 1c7fbbcc99
commit 56416a5470
3 changed files with 35 additions and 5 deletions

View file

@ -4,9 +4,9 @@ format (currently JSON and S-Expressions).
[]'s indicate lists. List items can be marked with a '?', meaning []'s indicate lists. List items can be marked with a '?', meaning
they are optional; or a '*', meaning there can be zero or more of that they are optional; or a '*', meaning there can be zero or more of that
item. {}'s indicate an object that maps from field identifiers to item. {}'s indicate an object that maps from field identifiers to
values. An object field marked '?' is optional. |'s indicate values. An object field marked '?' is optional; one marked with '*'
alternates (e.g., int|string means something can be an int or a can repeat (with a different name). |'s indicate alternates (e.g.,
string). int|string means something can be an int or a string).
For S-Expression output, lists are printed delimited by () instead of For S-Expression output, lists are printed delimited by () instead of
[]. Objects are printed as p-lists, i.e. lists where the keys and values []. Objects are printed as p-lists, i.e. lists where the keys and values
@ -48,6 +48,9 @@ threadid = string
# Message ID, sans "id:" # Message ID, sans "id:"
messageid = string messageid = string
# E-mail header name, sans trailing colon, like "Subject" or "In-Reply-To"
header_name = string
notmuch show schema notmuch show schema
------------------- -------------------
@ -88,9 +91,15 @@ crypto = {
status: sigstatus, status: sigstatus,
# was the set of signatures described under encrypted cover? # was the set of signatures described under encrypted cover?
encrypted: bool, encrypted: bool,
# which of the headers is covered by sigstatus?
headers: [header_name*]
}, },
decrypted?: { decrypted?: {
status: msgdecstatus, status: msgdecstatus,
# map encrypted headers that differed from the outside headers.
# the value of each item in the map is what that field showed externally
# (maybe null if it was not present in the external headers).
header-mask: { header_name*: string|null }
} }
} }

View file

@ -645,6 +645,12 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
sp->map_key (sp, "encrypted"); sp->map_key (sp, "encrypted");
sp->boolean (sp, msg_crypto->signature_encrypted); sp->boolean (sp, msg_crypto->signature_encrypted);
} }
if (msg_crypto->payload_subject) {
sp->map_key (sp, "headers");
sp->begin_list (sp);
sp->string (sp, "Subject");
sp->end (sp);
}
sp->end (sp); sp->end (sp);
} }
if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) { if (msg_crypto->decryption_status != NOTMUCH_MESSAGE_DECRYPTED_NONE) {
@ -652,6 +658,21 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
sp->begin_map (sp); sp->begin_map (sp);
sp->map_key (sp, "status"); sp->map_key (sp, "status");
sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ? "full" : "partial"); sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ? "full" : "partial");
if (msg_crypto->payload_subject) {
const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part);
if (subject == NULL || strcmp (subject, msg_crypto->payload_subject)) {
/* protected subject differs from the external header */
sp->map_key (sp, "header-mask");
sp->begin_map (sp);
sp->map_key (sp, "Subject");
if (subject == NULL)
sp->null (sp);
else
sp->string (sp, subject);
sp->end (sp);
}
}
sp->end (sp); sp->end (sp);
} }
} }

View file

@ -22,7 +22,7 @@ test_json_nodes <<<"$output" \
test_begin_subtest "verify protected header is visible with decryption" test_begin_subtest "verify protected header is visible with decryption"
output=$(notmuch show --decrypt=true --format=json id:protected-header@crypto.notmuchmail.org) output=$(notmuch show --decrypt=true --format=json id:protected-header@crypto.notmuchmail.org)
test_json_nodes <<<"$output" \ test_json_nodes <<<"$output" \
'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \ 'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
'subject:[0][0][0]["headers"]["Subject"]="This is a protected header"' 'subject:[0][0][0]["headers"]["Subject"]="This is a protected header"'
test_begin_subtest "misplaced protected headers should not be made visible during decryption" test_begin_subtest "misplaced protected headers should not be made visible during decryption"
@ -58,7 +58,7 @@ test_json_nodes <<<"$output" \
test_begin_subtest "verify nested message/rfc822 protected header is visible" test_begin_subtest "verify nested message/rfc822 protected header is visible"
output=$(notmuch show --decrypt=true --format=json id:nested-rfc822-message@crypto.notmuchmail.org) output=$(notmuch show --decrypt=true --format=json id:nested-rfc822-message@crypto.notmuchmail.org)
test_json_nodes <<<"$output" \ test_json_nodes <<<"$output" \
'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full"}}' \ 'crypto:[0][0][0]["crypto"]={"decrypted": {"status": "full", "header-mask": {"Subject": "Subject Unavailable"}}}' \
'subject:[0][0][0]["headers"]["Subject"]="This is a message using draft-melnikov-smime-header-signing"' 'subject:[0][0][0]["headers"]["Subject"]="This is a message using draft-melnikov-smime-header-signing"'
test_done test_done