Skip to content

Commit acebcec

Browse files
author
Justus Pendleton
committed
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.
1 parent 30e35ef commit acebcec

File tree

4 files changed

+174
-31
lines changed

4 files changed

+174
-31
lines changed

doc/customizing.txt

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Customising Roundup
33
===================
44

5-
:Version: $Revision: 1.221 $
5+
:Version: $Revision: 1.222 $
66

77
.. This document borrows from the ZopeBook section on ZPT. The original is at:
88
http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -95,6 +95,14 @@ Section **main**
9595
Path to the HTML templates directory. The path may be either absolute
9696
or relative to the directory containig this config file.
9797

98+
static_files -- default *blank*
99+
Path to directory holding additional static files available via Web
100+
UI. This directory may contain sitewide images, CSS stylesheets etc.
101+
and is searched for these files prior to the TEMPLATES directory
102+
specified above. If this option is not set, all static files are
103+
taken from the TEMPLATES directory The path may be either absolute or
104+
relative to the directory containig this config file.
105+
98106
admin_email -- ``roundup-admin``
99107
Email address that roundup will complain to if it runs into trouble. If
100108
the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
@@ -150,6 +158,9 @@ Section **main**
150158
your tracker. See the indexer source for the default list of
151159
stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``).
152160

161+
umask -- ``02``
162+
Defines the file creation mode mask.
163+
153164
Section **tracker**
154165
name -- ``Roundup issue tracker``
155166
A descriptive name for your roundup instance.
@@ -164,6 +175,11 @@ Section **tracker**
164175
email -- ``issue_tracker``
165176
Email address that mail to roundup should go to.
166177

178+
language -- default *blank*
179+
Default locale name for this tracker. If this option is not set, the
180+
language is determined by the environment variable LANGUAGE, LC_ALL,
181+
LC_MESSAGES, or LANG, in that order of preference.
182+
167183
Section **web**
168184
http_auth -- ``yes``
169185
Whether to use HTTP Basic Authentication, if present.
@@ -204,6 +220,13 @@ Section **rdbms**
204220
password -- ``roundup``
205221
Database user password.
206222

223+
read_default_file -- ``~/.my.cnf``
224+
Name of the MySQL defaults file. Only used in MySQL connections.
225+
226+
read_default_group -- ``roundup``
227+
Name of the group to use in the MySQL defaults file. Only used in
228+
MySQL connections.
229+
207230
Section **logging**
208231
config -- default *blank*
209232
Path to configuration file for standard Python logging module. If this
@@ -277,6 +300,16 @@ Section **mail**
277300
precedence. The path may be either absolute or relative to the directory
278301
containig this config file.
279302

303+
add_authorinfo -- ``yes``
304+
Add a line with author information at top of all messages send by
305+
roundup.
306+
307+
add_authoremail -- ``yes``
308+
Add the mail address of the author to the author information at the
309+
top of all messages. If this is false but add_authorinfo is true,
310+
only the name of the actor is added which protects the mail address
311+
of the actor from being exposed at mail archives, etc.
312+
280313
Section **mailgw**
281314
Roundup Mail Gateway options
282315

@@ -294,6 +327,10 @@ Section **mailgw**
294327
Default class to use in the mailgw if one isn't supplied in email subjects.
295328
To disable, leave the value blank.
296329

330+
language -- default *blank*
331+
Default locale name for the tracker mail gateway. If this option is
332+
not set, mail gateway will use the language of the tracker instance.
333+
297334
subject_prefix_parsing -- ``strict``
298335
Controls the parsing of the [prefix] on subject lines in incoming emails.
299336
``strict`` will return an error to the sender if the [prefix] is not
@@ -319,6 +356,42 @@ Section **mailgw**
319356
an issue for the interval after the issue's creation or last activity.
320357
The interval is a standard Roundup interval.
321358

359+
refwd_re -- ``(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+``
360+
Regular expression matching a single reply or forward prefix
361+
prepended by the mailer. This is explicitly stripped from the
362+
subject during parsing. Value is Python Regular Expression
363+
(UTF8-encoded).
364+
365+
origmsg_re -- `` ^[>|\s]*-----\s?Original Message\s?-----$``
366+
Regular expression matching start of an original message if quoted
367+
the in body. Value is Python Regular Expression (UTF8-encoded).
368+
369+
sign_re -- ``^[>|\s]*-- ?$``
370+
Regular expression matching the start of a signature in the message
371+
body. Value is Python Regular Expression (UTF8-encoded).
372+
373+
eol_re -- ``[\r\n]+``
374+
Regular expression matching end of line. Value is Python Regular
375+
Expression (UTF8-encoded).
376+
377+
blankline_re -- ``[\r\n]+\s*[\r\n]+``
378+
Regular expression matching a blank line. Value is Python Regular
379+
Expression (UTF8-encoded).
380+
381+
Section **pgp**
382+
OpenPGP mail processing options
383+
384+
enable -- ``no``
385+
Enable PGP processing. Requires pyme.
386+
387+
roles -- default *blank*
388+
If specified, a comma-separated list of roles to perform PGP
389+
processing on. If not specified, it happens for all users.
390+
391+
homedir -- default *blank*
392+
Location of PGP directory. Defaults to $HOME/.gnupg if not
393+
specified.
394+
322395
Section **nosy**
323396
Nosy messages sending
324397

@@ -349,6 +422,12 @@ Section **nosy**
349422
a separate email is sent to each recipient. If ``single`` then a single
350423
email is sent with each recipient as a CC address.
351424

425+
max_attachment_size -- ``2147483647``
426+
Attachments larger than the given number of bytes won't be attached
427+
to nosy mails. They will be replaced by a link to the tracker's
428+
download page for the file.
429+
430+
352431
You may generate a new default config file using the ``roundup-admin
353432
genconfig`` command.
354433

doc/installation.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Installing Roundup
33
==================
44

5-
:Version: $Revision: 1.126 $
5+
:Version: $Revision: 1.127 $
66

77
.. contents::
88
:depth: 2
@@ -81,10 +81,17 @@ pyopenssl
8181
proxy through a server with SSL support (e.g. apache) then this is
8282
unnecessary.
8383

84+
pyme
85+
If pyme_ is installed you can configure the mail gateway to perform
86+
verification or decryption of incoming OpenPGP MIME messages. When
87+
configured, you can require email to be cryptographically signed
88+
before roundup will allow it to make modifications to issues.
89+
8490
.. _Xapian: http://www.xapian.org/
8591
.. _pytz: http://www.python.org/pypi/pytz
8692
.. _Olson tz database: http://www.twinsun.com/tz/tz-link.htm
8793
.. _pyopenssl: http://pyopenssl.sourceforge.net
94+
.. _pyme: http://pyme.sourceforge.net
8895

8996

9097
Getting Roundup

roundup/configuration.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Roundup Issue Tracker configuration support
22
#
3-
# $Id: configuration.py,v 1.48 2007-09-22 07:25:34 jpend Exp $
3+
# $Id: configuration.py,v 1.49 2007-09-26 03:20:21 jpend Exp $
44
#
55
__docformat__ = "restructuredtext"
66

@@ -720,6 +720,10 @@ def str2value(self, value):
720720
("pgp", (
721721
(BooleanOption, "enable", "no",
722722
"Enable PGP processing. Requires pyme."),
723+
(NullableOption, "roles", "",
724+
"If specified, a comma-separated list of roles to perform\n"
725+
"PGP processing on. If not specified, it happens for all\n"
726+
"users."),
723727
(NullableOption, "homedir", "",
724728
"Location of PGP directory. Defaults to $HOME/.gnupg if\n"
725729
"not specified."),

roundup/mailgw.py

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class node. Any parts of other types are each stored in separate files
7373
an exception, the original message is bounced back to the sender with the
7474
explanatory message given in the exception.
7575
76-
$Id: mailgw.py,v 1.191 2007-09-24 09:52:18 a1s Exp $
76+
$Id: mailgw.py,v 1.192 2007-09-26 03:20:21 jpend Exp $
7777
"""
7878
__docformat__ = 'restructuredtext'
7979

@@ -146,29 +146,70 @@ def getparam(str, param):
146146
return rfc822.unquote(f[i+1:].strip())
147147
return None
148148

149-
def check_pgp_sigs(sig):
150-
''' Theoretically a PGP message can have several signatures. GPGME returns
151-
status on all signatures in a linked list. Walk that linked list making
152-
sure all signatures are valid.
149+
def gpgh_key_getall(key, attr):
150+
''' return list of given attribute for all uids in
151+
a key
153152
'''
154-
while sig != None:
155-
if not sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
156-
# try to narrow down the actual problem to give a more useful
157-
# message in our bounce
158-
if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
159-
raise MailUsageError, \
160-
_("Message signed with unknown key: %s") % sig.fpr
161-
elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
162-
raise MailUsageError, \
163-
_("Message signed with an expired key: %s") % sig.fpr
164-
elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
165-
raise MailUsageError, \
166-
_("Message signed with a revoked key: %s") % sig.fpr
167-
else:
168-
raise MailUsageError, \
169-
_("Invalid PGP signature detected.")
153+
u = key.uids
154+
while u:
155+
yield getattr(u, attr)
156+
u = u.next
157+
158+
def gpgh_sigs(sig):
159+
''' more pythonic iteration over GPG signatures '''
160+
while sig:
161+
yield sig
170162
sig = sig.next
171163

164+
165+
def iter_roles(roles):
166+
''' handle the text processing of turning the roles list
167+
into something python can use more easily
168+
'''
169+
for role in [x.lower().strip() for x in roles.split(',')]:
170+
yield role
171+
172+
def user_has_role(db, userid, role_list):
173+
''' see if the given user has any roles that appear
174+
in the role_list
175+
'''
176+
for role in iter_roles(db.user.get(userid, 'roles')):
177+
if role in iter_roles(role_list):
178+
return True
179+
return False
180+
181+
182+
def check_pgp_sigs(sig, gpgctx, author):
183+
''' Theoretically a PGP message can have several signatures. GPGME
184+
returns status on all signatures in a linked list. Walk that
185+
linked list looking for the author's signature
186+
'''
187+
for sig in gpgh_sigs(sig):
188+
key = gpgctx.get_key(sig.fpr, False)
189+
# we really only care about the signature of the user who
190+
# submitted the email
191+
if key and (author in gpgh_key_getall(key, 'email')):
192+
if sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
193+
return True
194+
else:
195+
# try to narrow down the actual problem to give a more useful
196+
# message in our bounce
197+
if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
198+
raise MailUsageError, \
199+
_("Message signed with unknown key: %s") % sig.fpr
200+
elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
201+
raise MailUsageError, \
202+
_("Message signed with an expired key: %s") % sig.fpr
203+
elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
204+
raise MailUsageError, \
205+
_("Message signed with a revoked key: %s") % sig.fpr
206+
else:
207+
raise MailUsageError, \
208+
_("Invalid PGP signature detected.")
209+
210+
# we couldn't find a key belonging to the author of the email
211+
raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
212+
172213
class Message(mimetools.Message):
173214
''' subclass mimetools.Message so we can retrieve the parts of the
174215
message...
@@ -349,7 +390,7 @@ def pgp_encrypted(self):
349390
return self.gettype() == 'multipart/encrypted' \
350391
and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
351392

352-
def decrypt(self):
393+
def decrypt(self, author):
353394
''' decrypt an OpenPGP MIME message
354395
This message must be signed as well as encrypted using the "combined"
355396
method. The decrypted contents are returned as a new message.
@@ -375,7 +416,7 @@ def decrypt(self):
375416
# key to send it to us. now check the signatures to see if it
376417
# was signed by someone we trust
377418
result = context.op_verify_result()
378-
check_pgp_sigs(result.signatures)
419+
check_pgp_sigs(result.signatures, context, author)
379420

380421
plaintext.seek(0,0)
381422
# pyme.core.Data implements a seek method with a different signature
@@ -386,7 +427,7 @@ def decrypt(self):
386427
c.seek(0)
387428
return Message(c)
388429

389-
def verify_signature(self):
430+
def verify_signature(self, author):
390431
''' verify the signature of an OpenPGP MIME message
391432
This only handles detached signatures. Old style
392433
PGP mail (i.e. '-----BEGIN PGP SIGNED MESSAGE----')
@@ -419,7 +460,7 @@ def verify_signature(self):
419460

420461
# check all signatures for validity
421462
result = context.op_verify_result()
422-
check_pgp_sigs(result.signatures)
463+
check_pgp_sigs(result.signatures, context, author)
423464

424465
class MailGW:
425466

@@ -1133,19 +1174,31 @@ def handle_message(self, message):
11331174

11341175
# if they've enabled PGP processing then verify the signature
11351176
# or decrypt the message
1136-
if self.instance.config.PGP_ENABLE:
1177+
1178+
# if PGP_ROLES is specified the user must have a Role in the list
1179+
# or we will skip PGP processing
1180+
def pgp_role():
1181+
if self.instance.config.PGP_ROLES:
1182+
return user_has_role(self.db, author,
1183+
self.instance.config.PGP_ROLES)
1184+
else:
1185+
return True
1186+
1187+
if self.instance.config.PGP_ENABLE and pgp_role():
11371188
assert pyme, 'pyme is not installed'
1189+
# signed/encrypted mail must come from the primary address
1190+
author_address = self.db.user.get(author, 'address')
11381191
if self.instance.config.PGP_HOMEDIR:
11391192
os.environ['GNUPGHOME'] = self.instance.config.PGP_HOMEDIR
11401193
if message.pgp_signed():
1141-
message.verify_signature()
1194+
message.verify_signature(author_address)
11421195
elif message.pgp_encrypted():
11431196
# replace message with the contents of the decrypted
11441197
# message for content extraction
11451198
# TODO: encrypted message handling is far from perfect
11461199
# bounces probably include the decrypted message, for
11471200
# instance :(
1148-
message = message.decrypt()
1201+
message = message.decrypt(author_address)
11491202
else:
11501203
raise MailUsageError, _("""
11511204
This tracker has been configured to require all email be PGP signed or

0 commit comments

Comments
 (0)