diff options
author | Justus Winter <4winter@informatik.uni-hamburg.de> | 2013-05-24 17:50:24 +0200 |
---|---|---|
committer | Patrick Totzke <patricktotzke@gmail.com> | 2013-06-16 21:16:57 +0100 |
commit | 5be498b1b22ba567fe6e62eb4a7d5a116f543952 (patch) | |
tree | a6fd15b02971673f1b8a1fb2560dfa876bcd16c4 | |
parent | c39e7684769fa31ddd7d0200d5baa7e25476275d (diff) |
Verify OpenPGP signatures and display the result
Verify OpenPGP signatures as specified in RFC 3156. Display the result
in the header list above the message.
Signed-off-by: Justus Winter <4winter@informatik.uni-hamburg.de>
-rw-r--r-- | alot/crypto.py | 18 | ||||
-rw-r--r-- | alot/db/message.py | 4 | ||||
-rw-r--r-- | alot/db/utils.py | 94 | ||||
-rw-r--r-- | alot/widgets/thread.py | 6 |
4 files changed, 119 insertions, 3 deletions
diff --git a/alot/crypto.py b/alot/crypto.py index 6281ecfd..74f1f5c5 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -191,6 +191,24 @@ def encrypt(plaintext_str, keys=None): return encrypted +def verify_detached(message, signature): + '''Verifies whether the message is authentic by checking the + signature. + + :param message: the message as `str` + :param signature: a `str` containing an OpenPGP signature + :returns: a list of :class:`gpgme.Signature` + :raises: :class:`~alot.errors.GPGProblem` if the verification fails + ''' + message_data = StringIO(message) + signature_data = StringIO(signature) + ctx = gpgme.Context() + try: + return ctx.verify(signature_data, message_data, None) + except gpgme.GpgmeError as e: + raise GPGProblem(e.message, code=e.code) + + def hash_key(key): """ Returns a hash of the given key. This is a workaround for diff --git a/alot/db/message.py b/alot/db/message.py index 65ef2dc8..002f7cf9 100644 --- a/alot/db/message.py +++ b/alot/db/message.py @@ -10,7 +10,7 @@ from notmuch import NullPointerError import alot.helper as helper from alot.settings import settings -from utils import extract_headers, extract_body +from utils import extract_headers, extract_body, message_from_file from alot.db.utils import decode_header from attachment import Attachment @@ -69,7 +69,7 @@ class Message(object): if not self._email: try: f_mail = open(path) - self._email = email.message_from_file(f_mail) + self._email = message_from_file(f_mail) f_mail.close() except IOError: self._email = email.message_from_string(warning) diff --git a/alot/db/utils.py b/alot/db/utils.py index 1280667a..545ba2b0 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -12,13 +12,107 @@ from email.iterators import typed_subpart_iterator import logging import mailcap +import alot.crypto as crypto import alot.helper as helper +from alot.errors import GPGProblem from alot.settings import settings from alot.helper import string_sanitize from alot.helper import string_decode from alot.helper import parse_mailcap_nametemplate from alot.helper import split_commandstring +X_SIGNATURE_VALID_HEADER = 'X-Alot-OpenPGP-Signature-Valid' +X_SIGNATURE_MESSAGE_HEADER = 'X-Alot-OpenPGP-Signature-Message' + + +def add_signature_headers(mail, sigs, error_msg): + '''Add pseudo headers to the mail indicating whether the signature + verification was successful. + + :param mail: :class:`email.message.Message` the message to entitle + :param sigs: list of :class:`gpgme.Signature` + :param error_msg: `str` containing an error message, the empty + string indicating no error + ''' + sig_from = '' + + if len(sigs) == 0: + error_msg = error_msg or 'no signature found' + else: + try: + sig_from = crypto.get_key(sigs[0].fpr).uids[0].uid + except: + sig_from = sigs[0].fpr + + mail.add_header( + X_SIGNATURE_VALID_HEADER, + 'False' if error_msg else 'True', + ) + mail.add_header( + X_SIGNATURE_MESSAGE_HEADER, + 'Invalid: {0}'.format(error_msg) + if error_msg else + 'Valid: {0}'.format(sig_from), + ) + + +def message_from_file(handle): + '''Reads a mail from the given file-like object and returns an email + object, very much like email.message_from_file. In addition to + that OpenPGP encrypted data is detected and decrypted. If this + succeeds, any mime messages found in the recovered plaintext + message are added to the returned message object. + + :param handle: a file-like object + :returns: :class:`email.message.Message` possibly augmented with decrypted data + ''' + m = email.message_from_file(handle) + + # make sure noone smuggles a token in (data from m is untrusted) + del m[X_SIGNATURE_VALID_HEADER] + del m[X_SIGNATURE_MESSAGE_HEADER] + + # handle OpenPGP signed data + if m.is_multipart() and m.get_content_subtype() == 'signed': + # RFC 3156 is quite strict: + # * exactly two messages + # * the second is of type 'application/pgp-signature' + # * the second contains the detached signature + + malformed = False + if len(m.get_payload()) != 2: + malformed = 'expected exactly two messages, got {0}'.format( + len(m.get_payload())) + + want = 'application/pgp-signature' + ct = m.get_payload(1).get_content_type() + if ct != want: + malformed = 'expected Content-Type: {0}, got: {1}'.format( + want, ct) + + p = {k:v for k, v in m.get_params()} + if p['protocol'] != want: + malformed = 'expected protocol={0}, got: {1}'.format( + want, p['protocol']) + + # TODO: RFC 3156 says the alg has to be lower case, but I've + # seen a message with 'PGP-'. maybe we should be more + # permissive here, or maybe not, this is crypto stuff... + if not p['micalg'].startswith('pgp-'): + malformed = 'expected micalg=pgp-..., got: {0}'.format(p['micalg']) + + sigs = [] + if not malformed: + try: + sigs = crypto.verify_detached(m.get_payload(0).as_string(), + m.get_payload(1).get_payload()) + except GPGProblem as e: + malformed = str(e) + + add_signature_headers(m, sigs, malformed) + + return m + def extract_headers(mail, headers=None): """ diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py index 17b43883..32229d2e 100644 --- a/alot/widgets/thread.py +++ b/alot/widgets/thread.py @@ -8,7 +8,7 @@ import urwid import logging from alot.settings import settings -from alot.db.utils import decode_header +from alot.db.utils import decode_header, X_SIGNATURE_MESSAGE_HEADER from alot.helper import tag_cmp from alot.widgets.globals import TagWidget from alot.widgets.globals import AttachmentWidget @@ -303,6 +303,10 @@ class MessageTree(CollapsibleTree): values.append(t) lines.append((key, ', '.join(values))) + # OpenPGP pseudo headers + if mail[X_SIGNATURE_MESSAGE_HEADER]: + lines.append(('PGP-Signature', mail[X_SIGNATURE_MESSAGE_HEADER])) + key_att = settings.get_theming_attribute('thread', 'header_key') value_att = settings.get_theming_attribute('thread', 'header_value') gaps_att = settings.get_theming_attribute('thread', 'header') |