commit ca5398470108b8f9444b5c0f823411eb735dcfd0 Author: aagbsn aagbsn@extc.org Date: Wed Jun 20 05:55:49 2012 -0700
5463 - Adds GPG clearsign to email distributor.
Two new configuration options are added to bridgedb.conf:
EMAIL_GPG_SIGNING_ENABLED EMAIL_GPG_SIGNING_KEY
The former may be either True or False, and the latter must point to the ascii-armored key file. The keyfile must not be passphrase protected.
The gpgme library will add the secret key to the secret key ring of user who runs BridgeDB. --- README | 16 ++++++++-- bridgedb.conf | 4 ++ lib/bridgedb/Main.py | 2 + lib/bridgedb/Server.py | 79 +++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 91 insertions(+), 10 deletions(-)
diff --git a/README b/README index 6ab1545..baca007 100644 --- a/README +++ b/README @@ -20,10 +20,11 @@ To set up: - A recaptcha.net account is required. - Install these required packages: - Debian: apt-get install python-recaptcha python-beautifulsoup - - Python: pip install recaptcha-client BeautifulSoup + python-gpgme + - Python: pip install recaptcha-client BeautifulSoup pygpgme - Others: http://pypi.python.org/pypi/recaptcha-client - : http://pypi.python.org/pypi/BeautifulSoup - + http://pypi.python.org/pypi/BeautifulSoup + http://pypi.python.org/pypi/pygpgme
To re-generate and update the i18n files (in case translated strings have changed in BridgeDB): @@ -67,6 +68,15 @@ To indicate which bridges are blocked: - If this file is present, bridgedb will filter blocked bridges from responses - For GeoIP support make sure to install Maxmind GeoIP
+To sign emails with gpg: + - Add these two options to your bridgedb.conf: + + EMAIL_GPG_SIGNING_ENABLED, EMAIL_GPG_SIGNING_KEY + + The former may be either True or False, and the latter must + point to the ascii-armored key file. The keyfile must not be + passphrase protected. + To update the SQL schema: - Install sqlite3: - Debian: apt-get install sqlite3 diff --git a/bridgedb.conf b/bridgedb.conf index f044ee6..2b97d78 100644 --- a/bridgedb.conf +++ b/bridgedb.conf @@ -146,6 +146,10 @@ EMAIL_N_BRIDGES_PER_ANSWER=3 # once we have the vidalia/tor interaction fixed for everbody. EMAIL_INCLUDE_FINGERPRINTS=False
+# Configuration options for GPG signed messages +EMAIL_GPG_SIGNING_ENABLED = False +EMAIL_GPG_SIGNING_KEY = '' + #========== # Options related to unallocated bridges.
diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py index 69a4761..977f37a 100644 --- a/lib/bridgedb/Main.py +++ b/lib/bridgedb/Main.py @@ -97,6 +97,8 @@ CONFIG = Conf( EMAIL_INCLUDE_FINGERPRINTS = False, EMAIL_SMTP_HOST="127.0.0.1", EMAIL_SMTP_PORT=25, + EMAIL_GPG_SIGNING_ENABLED = False, + EMAIL_GPG_SIGNING_KEY = "bridgedb-gpg.sec",
RESERVED_SHARE=2,
diff --git a/lib/bridgedb/Server.py b/lib/bridgedb/Server.py index 8ce9ce2..5c4641c 100644 --- a/lib/bridgedb/Server.py +++ b/lib/bridgedb/Server.py @@ -6,7 +6,7 @@ This module implements the web and email interfaces to the bridge database. """
-from cStringIO import StringIO +from StringIO import StringIO import MimeWriter import rfc822 import time @@ -31,12 +31,15 @@ from random import randint from bridgedb.Raptcha import Raptcha import base64 import textwrap + from ipaddr import IPv4Address, IPv6Address from bridgedb.Dist import BadEmail, TooSoonEmail, IgnoreEmail
from bridgedb.Filters import filterBridgesByIP6 from bridgedb.Filters import filterBridgesByIP4 from bridgedb.Filters import filterBridgesByTransport + +import gpgme
try: import GeoIP @@ -458,7 +461,8 @@ def getMailResponse(lines, ctx): # Compose a warning email # MAX_EMAIL_RATE is in seconds, convert to hours body = buildSpamWarningTemplate(t) % (bridgedb.Dist.MAX_EMAIL_RATE / 3600) - return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID) + return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID, + gpgContext=ctx.gpgContext)
except IgnoreEmail, e: logging.info("Got a mail too frequently; ignoring %r: %s.", @@ -483,7 +487,8 @@ def getMailResponse(lines, ctx):
body = buildMessageTemplate(t) % answer # Generate the message. - return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID) + return composeEmail(ctx.fromAddr, clientAddr, subject, body, msgID, + gpgContext=ctx.gpgContext)
def buildMessageTemplate(t): msg_template = t.gettext(I18n.BRIDGEDB_TEXT[5]) + "\n\n" \ @@ -575,6 +580,9 @@ class MailContext: # The number of bridges to send for each email. self.N = cfg.EMAIL_N_BRIDGES_PER_ANSWER
+ # Initialize a gpg context or set to None for backward compatibliity. + self.gpgContext = getGPGContext(cfg) + self.cfg = cfg
class MailMessage: @@ -690,7 +698,9 @@ def getCCFromRequest(request): return path.lower() return None
-def composeEmail(fromAddr, clientAddr, subject, body, msgID=False): +def composeEmail(fromAddr, clientAddr, subject, body, msgID=False, + gpgContext=None): + f = StringIO() w = MimeWriter.MimeWriter(f) w.addheader("From", fromAddr) @@ -702,10 +712,65 @@ def composeEmail(fromAddr, clientAddr, subject, body, msgID=False): w.addheader("In-Reply-To", msgID) w.addheader("Date", twisted.mail.smtp.rfc822date()) mailbody = w.startbody("text/plain") - mailbody.write(body)
- f.seek(0) - logging.debug(f.readlines()) + # gpg-clearsign messages + if gpgContext: + signature = StringIO() + plaintext = StringIO(body) + sigs = gpgContext.sign(plaintext, signature, gpgme.SIG_MODE_CLEAR) + if (len(sigs) != 1): + logging.warn('Failed to sign message!') + signature.seek(0) + [mailbody.write(l) for l in signature] + else: + mailbody.write(body) + f.seek(0) logging.info("Email looks good; we should send an answer.") return clientAddr, f + +def getGPGContext(cfg): + """ Returns a gpgme Context() with the signers initialized by the keyfile + specified by the option EMAIL_GPG_SIGNING_KEY in bridgedb.conf, or None + if the option was not enabled or unable to initialize. + + The key should not be protected by a passphrase. + """ + try: + # must have enabled signing and specified a key file + if not cfg.EMAIL_GPG_SIGNING_ENABLED or not cfg.EMAIL_GPG_SIGNING_KEY: + return None + except AttributeError: + return None + + try: + # import the key + keyfile = open(cfg.EMAIL_GPG_SIGNING_KEY) + logging.debug("Opened GPG Keyfile %s" % cfg.EMAIL_GPG_SIGNING_KEY) + ctx = gpgme.Context() + result = ctx.import_(keyfile) + + assert len(result.imports) == 1 + fingerprint = result.imports[0][0] + keyfile.close() + logging.debug("GPG Key with fingerprint %s imported" % fingerprint) + + ctx.armor = True + ctx.signers = [ctx.get_key(fingerprint)] + assert len(ctx.signers) == 1 + + # make sure we can sign + message = StringIO('Test') + signature = StringIO() + new_sigs = ctx.sign(message, signature, gpgme.SIG_MODE_CLEAR) + assert len(new_sigs) == 1 + + # return the ctx + return ctx + + except IOError, e: + # exit noisily if keyfile not found + exit(e) + except AssertionError: + # exit noisily if key does not pass tests + exit('Invalid GPG Signing Key')