summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/crypto.py18
-rw-r--r--alot/db/message.py4
-rw-r--r--alot/db/utils.py94
-rw-r--r--alot/widgets/thread.py6
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')