← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key into launchpad:master

 

Simone Pelosi has proposed merging ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key into launchpad:master.

Commit message:
Add logic to update 1024 PPAs keys
    
Cronscript to generate new 4096-bit RSA signing keys for the affected
PPAs (the ones with 1024-bit key) and add a row to the signingkey table
with the information about the newly generated key.
The new key will be generated for the default PPA and then propagated
to the other PPAs beloning to the same owner.
Add rows to the archivesigningkey containing updated PPAs
(i.e., one row per signing key-archive combination).
Also add information regarding the new keys to the gpgkey table.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pelpsi/launchpad/+git/launchpad/+merge/461648
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pelpsi/launchpad:create-new-4096-key-for-archives-with-1024-key into launchpad:master.
diff --git a/cronscripts/ppa-update-keys.py b/cronscripts/ppa-update-keys.py
new file mode 100755
index 0000000..2ab1ea5
--- /dev/null
+++ b/cronscripts/ppa-update-keys.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python3 -S
+#
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A cron script that generate missing PPA signing keys."""
+
+import _pythonpath  # noqa: F401
+
+from lp.services.config import config
+from lp.soyuz.scripts.ppakeyupdater import PPAKeyUpdater
+
+if __name__ == "__main__":
+    script = PPAKeyUpdater("ppa-generate-keys", config.archivepublisher.dbuser)
+    script.lock_and_run()
diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
index 722cecd..e1b880c 100644
--- a/lib/lp/archivepublisher/archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/archivegpgsigningkey.py
@@ -30,7 +30,7 @@ from lp.archivepublisher.run_parts import find_run_parts_dir, run_parts
 from lp.registry.interfaces.gpg import IGPGKeySet
 from lp.services.config import config
 from lp.services.features import getFeatureFlag
-from lp.services.gpg.interfaces import IGPGHandler, IPymeKey
+from lp.services.gpg.interfaces import GPGKeyAlgorithm, IGPGHandler, IPymeKey
 from lp.services.osutils import remove_if_exists
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.signing.enums import (
@@ -39,6 +39,7 @@ from lp.services.signing.enums import (
     SigningMode,
 )
 from lp.services.signing.interfaces.signingkey import (
+    IArchiveSigningKeySet,
     ISigningKey,
     ISigningKeySet,
 )
@@ -330,6 +331,80 @@ class ArchiveGPGSigningKey(SignableArchive):
             signing_key, async_keyserver=async_keyserver
         )
 
+    def generate4096SigningKey(self, log=None):
+        """See `IArchiveGPGSigningKey`."""
+        assert (
+            self.archive.signing_key_fingerprint is not None
+        ), "Archive doesn't have key to update."
+        old_gpg_key = getUtility(IGPGKeySet).getByFingerprint(
+            self.archive.signing_key_fingerprint
+        )
+        assert old_gpg_key.keysize == 1024, "Archive already has 4096 key."
+
+        default_ppa = (
+            self.archive.owner.archive if self.archive.is_ppa else self.archive
+        )
+        if self.archive != default_ppa:
+
+            def propagate_key(archive_signing_key):
+                getUtility(IArchiveSigningKeySet).create(
+                    self.archive, None, archive_signing_key
+                )
+                del get_property_cache(self.archive).signing_key
+                del get_property_cache(self.archive).signing_key_display_name
+
+            default_signing_key = getUtility(
+                IArchiveSigningKeySet
+            ).getSigningKey(SigningKeyType.OPENPGP, default_ppa, None)
+            if default_signing_key is None:
+                # Recursively update default_ppa key
+                IArchiveGPGSigningKey(default_ppa).upgradeSigningKey(log=log)
+            propagate_key(default_signing_key)
+            return
+
+        key_displayname = (
+            "Launchpad PPA for %s" % self.archive.owner.displayname
+        )
+        key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard
+        try:
+            signing_key = getUtility(ISigningKeySet).generate(
+                SigningKeyType.OPENPGP,
+                key_displayname,
+                openpgp_key_algorithm=OpenPGPKeyAlgorithm.RSA,
+                length=4096,
+            )
+            getUtility(IArchiveSigningKeySet).create(
+                self.archive, None, signing_key
+            )
+            getUtility(IGPGKeySet).new(
+                key_owner,
+                signing_key.fingerprint[-8:],
+                signing_key.fingerprint,
+                4096,
+                GPGKeyAlgorithm.R,
+            )
+        except Exception as e:
+            if log is not None:
+                log.exception(
+                    "Error generating signing key for %s: %s %s"
+                    % (self.archive.reference, e.__class__.__name__, e)
+                )
+            raise
+
+        if IPymeKey.providedBy(signing_key):
+            self.exportSecretKey(signing_key)
+
+        pub_key = self._uploadPublicSigningKey(signing_key)
+        if IPymeKey.providedBy(pub_key):
+            _, _ = getUtility(IGPGKeySet).activate(
+                key_owner, pub_key, pub_key.can_encrypt
+            )
+        else:
+            assert ISigningKey.providedBy(pub_key)
+            _ = pub_key
+        del get_property_cache(self.archive).signing_key
+        del get_property_cache(self.archive).signing_key_display_name
+
     def setSigningKey(self, key_path, async_keyserver=False):
         """See `IArchiveGPGSigningKey`."""
         assert (
diff --git a/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py b/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py
index d0b03e0..72c5b2d 100644
--- a/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/interfaces/archivegpgsigningkey.py
@@ -120,6 +120,25 @@ class IArchiveGPGSigningKey(ISignableArchive):
             upload to the keyserver.
         """
 
+    def generate4096SigningKey(log=None):
+        """Generate a new 4096 GPG secret/public key pair.
+
+        For named-ppas, the existing signing-key for the default PPA
+        owner by the same user/team is reused. The *trust* belongs to
+        the archive maintainer (owner) not the archive itself.
+
+        Default ppas get brand new 4096 keys via the following procedure.
+
+         * Export the secret key in the configuration disk location;
+         * Upload the public key to the configuration keyserver;
+         * Store a reference for the public key in GPGKey table, which
+           is set as the context archive 'signing_key'.
+
+        :param log: an optional logger.
+        :raises GPGUploadFailure: if the just-generated key could not be
+            upload to the keyserver.
+        """
+
     def setSigningKey(key_path, async_keyserver=False):
         """Set a given secret key export as the context archive signing key.
 
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index e1521ce..a288434 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -2878,6 +2878,14 @@ class IArchiveSet(Interface):
         :param purpose: Only return archives with this `ArchivePurpose`.
         """
 
+    def getArchivesWith1024Key(limit):
+        """Return all archives with old 1024 bit signing key.
+
+        The result is ordered by archive id.
+
+        :param limit: Limit the size of archive result set.
+        """
+
     def getLatestPPASourcePublicationsForDistribution(distribution):
         """The latest 5 PPA source publications for a given distribution.
 
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index b86c2e9..c1117f8 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -78,6 +78,7 @@ from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.role import IHasOwner, IPersonRoles
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.registry.model.gpgkey import GPGKey
 from lp.registry.model.sourcepackagename import SourcePackageName
 from lp.registry.model.teammembership import TeamParticipation
 from lp.services.config import config
@@ -95,6 +96,7 @@ from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.signing.enums import SigningKeyType
 from lp.services.signing.interfaces.signingkey import ISigningKeySet
+from lp.services.signing.model.signingkey import ArchiveSigningKey
 from lp.services.tokens import create_token
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.interfaces import ILaunchBag
@@ -3653,6 +3655,31 @@ class ArchiveSet:
         results.order_by(Archive.date_created)
         return results.config(distinct=True)
 
+    def getArchivesWith1024Key(self, limit):
+        """See `IArchiveSet`."""
+        origin = (
+            Archive,
+            Join(
+                GPGKey,
+                GPGKey.fingerprint == Archive.signing_key_fingerprint,
+            ),
+        )
+        inner_results = IStore(ArchiveSigningKey).find(
+            ArchiveSigningKey.archive_id,
+            ArchiveSigningKey.key_type == SigningKeyType.OPENPGP,
+        )
+        results = (
+            IStore(Archive)
+            .using(*origin)
+            .find(
+                Archive,
+                GPGKey.keysize == 1024,
+                Not(Archive.id.is_in(inner_results)),
+            )
+        )
+        results.order_by(Archive.id)
+        return results.config(distinct=True, limit=limit)
+
     def getLatestPPASourcePublicationsForDistribution(self, distribution):
         """See `IArchiveSet`."""
         # Circular import.
diff --git a/lib/lp/soyuz/scripts/ppakeyupdater.py b/lib/lp/soyuz/scripts/ppakeyupdater.py
new file mode 100644
index 0000000..a33054a
--- /dev/null
+++ b/lib/lp/soyuz/scripts/ppakeyupdater.py
@@ -0,0 +1,46 @@
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+    "PPAKeyUpdater",
+]
+
+from zope.component import getUtility
+
+from lp.archivepublisher.interfaces.archivegpgsigningkey import (
+    IArchiveGPGSigningKey,
+)
+from lp.services.scripts.base import LaunchpadCronScript
+from lp.soyuz.interfaces.archive import IArchiveSet
+
+
+class PPAKeyUpdater(LaunchpadCronScript):
+    usage = "%prog [-L]"
+    description = "Update 1024 GPG signing key for PPAs."
+
+    def add_my_options(self):
+        self.parser.add_option(
+            "-L",
+            "--limit",
+            type=int,
+            help="Number of PPAs to process per run.",
+        )
+
+    def generate4096Key(self, archive):
+        """Generate a signing key for the given archive."""
+        self.logger.info(
+            "Generating signing key for %s (%s)"
+            % (archive.reference, archive.displayname)
+        )
+        archive_signing_key = IArchiveGPGSigningKey(archive)
+        archive_signing_key.generate4096SigningKey(log=self.logger)
+
+    def main(self):
+        """Generate signing keys for the selected PPAs."""
+        archive_set = getUtility(IArchiveSet)
+
+        archives = list(archive_set.getArchivesWith1024Key(self.options.limit))
+
+        for archive in archives:
+            self.generate4096Key(archive)
+            self.txn.commit()
diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py b/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py
new file mode 100644
index 0000000..9840a38
--- /dev/null
+++ b/lib/lp/soyuz/scripts/tests/test_ppakeyupdater.py
@@ -0,0 +1,155 @@
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""`PPAKeyUpdater` script class tests."""
+
+from zope.component import getUtility
+
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.gpg import IGPGKeySet
+from lp.services.gpg.interfaces import GPGKeyAlgorithm
+from lp.services.signing.enums import SigningKeyType
+from lp.services.signing.interfaces.signingkey import IArchiveSigningKeySet
+from lp.services.signing.model.signingkey import SigningKey
+from lp.soyuz.enums import ArchivePurpose
+from lp.soyuz.scripts.ppakeyupdater import PPAKeyUpdater
+from lp.testing import TestCaseWithFactory
+from lp.testing.faketransaction import FakeTransaction
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestPPAKeyUpdater(TestCaseWithFactory):
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super().setUp()
+        # Generate two mock 4096 signing keys
+        self.gpg_key = getUtility(IGPGKeySet).new(
+            16,
+            "12345678",
+            "ABCDEF0123456789ABCDDCBA0000111112345679",
+            4096,
+            GPGKeyAlgorithm.R,
+        )
+
+        self.signing_key = SigningKey(
+            SigningKeyType.OPENPGP,
+            "ABCDEF0123456789ABCDDCBA0000111112345679",
+            bytes("PublicKey", "utf-8"),
+        )
+
+    def _fixArchiveFor4096KeyGeneration(self, archive):
+        """Override the given archive distribution to 'ubuntutest'.
+
+        This is necessary because 'ubuntutest' is the only distribution in
+        the sampledata that contains a usable publishing configuration.
+        """
+        ubuntutest = getUtility(IDistributionSet).getByName("ubuntutest")
+        archive.distribution = ubuntutest
+
+    def _get4096KeyGenerator(self, limit=None, txn=None):
+        """Return a `PPAKeyUpdater` instance.
+
+        Monkey-patch the script object with a fake transaction manager
+        and also make it use an alternative (fake and lighter) procedure
+        to generate 4096 keys for each PPA.
+        """
+        test_args = []
+        if limit:
+            test_args.extend(["-L", limit])
+
+        key_generator = PPAKeyUpdater(
+            name="ppa-generate-keys", test_args=test_args
+        )
+
+        if txn is None:
+            txn = FakeTransaction()
+        key_generator.txn = txn
+
+        def fake_key_generation(archive):
+            getUtility(IArchiveSigningKeySet).create(
+                archive, None, self.signing_key
+            )
+
+        key_generator.generate4096Key = fake_key_generation
+
+        return key_generator
+
+    def testNoPPAsToUpdate(self):
+        txn = FakeTransaction()
+        key_generator = self._get4096KeyGenerator(txn=txn)
+        key_generator.main()
+
+        self.assertEqual(txn.commit_count, 0)
+
+    def testGenerate4096KeyForPPAs(self):
+        """Signing key updating for PPAs.
+
+        The new 4096 'signing_key' for the specified PPAs is generated.
+        """
+        archives = []
+        key_1024 = "ABCDEF0123456789ABCDDCBA0000111112345678"
+        key_4096 = "ABCDEF0123456789ABCDDCBA0000111112345679"
+        for _ in range(3):
+            rebuild = self.factory.makeArchive(
+                distribution=getUtility(IDistributionSet).getByName(
+                    "ubuntutest"
+                ),
+                purpose=ArchivePurpose.PPA,
+            )
+            self.factory.makeSourcePackagePublishingHistory(archive=rebuild)
+            rebuild.signing_key_fingerprint = key_1024
+            archives.append(rebuild)
+
+        txn = FakeTransaction()
+        key_generator = self._get4096KeyGenerator(txn=txn)
+        key_generator.main()
+
+        self.assertEqual(txn.commit_count, 3)
+
+        for archive in archives:
+            signing_key = getUtility(IArchiveSigningKeySet).getSigningKey(
+                SigningKeyType.OPENPGP, archive, None
+            )
+            self.assertEqual(signing_key.fingerprint, key_4096)
+            self.assertEqual(archive.signing_key_fingerprint, key_1024)
+
+    def testGenerate4096KeyForPPAsLimit(self):
+        """Signing key updating for PPAs.
+
+        The new 4096 'signing_key' for the specified number of
+        PPAs is generated.
+        """
+        archives = []
+        key_1024 = "ABCDEF0123456789ABCDDCBA0000111112345678"
+        key_4096 = "ABCDEF0123456789ABCDDCBA0000111112345679"
+        for _ in range(3):
+            rebuild = self.factory.makeArchive(
+                distribution=getUtility(IDistributionSet).getByName(
+                    "ubuntutest"
+                ),
+                purpose=ArchivePurpose.PPA,
+            )
+            self.factory.makeSourcePackagePublishingHistory(archive=rebuild)
+            rebuild.signing_key_fingerprint = key_1024
+            archives.append(rebuild)
+
+        txn = FakeTransaction()
+        key_generator = self._get4096KeyGenerator(limit="2", txn=txn)
+        key_generator.main()
+
+        # 2/3 PPAs processed
+        self.assertEqual(txn.commit_count, 2)
+
+        key_generator = self._get4096KeyGenerator(limit="2", txn=txn)
+        key_generator.main()
+
+        # 3/3 PPAs processed
+        self.assertEqual(txn.commit_count, 3)
+
+        for archive in archives:
+            signing_key = getUtility(IArchiveSigningKeySet).getSigningKey(
+                SigningKeyType.OPENPGP, archive, None
+            )
+            self.assertEqual(signing_key.fingerprint, key_4096)
+            self.assertEqual(archive.signing_key_fingerprint, key_1024)

Follow ups