summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustus Winter <4winter@informatik.uni-hamburg.de>2013-05-24 17:50:24 +0200
committerPatrick Totzke <patricktotzke@gmail.com>2013-06-16 21:16:57 +0100
commit5be498b1b22ba567fe6e62eb4a7d5a116f543952 (patch)
treea6fd15b02971673f1b8a1fb2560dfa876bcd16c4
parentc39e7684769fa31ddd7d0200d5baa7e25476275d (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.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')