[bridgedb/develop] Move EmailServer.getGPGContext() → crypto.getGPGContext().

isis at torproject.org isis at torproject.org
Thu Apr 17 05:10:03 UTC 2014


commit 7b75cff76edac559e6dbf6f24584e86f948aa597
Author: Isis Lovecruft <isis at 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-error.c
+        """
+        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





More information about the tor-commits mailing list