diff roundup/mailgw.py @ 3922:586679a314f7

role checking for PGP mail and docs Erik's suggestion to allow the admin to specify a set of roles to perform PGP processing on seemed like a reasonable one I implemented it. There is a new config option to control it. I also realized that the signature verification had a slight problem: it was simply checking for a valid, known signature before continuing on. If another user in the keyring forged mail it was pass the PGP check and then modify the db as the forged user. I changed the logic to make sure that the author of the email matches the key of the verifying signature. As I was adding the documentation for the PGP processing I noticed that there were several other new-ish options that didn't appear in customizing.txt so I added them as well.
author Justus Pendleton <jpend@users.sourceforge.net>
date Wed, 26 Sep 2007 03:20:21 +0000
parents 1f3310c0a100
children 1dd64778bc45
line wrap: on
line diff
--- a/roundup/mailgw.py	Wed Sep 26 03:07:55 2007 +0000
+++ b/roundup/mailgw.py	Wed Sep 26 03:20:21 2007 +0000
@@ -73,7 +73,7 @@
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception.
 
-$Id: mailgw.py,v 1.191 2007-09-24 09:52:18 a1s Exp $
+$Id: mailgw.py,v 1.192 2007-09-26 03:20:21 jpend Exp $
 """
 __docformat__ = 'restructuredtext'
 
@@ -146,28 +146,69 @@
                 return rfc822.unquote(f[i+1:].strip())
     return None
 
-def check_pgp_sigs(sig):
-    ''' Theoretically a PGP message can have several signatures. GPGME returns
-        status on all signatures in a linked list. Walk that linked list making
-        sure all signatures are valid.
+def gpgh_key_getall(key, attr):
+    ''' return list of given attribute for all uids in
+        a key
+    '''
+    u = key.uids
+    while u:
+        yield getattr(u, attr)
+        u = u.next
+
+def gpgh_sigs(sig):
+    ''' more pythonic iteration over GPG signatures '''
+    while sig:
+        yield sig
+        sig = sig.next
+
+
+def iter_roles(roles):
+    ''' handle the text processing of turning the roles list
+        into something python can use more easily
+    '''
+    for role in [x.lower().strip() for x in roles.split(',')]:
+        yield role
+
+def user_has_role(db, userid, role_list):
+    ''' see if the given user has any roles that appear
+        in the role_list
     '''
-    while sig != None:
-        if not sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
-            # try to narrow down the actual problem to give a more useful
-            # message in our bounce
-            if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
-                raise MailUsageError, \
-                    _("Message signed with unknown key: %s") % sig.fpr
-            elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
-                raise MailUsageError, \
-                    _("Message signed with an expired key: %s") % sig.fpr
-            elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
-                raise MailUsageError, \
-                    _("Message signed with a revoked key: %s") % sig.fpr
+    for role in iter_roles(db.user.get(userid, 'roles')):
+        if role in iter_roles(role_list):
+            return True
+    return False
+
+
+def check_pgp_sigs(sig, gpgctx, author):
+    ''' Theoretically a PGP message can have several signatures. GPGME
+        returns status on all signatures in a linked list. Walk that
+        linked list looking for the author's signature
+    '''
+    for sig in gpgh_sigs(sig):
+        key = gpgctx.get_key(sig.fpr, False)
+        # we really only care about the signature of the user who
+        # submitted the email
+        if key and (author in gpgh_key_getall(key, 'email')):
+            if sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
+                return True
             else:
-                raise MailUsageError, \
-                    _("Invalid PGP signature detected.")
-        sig = sig.next
+                # try to narrow down the actual problem to give a more useful
+                # message in our bounce
+                if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
+                    raise MailUsageError, \
+                        _("Message signed with unknown key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
+                    raise MailUsageError, \
+                        _("Message signed with an expired key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
+                    raise MailUsageError, \
+                        _("Message signed with a revoked key: %s") % sig.fpr
+                else:
+                    raise MailUsageError, \
+                        _("Invalid PGP signature detected.")
+
+    # we couldn't find a key belonging to the author of the email
+    raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
 
 class Message(mimetools.Message):
     ''' subclass mimetools.Message so we can retrieve the parts of the
@@ -349,7 +390,7 @@
         return self.gettype() == 'multipart/encrypted' \
             and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
 
-    def decrypt(self):
+    def decrypt(self, author):
         ''' decrypt an OpenPGP MIME message
             This message must be signed as well as encrypted using the "combined"
             method. The decrypted contents are returned as a new message.
@@ -375,7 +416,7 @@
         # key to send it to us. now check the signatures to see if it
         # was signed by someone we trust
         result = context.op_verify_result()
-        check_pgp_sigs(result.signatures)
+        check_pgp_sigs(result.signatures, context, author)
 
         plaintext.seek(0,0)
         # pyme.core.Data implements a seek method with a different signature
@@ -386,7 +427,7 @@
         c.seek(0)
         return Message(c)
 
-    def verify_signature(self):
+    def verify_signature(self, author):
         ''' verify the signature of an OpenPGP MIME message
             This only handles detached signatures. Old style
             PGP mail (i.e. '-----BEGIN PGP SIGNED MESSAGE----')
@@ -419,7 +460,7 @@
 
         # check all signatures for validity
         result = context.op_verify_result()
-        check_pgp_sigs(result.signatures)
+        check_pgp_sigs(result.signatures, context, author)
 
 class MailGW:
 
@@ -1133,19 +1174,31 @@
 
         # if they've enabled PGP processing then verify the signature
         # or decrypt the message
-        if self.instance.config.PGP_ENABLE:
+
+        # if PGP_ROLES is specified the user must have a Role in the list
+        # or we will skip PGP processing
+        def pgp_role():
+            if self.instance.config.PGP_ROLES:
+                return user_has_role(self.db, author,
+                    self.instance.config.PGP_ROLES)
+            else:
+                return True
+
+        if self.instance.config.PGP_ENABLE and pgp_role():
             assert pyme, 'pyme is not installed'
+            # signed/encrypted mail must come from the primary address
+            author_address = self.db.user.get(author, 'address')
             if self.instance.config.PGP_HOMEDIR:
                 os.environ['GNUPGHOME'] = self.instance.config.PGP_HOMEDIR
             if message.pgp_signed():
-                message.verify_signature()
+                message.verify_signature(author_address)
             elif message.pgp_encrypted():
                 # replace message with the contents of the decrypted
                 # message for content extraction
                 # TODO: encrypted message handling is far from perfect
                 # bounces probably include the decrypted message, for
                 # instance :(
-                message = message.decrypt()
+                message = message.decrypt(author_address)
             else:
                 raise MailUsageError, _("""
 This tracker has been configured to require all email be PGP signed or

Roundup Issue Tracker: http://roundup-tracker.org/