commit 7b75cff76edac559e6dbf6f24584e86f948aa597 Author: Isis Lovecruft isis@torproject.org Date: Wed Apr 16 22:33:33 2014 +0000
Move EmailServer.getGPGContext() → crypto.getGPGContext().
* ADD new class, `bridgedb.crypto.LessCrypticGpgmeError` for parsing GPGME errors.
Usually, these errors are just a tuple of integers, i.e.: (7, 32586, "Unsupported command") which means absolutely nothing to me, and is not helping with debugging. It takes a `gpgme.GpgmeError` as its initialisation argument, and it automatically makes sense of the stupid thing.
Consequently, the getGPGContext() and signing done with GPGME now have better error handling. --- lib/bridgedb/EmailServer.py | 66 +------------------ lib/bridgedb/crypto.py | 153 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 65 deletions(-)
diff --git a/lib/bridgedb/EmailServer.py b/lib/bridgedb/EmailServer.py index efa7514..eed96bf 100644 --- a/lib/bridgedb/EmailServer.py +++ b/lib/bridgedb/EmailServer.py @@ -28,6 +28,7 @@ from zope.interface import implements from bridgedb import Dist from bridgedb import I18n from bridgedb import safelog +from bridgedb.crypto import getGPGContext from bridgedb.Filters import filterBridgesByIP6 from bridgedb.Filters import filterBridgesByIP4 from bridgedb.Filters import filterBridgesByTransport @@ -500,68 +501,3 @@ def addSMTPServer(cfg, dist, sched): lc = LoopingCall(dist.cleanDatabase) lc.start(1800, now=False) return factory - - - - - -def getGPGContext(cfg): - """Import a key from a file and initialise a context for GnuPG operations. - - The key should not be protected by a passphrase, and should have the - signing flag enabled. - - :type cfg: :class:`bridgedb.persistent.Conf` - :param cfg: The loaded config file. - :rtype: :class:`gpgme.Context` or None - :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 was unable to initialize. - """ - 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 - - keyfile = None - ctx = gpgme.Context() - - try: - logging.debug("Opening GPG keyfile %s..." % cfg.EMAIL_GPG_SIGNING_KEY) - keyfile = open(cfg.EMAIL_GPG_SIGNING_KEY) - key = ctx.import_(keyfile) - - if not (len(key.imports) > 0): - logging.debug( - "Unexpected result from gpgme.Context.import_(): %r" % key) - raise gpgme.GpgmeError("Could not import GnuPG key from file %r" - % cfg.EMAIL_GPG_SIGNING_KEY) - - fingerprint = key.imports[0][0] - logging.info("GPG Key with fingerprint %s imported" % fingerprint) - - ctx.armor = True - ctx.signers = [ctx.get_key(fingerprint)] - - logging.info("Testing signature created with GnuPG key...") - message = io.StringIO('Test') - new_sigs = ctx.sign(message, io.StringIO(), gpgme.SIG_MODE_CLEAR) - if not len(new_sigs) == 1: - raise gpgme.GpgmeError( - "Testing was unable to produce a signature with GnuPG key.") - - except (IOError, OSError) as error: - logging.debug(error) - logging.error("Could not open or read from GnuPG key file %r!" - % cfg.EMAIL_GPG_SIGNING_KEY) - ctx = None - except gpgme.GpgmeError as error: - logging.exception(error) - ctx = None - finally: - if keyfile and not keyfile.closed: - keyfile.close() - - return ctx diff --git a/lib/bridgedb/crypto.py b/lib/bridgedb/crypto.py index 7f6d597..5c68794 100644 --- a/lib/bridgedb/crypto.py +++ b/lib/bridgedb/crypto.py @@ -29,8 +29,10 @@ from __future__ import absolute_import from __future__ import unicode_literals
+import gpgme import hashlib import hmac +import io import logging import os import re @@ -51,6 +53,46 @@ DIGESTMOD = hashlib.sha1 class RSAKeyGenerationError(Exception): """Raised when there was an error creating an RSA keypair."""
+class PythonicGpgmeError(Exception): + """Replacement for ``gpgme.GpgmeError`` with understandable error info.""" + +class LessCrypticGPGMEError(Exception): + """Holds interpreted info on source/type of a ``gpgme.GpgmeError``.""" + + def __init__(self, gpgmeError, *args): + self.interpretCrypticGPGMEError(gpgmeError) + super(LessCrypticGPGMEError, self).__init__(self.message) + + def interpretCrypticGPGMEError(self, gpgmeError): + """Set our ``message`` attribute with a decoded explanation of the + GPGME error code received. + + :type gpgmeError: ``gpgme.GpgmeError`` + :param gpgmeError: An exception raised by the gpgme_ module. + + .. _gpgme: https://bazaar.launchpad.net/~jamesh/pygpgme/trunk/view/head:/src/pygpgme-er... + """ + try: + errorSource, errorCode, errorMessage = gpgmeError.args + except (AttributeError, ValueError): + self.message = "Could not get error code from gpgme.GpgmeError!" + return + + if errorCode and errorSource: + try: + sources = gpgmeErrorTranslations[str(errorSource)] + except KeyError: + sources = ['UNKNOWN'] + sources = ', '.join(sources).strip(',') + + try: + names = gpgmeErrorTranslations[str(errorCode)] + except KeyError: + names = ['UNKNOWN'] + names = ', '.join(names).strip(',') + + self.message = "GpgmeError: {0} stemming from {1}: '{2}'""".format( + names, sources, str(errorMessage))
def writeKeyToFile(key, filename): """Write **key** to **filename**, with ``0400`` permissions. @@ -194,6 +236,117 @@ def getHMACFunc(key, hex=True): return h_tmp.digest() return hmac_fn
+def _createGPGMEErrorInterpreters(): + errorDict = {} + errorAttrs = [] + + if gpgme is not None: + errorAttrs = dir(gpgme) + + for attr in errorAttrs: + if attr.startswith('ERR'): + errorName = attr + errorCode = getattr(gpgme, attr, None) + if errorCode is not None: + try: + allErrorNames = errorDict[str(errorCode)] + except KeyError: + allErrorNames = [] + allErrorNames.append(str(errorName)) + + errorDict.update({str(errorCode): allErrorNames}) + errorDict.update({str(errorName): str(errorCode)}) + + return errorDict + +gpgmeErrorTranslations = _createGPGMEErrorInterpreters() + +def getGPGContext(cfg): + """Import a key from a file and initialise a context for GnuPG operations. + + The key should not be protected by a passphrase, and should have the + signing flag enabled. + + :type cfg: :class:`bridgedb.persistent.Conf` + :param cfg: The loaded config file. + :rtype: :class:`gpgme.Context` or None + :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 was unable to initialize. + """ + 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 + + keyfile = None + ctx = gpgme.Context() + + try: + logging.debug("Opening GPG keyfile %s..." % cfg.EMAIL_GPG_SIGNING_KEY) + keyfile = open(cfg.EMAIL_GPG_SIGNING_KEY) + key = ctx.import_(keyfile) + + if not len(key.imports) > 0: + logging.debug("Unexpected result from gpgme.Context.import_(): %r" + % key) + raise PythonicGpgmeError("Could not import GnuPG key from file %r" + % cfg.EMAIL_GPG_SIGNING_KEY) + + fingerprint = key.imports[0][0] + subkeyFingerprints = [] + # For some reason, if we don't do it exactly like this, we can get + # signatures for *any* key in the current process owner's keyring + # file: + bridgedbKey = ctx.get_key(fingerprint) + bridgedbUID = bridgedbKey.uids[0].uid + logging.info("GnuPG key imported: %s" % bridgedbUID) + logging.info(" Fingerprint: %s" % fingerprint) + for subkey in bridgedbKey.subkeys: + logging.info("Subkey fingerprint: %s" % subkey.fpr) + subkeyFingerprints.append(subkey.fpr) + + ctx.armor = True + ctx.signers = (bridgedbKey,) + + logging.debug("Testing signature created with GnuPG key...") + testMessage = "Testing 1 2 3" + signatureText, sigs = gpgSignMessage(ctx, testMessage) + + if not len(sigs) == 1: + raise PythonicGpgmeError("Testing couldn't produce a signature "\ + "with GnuPG key: %s" % fingerprint) + + sigFingerprint = sigs[0].fpr + if sigFingerprint in subkeyFingerprints: + logging.info("GPG signatures will use subkey with fingerprint: %s" + % sigFingerprint) + else: + if sigFingerprint != fingerprint: + raise PythonicGpgmeError( + "Test sig fingerprint '%s' not from any appropriate key!" + % sigFingerprint) + + except (IOError, OSError) as error: + logging.debug(error) + logging.error("Could not open or read from GnuPG key file %r!" + % cfg.EMAIL_GPG_SIGNING_KEY) + ctx = None + except gpgme.GpgmeError as error: + lessCryptic = LessCrypticGPGMEError(error) + logging.error(lessCryptic) + ctx = None + except PythonicGpgmeError as error: + logging.error(error) + ctx = None + finally: + if keyfile and not keyfile.closed: + keyfile.close() + + return ctx +
class SSLVerifyingContextFactory(ssl.CertificateOptions): """``OpenSSL.SSL.Context`` factory which does full certificate-chain and