← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/faster-gpg-operations into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/faster-gpg-operations into lp:launchpad.

Commit message:
Sign/encrypt content based on an IPymeKey rather than a fingerprint, for performance.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/faster-gpg-operations/+merge/289950

Sign/encrypt content based on an IPymeKey rather than a fingerprint.  Getting from a fingerprint to a key involves two subprocess invocations in gpgme, and in nearly all cases we already have the key in hand so this is unnecessary work.  This makes a difference when re-signing thousands of PPAs: it saves in the region of .2s per signature on dogfood.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/faster-gpg-operations into lp:launchpad.
=== modified file 'lib/lp/archivepublisher/archivesigningkey.py'
--- lib/lp/archivepublisher/archivesigningkey.py	2016-03-16 23:13:58 +0000
+++ lib/lp/archivepublisher/archivesigningkey.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """ArchiveSigningKey implementation."""
@@ -23,10 +23,7 @@
     )
 from lp.registry.interfaces.gpg import IGPGKeySet
 from lp.services.config import config
-from lp.services.gpg.interfaces import (
-    GPGKeyAlgorithm,
-    IGPGHandler,
-    )
+from lp.services.gpg.interfaces import IGPGHandler
 from lp.services.propertycache import get_property_cache
 
 
@@ -133,8 +130,7 @@
 
         release_file_content = open(release_file_path).read()
         signature = gpghandler.signContent(
-            release_file_content, secret_key.fingerprint,
-            mode=gpgme.SIG_MODE_DETACH)
+            release_file_content, secret_key, mode=gpgme.SIG_MODE_DETACH)
 
         release_signature_file = open(
             os.path.join(suite_path, 'Release.gpg'), 'w')
@@ -142,8 +138,7 @@
         release_signature_file.close()
 
         inline_release = gpghandler.signContent(
-            release_file_content, secret_key.fingerprint,
-            mode=gpgme.SIG_MODE_CLEAR)
+            release_file_content, secret_key, mode=gpgme.SIG_MODE_CLEAR)
 
         inline_release_file = open(
             os.path.join(suite_path, 'InRelease'), 'w')

=== modified file 'lib/lp/bugs/mail/tests/test_handler.py'
--- lib/lp/bugs/mail/tests/test_handler.py	2015-09-08 09:09:28 +0000
+++ lib/lp/bugs/mail/tests/test_handler.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test MaloneHandler."""
@@ -682,7 +682,7 @@
         # isn't too far in the future or past.  This test shows that a
         # signature with a timestamp of appxoimately now will be accepted.
         signing_context = GPGSigningContext(
-            import_secret_test_key().fingerprint, password='test')
+            import_secret_test_key(), password='test')
         msg = self.factory.makeSignedMessage(
             body=' security no', signing_context=signing_context)
         handler = MaloneHandler()

=== modified file 'lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt'
--- lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt	2016-03-17 23:32:28 +0000
+++ lib/lp/registry/stories/gpg-coc/gpg-with-gpgservice-ff.txt	2016-03-23 18:12:33 +0000
@@ -261,8 +261,7 @@
     >>> login(ANONYMOUS)
     >>> key = import_secret_test_key('sign.only@xxxxxxxxxxxxxxxxx')
     >>> bad = gpghandler.signContent(
-    ...     'This is not the verification message!',
-    ...     '447DBF38C4F9C4ED752246B77D88913717B05A8F', 'test')
+    ...     'This is not the verification message!', key, 'test')
     >>> logout()
 
     >>> browser.getControl('Signed text').value = bad
@@ -298,9 +297,7 @@
 If they sign the text correctly, they are redirected to their home page.
 
     >>> login(ANONYMOUS)
-    >>> good = gpghandler.signContent(
-    ...     str(verification_content),
-    ...     '447DBF38C4F9C4ED752246B77D88913717B05A8F', 'test')
+    >>> good = gpghandler.signContent(str(verification_content), key, 'test')
     >>> logout()
 
     >>> browser.getControl('Signed text').value = good

=== modified file 'lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt'
--- lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt	2016-02-05 16:51:12 +0000
+++ lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt	2016-03-23 18:12:33 +0000
@@ -246,8 +246,7 @@
     >>> login(ANONYMOUS)
     >>> key = import_secret_test_key('sign.only@xxxxxxxxxxxxxxxxx')
     >>> bad = gpghandler.signContent(
-    ...     'This is not the verification message!',
-    ...     '447DBF38C4F9C4ED752246B77D88913717B05A8F', 'test')
+    ...     'This is not the verification message!', key, 'test')
     >>> logout()
 
     >>> browser.getControl('Signed text').value = bad
@@ -283,9 +282,7 @@
 If they sign the text correctly, they are redirected to their home page.
 
     >>> login(ANONYMOUS)
-    >>> good = gpghandler.signContent(
-    ...     str(verification_content),
-    ...     '447DBF38C4F9C4ED752246B77D88913717B05A8F', 'test')
+    >>> good = gpghandler.signContent(str(verification_content), key, 'test')
     >>> logout()
 
     >>> browser.getControl('Signed text').value = good

=== modified file 'lib/lp/services/gpg/doc/gpg-encryption.txt'
--- lib/lp/services/gpg/doc/gpg-encryption.txt	2011-12-24 17:49:30 +0000
+++ lib/lp/services/gpg/doc/gpg-encryption.txt	2016-03-23 18:12:33 +0000
@@ -48,8 +48,8 @@
     >>> fingerprint
     u'A419AE861E88BC9E04B9C26FBA2B9389DFD20543'
 
-    >>> cipher = gpghandler.encryptContent(content.encode('utf-8'),
-    ...                                    fingerprint)
+    >>> key = gpghandler.retrieveKey(fingerprint)
+    >>> cipher = gpghandler.encryptContent(content.encode('utf-8'), key)
     
 cipher constains the encrypted content.
 
@@ -67,15 +67,14 @@
 Verify if the encrytion process support passing another charset string
 
     >>> content = u'a\xe7ucar'
-    >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'),
-    ... fingerprint)
+    >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
     >>> plain = decrypt_content(cipher, 'test')    
     >>> plain.decode('iso-8859-1')
     u'a\xe7ucar'
 
 Let's try to pass unicode and see if it fails
 
-    >>> cipher = gpghandler.encryptContent(content,fingerprint)
+    >>> cipher = gpghandler.encryptContent(content, key)
     Traceback (most recent call last):
     ...
     TypeError: Content cannot be Unicode.
@@ -83,8 +82,7 @@
 Decrypt a unicode content:
 
     >>> content = u'a\xe7ucar'
-    >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'),
-    ... fingerprint)
+    >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
     >>> cipher = unicode(cipher)
     >>> plain = decrypt_content(cipher, 'test')    
     Traceback (most recent call last):

=== modified file 'lib/lp/services/gpg/handler.py'
--- lib/lp/services/gpg/handler.py	2016-03-21 00:20:23 +0000
+++ lib/lp/services/gpg/handler.py	2016-03-23 18:12:33 +0000
@@ -26,6 +26,7 @@
 from gpgservice_client import GPGClient
 from lazr.restful.utils import get_current_browser_request
 from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
 
 from lp.app.validators.email import valid_email
 from lp.services.config import config
@@ -320,7 +321,7 @@
 
         return key
 
-    def encryptContent(self, content, fingerprint):
+    def encryptContent(self, content, key):
         """See IGPGHandler."""
         if isinstance(content, unicode):
             raise TypeError('Content cannot be Unicode.')
@@ -333,22 +334,21 @@
         plain = StringIO(content)
         cipher = StringIO()
 
-        # retrive gpgme key object
-        try:
-            key = ctx.get_key(fingerprint.encode('ascii'), 0)
-        except gpgme.GpgmeError:
+        if key.key is None:
             return None
 
         if not key.can_encrypt:
             raise ValueError('key %s can not be used for encryption'
-                             % fingerprint)
+                             % key.fingerprint)
 
         # encrypt content
-        ctx.encrypt([key], gpgme.ENCRYPT_ALWAYS_TRUST, plain, cipher)
+        ctx.encrypt(
+            [removeSecurityProxy(key.key)], gpgme.ENCRYPT_ALWAYS_TRUST,
+            plain, cipher)
 
         return cipher.getvalue()
 
-    def signContent(self, content, key_fingerprint, password='', mode=None):
+    def signContent(self, content, key, password='', mode=None):
         """See IGPGHandler."""
         if not isinstance(content, str):
             raise TypeError('Content should be a string.')
@@ -361,8 +361,7 @@
         context = gpgme.Context()
         context.armor = True
 
-        key = context.get_key(key_fingerprint.encode('ascii'), True)
-        context.signers = [key]
+        context.signers = [removeSecurityProxy(key.key)]
 
         # Set up containers.
         plaintext = StringIO(content)
@@ -561,6 +560,7 @@
 
     def _buildFromGpgmeKey(self, key):
         self.exists_in_local_keyring = True
+        self.key = key
         subkey = key.subkeys[0]
         self.fingerprint = subkey.fpr
         self.revoked = subkey.revoked

=== modified file 'lib/lp/services/gpg/interfaces.py'
--- lib/lp/services/gpg/interfaces.py	2016-03-16 02:08:40 +0000
+++ lib/lp/services/gpg/interfaces.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __all__ = [
@@ -290,24 +290,23 @@
         :return: a `PymeKey` object for the just-generated secret key.
         """
 
-    def encryptContent(content, fingerprint):
-        """Encrypt the given content for the given fingerprint.
+    def encryptContent(content, key):
+        """Encrypt the given content for the given key.
 
         content must be a traditional string. It's up to the caller to
-        encode or decode properly. Fingerprint must be hexadecimal string.
+        encode or decode properly.
 
         :param content: the Unicode content to be encrypted.
-        :param fingerprint: the OpenPGP key's fingerprint.
+        :param key: the `IPymeKey` to encrypt the content for.
 
         :return: the encrypted content or None if failed.
         """
 
-    def signContent(content, key_fingerprint, password='', mode=None):
-        """Signs content with a given GPG fingerprint.
+    def signContent(content, key, password='', mode=None):
+        """Signs content with a given GPG key.
 
         :param content: the content to sign.
-        :param key_fingerprint: the fingerprint of the key to use when
-            signing the content.
+        :param key: the `IPymeKey` to use when signing the content.
         :param password: optional password to the key identified by
             key_fingerprint, the default value is '',
         :param mode: optional type of GPG signature to produce, the
@@ -386,6 +385,7 @@
     """pyME key model."""
 
     fingerprint = Attribute("Key Fingerprint")
+    key = Attribute("Underlying GpgmeKey object")
     algorithm = Attribute("Key Algorithm")
     revoked = Attribute("Key Revoked")
     expired = Attribute("Key Expired")

=== modified file 'lib/lp/services/gpg/tests/test_gpghandler.py'
--- lib/lp/services/gpg/tests/test_gpghandler.py	2016-03-21 00:20:23 +0000
+++ lib/lp/services/gpg/tests/test_gpghandler.py	2016-03-23 18:12:33 +0000
@@ -221,7 +221,7 @@
             secret_key = import_secret_test_key(key_name)
             content = "abc\n"
             signed_content = self.gpg_handler.signContent(
-                content, secret_key.fingerprint, password)
+                content, secret_key, password)
             signature = self.gpg_handler.getVerifiedSignature(signed_content)
             self.assertEqual(content, signature.plain_data)
             self.assertEqual(secret_key.fingerprint, signature.fingerprint)

=== modified file 'lib/lp/services/mail/tests/test_incoming.py'
--- lib/lp/services/mail/tests/test_incoming.py	2015-07-08 16:05:11 +0000
+++ lib/lp/services/mail/tests/test_incoming.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 
@@ -182,7 +182,7 @@
         """If the signature is nontrivial future-dated, it's not trusted."""
 
         signing_context = GPGSigningContext(
-            import_secret_test_key().fingerprint, password='test')
+            import_secret_test_key(), password='test')
         msg = self.factory.makeSignedMessage(signing_context=signing_context)
         # It's not trivial to make a gpg signature with a bogus timestamp, so
         # let's just treat everything as invalid, and trust that the regular

=== modified file 'lib/lp/services/mail/tests/test_signedmessage.py'
--- lib/lp/services/mail/tests/test_signedmessage.py	2015-03-13 19:05:50 +0000
+++ lib/lp/services/mail/tests/test_signedmessage.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test the SignedMessage class."""
@@ -62,7 +62,7 @@
         # Create a signed message for the sender specified with the test
         # secret key.
         key = import_secret_test_key()
-        signing_context = GPGSigningContext(key.fingerprint, password='test')
+        signing_context = GPGSigningContext(key, password='test')
         if body is None:
             body = dedent("""\
                 This is a multi-line body.
@@ -136,7 +136,7 @@
         gpghandler = getUtility(IGPGHandler)
         signature = gpghandler.signContent(
             canonicalise_line_endings(body_text.as_string()),
-            key.fingerprint, 'test', gpgme.SIG_MODE_DETACH)
+            key, 'test', gpgme.SIG_MODE_DETACH)
 
         attachment = Message()
         attachment.set_payload(signature)

=== modified file 'lib/lp/services/verification/model/logintoken.py'
--- lib/lp/services/verification/model/logintoken.py	2015-09-28 17:38:45 +0000
+++ lib/lp/services/verification/model/logintoken.py	2016-03-23 18:12:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -167,8 +167,8 @@
         # Encrypt this part's content if requested.
         if key.can_encrypt:
             gpghandler = getUtility(IGPGHandler)
-            token_text = gpghandler.encryptContent(token_text.encode('utf-8'),
-                                                   key.fingerprint)
+            token_text = gpghandler.encryptContent(
+                token_text.encode('utf-8'), key)
             # In this case, we need to include some clear text instructions
             # for people who do not have an MUA that can decrypt the ASCII
             # armored text.

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-03-21 22:03:39 +0000
+++ lib/lp/testing/factory.py	2016-03-23 18:12:33 +0000
@@ -392,10 +392,10 @@
 
 
 class GPGSigningContext:
-    """A helper object to hold the fingerprint, password and mode."""
+    """A helper object to hold the key, password and mode."""
 
-    def __init__(self, fingerprint, password='', mode=None):
-        self.fingerprint = fingerprint
+    def __init__(self, key, password='', mode=None):
+        self.key = key
         self.password = password
         self.mode = mode
 
@@ -2158,8 +2158,8 @@
         if signing_context is not None:
             gpghandler = getUtility(IGPGHandler)
             body = gpghandler.signContent(
-                body, signing_context.fingerprint,
-                signing_context.password, signing_context.mode)
+                body, signing_context.key, signing_context.password,
+                signing_context.mode)
             assert body is not None
         if attachment_contents is None:
             mail.set_payload(body)


Follow ups