← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/send-keys-to-builders into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/send-keys-to-builders into lp:launchpad with lp:~cjwatson/launchpad/faster-archive-signing-key-tests as a prerequisite.

Commit message:
Send the necessary set of archive signing keys to builders.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1626739 in Launchpad itself: "Snapcraft build failing in Yakkety for unauthenticated stage-packages"
  https://bugs.launchpad.net/launchpad/+bug/1626739

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/send-keys-to-builders/+merge/323438
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/send-keys-to-builders into lp:launchpad.
=== modified file 'lib/lp/code/model/recipebuilder.py'
--- lib/lp/code/model/recipebuilder.py	2017-04-29 23:54:40 +0000
+++ lib/lp/code/model/recipebuilder.py	2017-04-29 23:54:40 +0000
@@ -67,10 +67,11 @@
         args["ogrecomponent"] = get_primary_current_component(
             self.build.archive, self.build.distroseries,
             None).name
-        args['archives'] = yield get_sources_list_for_building(
-            self.build, distroarchseries, None,
-            tools_source=config.builddmaster.bzr_builder_sources_list,
-            logger=logger)
+        args['archives'], args['trusted_keys'] = (
+            yield get_sources_list_for_building(
+                self.build, distroarchseries, None,
+                tools_source=config.builddmaster.bzr_builder_sources_list,
+                logger=logger))
         args['archive_private'] = self.build.archive.private
         args['distroseries_name'] = self.build.distroseries.name
         if self.build.recipe.base_git_repository is not None:

=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
--- lib/lp/code/model/tests/test_recipebuilder.py	2017-04-29 23:54:40 +0000
+++ lib/lp/code/model/tests/test_recipebuilder.py	2017-04-29 23:54:40 +0000
@@ -5,16 +5,21 @@
 
 __metaclass__ = type
 
+import os.path
 import shutil
 import tempfile
 
 from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import MatchesListwise
 import transaction
 from twisted.internet import defer
 from twisted.trial.unittest import TestCase as TrialTestCase
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interactor import BuilderInteractor
 from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
@@ -40,13 +45,19 @@
 from lp.soyuz.adapters.archivedependencies import (
     get_sources_list_for_building,
     )
-from lp.soyuz.enums import ArchivePurpose
+from lp.soyuz.enums import (
+    ArchivePurpose,
+    PackagePublishingStatus,
+    )
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import (
     person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.fakemethod import FakeMethod
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import LaunchpadZopelessLayer
 from lp.testing.mail_helpers import pop_notifications
 
@@ -59,7 +70,10 @@
                 archive=None, git=False):
         """Create a sample `ISourcePackageRecipeBuild`."""
         spn = self.factory.makeSourcePackageName("apackage")
-        distro = self.factory.makeDistribution(name="distro")
+        if archive is None:
+            distro = self.factory.makeDistribution(name="distro")
+        else:
+            distro = archive.distribution
         distroseries = self.factory.makeDistroSeries(
             name="mydistro", distribution=distro)
         processor = getUtility(IProcessorSet).getByName('386')
@@ -144,7 +158,7 @@
 
 class TestAsyncRecipeBuilder(TestRecipeBuilderBase):
 
-    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
 
     def _setBuilderConfig(self):
         """Setup a temporary builder config."""
@@ -158,8 +172,9 @@
         self._setBuilderConfig()
         job = self.makeJob()
         distroarchseries = job.build.distroseries.architectures[0]
-        expected_archives = yield get_sources_list_for_building(
-            job.build, distroarchseries, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, distroarchseries, None))
         expected_archives.insert(
             0, "deb http://foo %s main" % job.build.distroseries.name)
         args = yield job._extraBuildArgs(distroarchseries)
@@ -175,6 +190,7 @@
             '# bzr-builder format 0.3 deb-version {debupstream}-0~{revno}\n'
             'lp://dev/~joe/someapp/pkg\n',
             'archives': expected_archives,
+            'trusted_keys': expected_trusted_keys,
             'distroseries_name': job.build.distroseries.name,
         }, args)
 
@@ -249,8 +265,9 @@
         # (note the missing 's' in %(series)
         job = self.makeJob()
         distroarchseries = job.build.distroseries.architectures[0]
-        expected_archives = yield get_sources_list_for_building(
-            job.build, distroarchseries, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, distroarchseries, None))
         logger = BufferLogger()
         extra_args = yield job._extraBuildArgs(distroarchseries, logger)
         self.assertEqual({
@@ -265,6 +282,7 @@
             '# bzr-builder format 0.3 deb-version {debupstream}-0~{revno}\n'
             'lp://dev/~joe/someapp/pkg\n',
             'archives': expected_archives,
+            'trusted_keys': expected_trusted_keys,
             'distroseries_name': job.build.distroseries.name,
             }, extra_args)
         self.assertIn(
@@ -278,16 +296,19 @@
         job = self.makeJob()
         distroarchseries = job.build.distroseries.architectures[0]
         args = yield job._extraBuildArgs(distroarchseries)
-        expected_archives = yield get_sources_list_for_building(
-            job.build, distroarchseries, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, distroarchseries, None))
         self.assertEqual(args["archives"], expected_archives)
+        self.assertEqual(args["trusted_keys"], expected_trusted_keys)
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_git(self):
         job = self.makeJob(git=True)
         distroarchseries = job.build.distroseries.architectures[0]
-        expected_archives = yield get_sources_list_for_building(
-            job.build, distroarchseries, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, distroarchseries, None))
         extra_args = yield job._extraBuildArgs(distroarchseries)
         self.assertEqual({
             'archive_private': False,
@@ -302,11 +323,30 @@
                 '{debupstream}-0~{revtime}\n'
                 'lp:~joe/someapp/+git/pkg packaging\n',
             'archives': expected_archives,
+            'trusted_keys': expected_trusted_keys,
             'distroseries_name': job.build.distroseries.name,
             'git': True,
             }, extra_args)
 
     @defer.inlineCallbacks
+    def test_extraBuildArgs_archive_trusted_keys(self):
+        # If the archive has a signing key, _extraBuildArgs sends it.
+        yield self.useFixture(InProcessKeyServerFixture()).start()
+        archive = self.factory.makeArchive()
+        key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
+        yield IArchiveSigningKey(archive).setSigningKey(
+            key_path, async_keyserver=True)
+        job = self.makeJob(archive=archive)
+        distroarchseries = job.build.distroseries.architectures[0]
+        self.factory.makeBinaryPackagePublishingHistory(
+            distroarchseries=distroarchseries, pocket=job.build.pocket,
+            archive=archive, status=PackagePublishingStatus.PUBLISHED)
+        args = yield job._extraBuildArgs(distroarchseries)
+        self.assertThat(args["trusted_keys"], MatchesListwise([
+            Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
+            ]))
+
+    @defer.inlineCallbacks
     def test_composeBuildRequest(self):
         job = self.makeJob()
         test_publisher = SoyuzTestPublisher()

=== modified file 'lib/lp/services/tests/test_timeout.py'
--- lib/lp/services/tests/test_timeout.py	2016-07-01 20:07:09 +0000
+++ lib/lp/services/tests/test_timeout.py	2017-04-29 23:54:40 +0000
@@ -22,6 +22,7 @@
 from testtools.matchers import MatchesStructure
 
 from lp.services.timeout import (
+    default_timeout,
     get_default_timeout_function,
     reduced_timeout,
     set_default_timeout_function,
@@ -207,6 +208,15 @@
         no_default_timeout()
         self.assertEqual([True], using_default)
 
+    def test_default_timeout(self):
+        """default_timeout sets the default timeout if none is set."""
+        self.addCleanup(set_default_timeout_function, None)
+        with default_timeout(1.0):
+            self.assertEqual(1.0, get_default_timeout_function()())
+        set_default_timeout_function(lambda: 5.0)
+        with default_timeout(1.0):
+            self.assertEqual(5.0, get_default_timeout_function()())
+
     def test_reduced_timeout(self):
         """reduced_timeout caps the available timeout in various ways."""
         self.addCleanup(set_default_timeout_function, None)

=== modified file 'lib/lp/services/timeout.py'
--- lib/lp/services/timeout.py	2016-12-22 16:32:38 +0000
+++ lib/lp/services/timeout.py	2017-04-29 23:54:40 +0000
@@ -1,10 +1,11 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Helpers to time out external operations."""
 
 __metaclass__ = type
 __all__ = [
+    "default_timeout",
     "get_default_timeout_function",
     "reduced_timeout",
     "SafeTransportWithTimeout",
@@ -57,6 +58,23 @@
 
 
 @contextmanager
+def default_timeout(default):
+    """A context manager that sets the default timeout if none is set.
+
+    :param default: The default timeout to use if none is set.
+    """
+    original_timeout_function = get_default_timeout_function()
+
+    if original_timeout_function is None:
+        set_default_timeout_function(lambda: default)
+    try:
+        yield
+    finally:
+        if original_timeout_function is None:
+            set_default_timeout_function(None)
+
+
+@contextmanager
 def reduced_timeout(clearance, webapp_max=None, default=None):
     """A context manager that reduces the default timeout.
 

=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -99,9 +99,10 @@
         args["arch_tag"] = build.distro_arch_series.architecturetag
         # XXX cjwatson 2015-08-03: Allow tools_source to be overridden at
         # some more fine-grained level.
-        args["archives"] = yield get_sources_list_for_building(
-            build, build.distro_arch_series, None,
-            tools_source=config.snappy.tools_source, logger=logger)
+        args["archives"], args["trusted_keys"] = (
+            yield get_sources_list_for_building(
+                build, build.distro_arch_series, None,
+                tools_source=config.snappy.tools_source, logger=logger))
         args["archive_private"] = build.archive.private
         if build.snap.branch is not None:
             args["branch"] = build.snap.branch.bzr_identity

=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 from datetime import datetime
+import os.path
 from textwrap import dedent
 import uuid
 
@@ -17,12 +18,18 @@
 from pymacaroons import Macaroon
 from testtools import ExpectedException
 from testtools.deferredruntest import AsynchronousDeferredRunTest
-from testtools.matchers import IsInstance
+from testtools.matchers import (
+    IsInstance,
+    MatchesListwise,
+    )
 import transaction
 from twisted.internet import defer
 from twisted.trial.unittest import TestCase as TrialTestCase
 from zope.component import getUtility
 
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.builder import CannotBuild
 from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
@@ -47,8 +54,12 @@
 from lp.soyuz.adapters.archivedependencies import (
     get_sources_list_for_building,
     )
+from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.archive import ArchiveDisabled
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.testing import TestCaseWithFactory
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
@@ -59,9 +70,13 @@
         super(TestSnapBuildBehaviourBase, self).setUp()
         self.pushConfig("snappy", tools_source=None)
 
-    def makeJob(self, pocket=PackagePublishingPocket.UPDATES, **kwargs):
+    def makeJob(self, archive=None, pocket=PackagePublishingPocket.UPDATES,
+                **kwargs):
         """Create a sample `ISnapBuildBehaviour`."""
-        distribution = self.factory.makeDistribution(name="distro")
+        if archive is None:
+            distribution = self.factory.makeDistribution(name="distro")
+        else:
+            distribution = archive.distribution
         distroseries = self.factory.makeDistroSeries(
             distribution=distribution, name="unstable")
         processor = getUtility(IProcessorSet).getByName("386")
@@ -69,7 +84,7 @@
             distroseries=distroseries, architecturetag="i386",
             processor=processor)
         build = self.factory.makeSnapBuild(
-            distroarchseries=distroarchseries, pocket=pocket,
+            archive=archive, distroarchseries=distroarchseries, pocket=pocket,
             name=u"test-snap", **kwargs)
         return IBuildFarmJobBehaviour(build)
 
@@ -168,7 +183,7 @@
 
 
 class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase):
-    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
 
     def setUp(self):
         super(TestAsyncSnapBuildBehaviour, self).setUp()
@@ -211,8 +226,9 @@
         # job for a Bazaar branch.
         branch = self.factory.makeBranch()
         job = self.makeJob(branch=branch)
-        expected_archives = yield get_sources_list_for_building(
-            job.build, job.build.distro_arch_series, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, job.build.distro_arch_series, None))
         args = yield job._extraBuildArgs()
         self.assertEqual({
             "archive_private": False,
@@ -222,6 +238,7 @@
             "name": u"test-snap",
             "proxy_url": self.proxy_url,
             "revocation_endpoint": self.revocation_endpoint,
+            "trusted_keys": expected_trusted_keys,
             }, args)
 
     @defer.inlineCallbacks
@@ -230,8 +247,9 @@
         # job for a Git branch.
         [ref] = self.factory.makeGitRefs()
         job = self.makeJob(git_ref=ref)
-        expected_archives = yield get_sources_list_for_building(
-            job.build, job.build.distro_arch_series, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, job.build.distro_arch_series, None))
         args = yield job._extraBuildArgs()
         self.assertEqual({
             "archive_private": False,
@@ -242,6 +260,7 @@
             "name": u"test-snap",
             "proxy_url": self.proxy_url,
             "revocation_endpoint": self.revocation_endpoint,
+            "trusted_keys": expected_trusted_keys,
             }, args)
 
     @defer.inlineCallbacks
@@ -252,8 +271,9 @@
         ref = self.factory.makeGitRefRemote(
             repository_url=url, path=u"refs/heads/master")
         job = self.makeJob(git_ref=ref)
-        expected_archives = yield get_sources_list_for_building(
-            job.build, job.build.distro_arch_series, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, job.build.distro_arch_series, None))
         args = yield job._extraBuildArgs()
         self.assertEqual({
             "archive_private": False,
@@ -264,10 +284,29 @@
             "name": u"test-snap",
             "proxy_url": self.proxy_url,
             "revocation_endpoint": self.revocation_endpoint,
+            "trusted_keys": expected_trusted_keys,
             }, args)
 
     @defer.inlineCallbacks
-    def test_extraBuildArgs_proxy_url_set(self):
+    def test_extraBuildArgs_archive_trusted_keys(self):
+        # If the archive has a signing key, _extraBuildArgs sends it.
+        yield self.useFixture(InProcessKeyServerFixture()).start()
+        archive = self.factory.makeArchive()
+        key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
+        yield IArchiveSigningKey(archive).setSigningKey(
+            key_path, async_keyserver=True)
+        job = self.makeJob(archive=archive)
+        self.factory.makeBinaryPackagePublishingHistory(
+            distroarchseries=job.build.distro_arch_series,
+            pocket=job.build.pocket, archive=archive,
+            status=PackagePublishingStatus.PUBLISHED)
+        args = yield job._extraBuildArgs()
+        self.assertThat(args["trusted_keys"], MatchesListwise([
+            Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
+            ]))
+
+    @defer.inlineCallbacks
+    def test_composeBuildRequest_proxy_url_set(self):
         job = self.makeJob()
         build_request = yield job.composeBuildRequest(None)
         proxy_url = ("http://{username}:{password}";

=== modified file 'lib/lp/soyuz/adapters/archivedependencies.py'
--- lib/lp/soyuz/adapters/archivedependencies.py	2016-04-07 00:04:42 +0000
+++ lib/lp/soyuz/adapters/archivedependencies.py	2017-04-29 23:54:40 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Archive dependencies helper function.
@@ -36,11 +36,15 @@
     'pocket_dependencies',
     ]
 
+import base64
 import logging
 import traceback
 
 from lazr.uri import URI
+from twisted.internet import defer
+from twisted.internet.threads import deferToThread
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from lp.app.errors import NotFoundError
 from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
@@ -48,6 +52,11 @@
     PackagePublishingPocket,
     pocketsuffix,
     )
+from lp.services.gpg.interfaces import (
+    GPGKeyNotFoundError,
+    IGPGHandler,
+    )
+from lp.services.timeout import default_timeout
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
@@ -229,16 +238,19 @@
     return deps
 
 
+@defer.inlineCallbacks
 def get_sources_list_for_building(build, distroarchseries, sourcepackagename,
                                   tools_source=None, logger=None):
-    """Return the sources_list entries required to build the given item.
+    """Return sources.list entries and keys required to build the given item.
 
-    The entries are returned in the order that is most useful;
+    The sources.list entries are returned in the order that is most useful:
      1. the context archive itself
      2. external dependencies
      3. user-selected archive dependencies
      4. the default primary archive
 
+    The keys are in an arbitrary order.
+
     :param build: a context `IBuild`.
     :param distroarchseries: A `IDistroArchSeries`
     :param sourcepackagename: A source package name (as text)
@@ -246,14 +258,16 @@
         additional dependency for build tools, just before the default
         primary archive.
     :param logger: an optional logger.
-    :return: a deb sources_list entries (lines).
+    :return: a Deferred resolving to a tuple containing a list of deb
+        sources.list entries (lines) and a list of base64-encoded public
+        keys.
     """
     deps = expand_dependencies(
         build.archive, distroarchseries, build.pocket,
         build.current_component, sourcepackagename,
         tools_source=tools_source, logger=logger)
-    sources_list_lines = \
-        _get_sources_list_for_dependencies(deps)
+    sources_list_lines, trusted_keys = (
+        yield _get_sources_list_for_dependencies(deps, logger=logger))
 
     external_dep_lines = []
     # Append external sources.list lines for this build if specified.  No
@@ -289,8 +303,9 @@
     # binaries that need to override primary binaries of the same
     # version), we want the external dependency lines to show up second:
     # after the archive itself, but before any other dependencies.
-    return [sources_list_lines[0]] + external_dep_lines + \
-           sources_list_lines[1:]
+    defer.returnValue(
+        ([sources_list_lines[0]] + external_dep_lines + sources_list_lines[1:],
+         trusted_keys))
 
 
 def _has_published_binaries(archive, distroarchseries, pocket):
@@ -323,8 +338,9 @@
     return 'deb %s %s %s' % (url, suite, ' '.join(components))
 
 
-def _get_sources_list_for_dependencies(dependencies):
-    """Return a list of sources_list lines.
+@defer.inlineCallbacks
+def _get_sources_list_for_dependencies(dependencies, logger=None):
+    """Return sources.list entries and keys.
 
     Process the given list of dependency tuples for the given
     `DistroArchseries`.
@@ -334,9 +350,15 @@
          list of `IComponent` names)
     :param distroarchseries: target `IDistroArchSeries`;
 
-    :return: a list of sources_list formatted lines.
+    :return: a tuple containing a list of sources.list formatted lines and a
+        list of base64-encoded public keys.
     """
     sources_list_lines = []
+    trusted_keys = {}
+    # The handler's security proxying doesn't protect anything useful here,
+    # and the thread that we defer key retrieval to doesn't have an
+    # interaction.
+    gpghandler = removeSecurityProxy(getUtility(IGPGHandler))
     for dep in dependencies:
         if isinstance(dep, basestring):
             sources_list_lines.append(dep)
@@ -356,8 +378,27 @@
             sources_list_line = _get_binary_sources_list_line(
                 archive, distro_arch_series, pocket, components)
             sources_list_lines.append(sources_list_line)
-
-    return sources_list_lines
+            fingerprint = archive.signing_key_fingerprint
+            if fingerprint is not None and fingerprint not in trusted_keys:
+                def get_key():
+                    with default_timeout(15.0):
+                        try:
+                            return gpghandler.retrieveKey(fingerprint)
+                        except GPGKeyNotFoundError as e:
+                            # For now, just log this and proceed without the
+                            # key.  We'll have to fix any outstanding cases
+                            # of this before we can switch to requiring
+                            # authentication across the board.
+                            if logger is not None:
+                                logger.warning(str(e))
+                            return None
+
+                key = yield deferToThread(get_key)
+                if key is not None:
+                    trusted_keys[fingerprint] = base64.b64encode(key.export())
+
+    defer.returnValue(
+        (sources_list_lines, [v for k, v in sorted(trusted_keys.items())]))
 
 
 def _get_default_primary_dependencies(archive, distro_arch_series, component,

=== modified file 'lib/lp/soyuz/adapters/tests/test_archivedependencies.py'
--- lib/lp/soyuz/adapters/tests/test_archivedependencies.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/adapters/tests/test_archivedependencies.py	2017-04-29 23:54:40 +0000
@@ -7,10 +7,20 @@
 
 __metaclass__ = type
 
-from testtools.matchers import StartsWith
+import os.path
+
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import (
+    MatchesSetwise,
+    StartsWith,
+    )
 import transaction
+from twisted.internet import defer
 from zope.component import getUtility
 
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.log.logger import BufferLogger
@@ -25,8 +35,11 @@
 from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.archive import IArchive
 from lp.soyuz.interfaces.component import IComponentSet
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import TestCaseWithFactory
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import (
     LaunchpadZopelessLayer,
     ZopelessDatabaseLayer,
@@ -111,10 +124,18 @@
     """Test sources.list contents for building, and related mechanisms."""
 
     layer = LaunchpadZopelessLayer
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
 
     ubuntu_components = [
         "main", "restricted", "universe", "multiverse", "partner"]
 
+    fingerprints = {
+        "ppa-sample@xxxxxxxxxxxxx": "0D57E99656BEFB0897606EE9A022DD1F5001B46D",
+        "ppa-sample-4096@xxxxxxxxxxxxx": (
+            "B7B1966662BA8D3F5A6ED89BD640F4A593B2CF67"),
+        }
+
+    @defer.inlineCallbacks
     def setUp(self):
         super(TestSourcesList, self).setUp()
         self.publisher = SoyuzTestPublisher()
@@ -126,6 +147,7 @@
             component = getUtility(IComponentSet)[component_name]
             if component not in self.hoary.components:
                 self.factory.makeComponentSelection(self.hoary, component)
+        yield self.useFixture(InProcessKeyServerFixture()).start()
 
     def test_defaults(self):
         # Non-primary archives by default use the Release, Security and
@@ -140,12 +162,18 @@
              PackagePublishingPocket.UPDATES),
             pocket_dependencies[default_pocket_dependency])
 
-    def makeArchive(self, publish_binary=False, **kwargs):
+    @defer.inlineCallbacks
+    def makeArchive(self, signing_key_name="ppa-sample@xxxxxxxxxxxxx",
+                    publish_binary=False, **kwargs):
         archive = self.factory.makeArchive(distribution=self.ubuntu, **kwargs)
+        if signing_key_name is not None:
+            key_path = os.path.join(gpgkeysdir, "%s.sec" % signing_key_name)
+            yield IArchiveSigningKey(archive).setSigningKey(
+                key_path, async_keyserver=True)
         if publish_binary:
             self.publisher.getPubBinaries(
                 archive=archive, status=PackagePublishingStatus.PUBLISHED)
-        return archive
+        defer.returnValue(archive)
 
     def makeBuild(self, **kwargs):
         pub_source = self.publisher.getPubSource(**kwargs)
@@ -159,77 +187,90 @@
                 build.archive, build.distro_series,
                 build.source_package_release.name).name)
 
-    def assertSourcesList(self, expected, build, **kwargs):
+    @defer.inlineCallbacks
+    def assertSourcesListAndKeys(self, expected_sources_list,
+                                 expected_key_names, build, **kwargs):
         expected_lines = []
-        for archive_or_prefix, suffixes in expected:
+        for archive_or_prefix, suffixes in expected_sources_list:
             if IArchive.providedBy(archive_or_prefix):
                 prefix = "deb %s " % archive_or_prefix.archive_url
             else:
                 prefix = archive_or_prefix + " "
             expected_lines.extend([prefix + suffix for suffix in suffixes])
-        sources_list = get_sources_list_for_building(
+        sources_list, trusted_keys = yield get_sources_list_for_building(
             build, build.distro_arch_series, build.source_package_release.name,
             **kwargs)
         self.assertEqual(expected_lines, sources_list)
+        key_matchers = [
+            Base64KeyMatches(self.fingerprints[key_name])
+            for key_name in expected_key_names]
+        self.assertThat(trusted_keys, MatchesSetwise(*key_matchers))
 
+    @defer.inlineCallbacks
     def test_ppa_with_no_binaries(self):
         # If there are no published binaries in a PPA, only its primary
         # archive dependencies need to be considered.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         build = self.makeBuild(archive=ppa)
         self.assertEqual(
             0, ppa.getAllPublishedBinaries(
                 distroarchseries=build.distro_arch_series,
                 status=PackagePublishingStatus.PUBLISHED).count())
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_ppa_with_binaries(self):
         # If there are binaries published in a PPA, then the PPA is
         # considered as well as its primary dependencies.
-        ppa = self.makeArchive(publish_binary=True)
+        ppa = yield self.makeArchive(publish_binary=True)
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], ["ppa-sample@xxxxxxxxxxxxx"], build)
 
+    @defer.inlineCallbacks
     def test_dependent_ppa_with_no_binaries(self):
         # A depended-upon PPA is not considered if it has no published
         # binaries.
-        lower_ppa = self.makeArchive()
-        upper_ppa = self.makeArchive(publish_binary=True)
+        lower_ppa = yield self.makeArchive(
+            signing_key_name="ppa-sample-4096@xxxxxxxxxxxxx")
+        upper_ppa = yield self.makeArchive(publish_binary=True)
         upper_ppa.addArchiveDependency(
             lower_ppa, PackagePublishingPocket.RELEASE,
             getUtility(IComponentSet)["main"])
         build = self.makeBuild(archive=upper_ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(upper_ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], ["ppa-sample@xxxxxxxxxxxxx"], build)
 
+    @defer.inlineCallbacks
     def test_dependent_ppa_with_binaries(self):
         # A depended-upon PPA is considered if it has published binaries.
-        lower_ppa = self.makeArchive(publish_binary=True)
-        upper_ppa = self.makeArchive(publish_binary=True)
+        lower_ppa = yield self.makeArchive(
+            signing_key_name="ppa-sample-4096@xxxxxxxxxxxxx",
+            publish_binary=True)
+        upper_ppa = yield self.makeArchive(publish_binary=True)
         upper_ppa.addArchiveDependency(
             lower_ppa, PackagePublishingPocket.RELEASE,
             getUtility(IComponentSet)["main"])
         build = self.makeBuild(archive=upper_ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(upper_ppa, ["hoary main"]),
              (lower_ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
@@ -237,14 +278,19 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ],
+            ["ppa-sample@xxxxxxxxxxxxx", "ppa-sample-4096@xxxxxxxxxxxxx"],
+            build)
 
+    @defer.inlineCallbacks
     def test_lax_supported_component_dependencies(self):
         # Dependencies for series with
         # strict_supported_component_dependencies=False are reasonable.
         # PPAs only have the "main" component.
-        lower_ppa = self.makeArchive(publish_binary=True)
-        upper_ppa = self.makeArchive(publish_binary=True)
+        lower_ppa = yield self.makeArchive(
+            signing_key_name="ppa-sample-4096@xxxxxxxxxxxxx",
+            publish_binary=True)
+        upper_ppa = yield self.makeArchive(publish_binary=True)
         upper_ppa.addArchiveDependency(
             lower_ppa, PackagePublishingPocket.RELEASE,
             getUtility(IComponentSet)["main"])
@@ -252,7 +298,7 @@
             self.ubuntu.main_archive, PackagePublishingPocket.UPDATES,
             getUtility(IComponentSet)["restricted"])
         build = self.makeBuild(archive=upper_ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(upper_ppa, ["hoary main"]),
              (lower_ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
@@ -260,10 +306,12 @@
                  "hoary-security main restricted",
                  "hoary-updates main restricted",
                  ]),
-             ], build)
+             ],
+            ["ppa-sample@xxxxxxxxxxxxx", "ppa-sample-4096@xxxxxxxxxxxxx"],
+            build)
         self.hoary.strict_supported_component_dependencies = False
         transaction.commit()
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(upper_ppa, ["hoary main"]),
              (lower_ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
@@ -271,41 +319,45 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ],
+            ["ppa-sample@xxxxxxxxxxxxx", "ppa-sample-4096@xxxxxxxxxxxxx"],
+            build)
 
+    @defer.inlineCallbacks
     def test_no_op_primary_archive_dependency(self):
         # Overriding the default primary archive dependencies with exactly
         # the same values has no effect.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         ppa.addArchiveDependency(
             self.ubuntu.main_archive, PackagePublishingPocket.UPDATES,
             getUtility(IComponentSet)["multiverse"])
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_primary_archive_dependency_security(self):
         # The primary archive dependency can be modified to behave as an
         # embargoed archive that builds security updates.  This is done by
         # setting the SECURITY pocket dependencies (RELEASE and SECURITY)
         # and following the component dependencies of the component where
         # the source was last published in the primary archive.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         ppa.addArchiveDependency(
             self.ubuntu.main_archive, PackagePublishingPocket.SECURITY)
         build = self.makeBuild(archive=ppa)
         self.assertPrimaryCurrentComponent("universe", build)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main universe",
                  "hoary-security main universe",
                  ]),
-             ], build)
+             ], [], build)
         self.publisher.getPubSource(
             sourcename="with-ancestry", version="1.0",
             archive=self.ubuntu.main_archive)
@@ -313,59 +365,63 @@
             sourcename="with-ancestry", version="1.1",
             archive=ppa).createMissingBuilds()
         self.assertPrimaryCurrentComponent("main", build_with_ancestry)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main",
                  "hoary-security main",
                  ]),
-             ], build_with_ancestry)
+             ], [], build_with_ancestry)
 
+    @defer.inlineCallbacks
     def test_primary_archive_dependency_release(self):
         # The primary archive dependency can be modified to behave as a
         # pristine build environment based only on what was included in the
         # original release of the corresponding series.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         ppa.addArchiveDependency(
             self.ubuntu.main_archive, PackagePublishingPocket.RELEASE,
             getUtility(IComponentSet)["restricted"])
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
-            [(self.ubuntu.main_archive, ["hoary main restricted"])], build)
+        yield self.assertSourcesListAndKeys(
+            [(self.ubuntu.main_archive, ["hoary main restricted"])], [], build)
 
+    @defer.inlineCallbacks
     def test_primary_archive_dependency_proposed(self):
         # The primary archive dependency can be modified to extend the build
         # environment for PROPOSED.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         ppa.addArchiveDependency(
             self.ubuntu.main_archive, PackagePublishingPocket.PROPOSED,
             getUtility(IComponentSet)["multiverse"])
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  "hoary-proposed main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_primary_archive_dependency_backports(self):
         # The primary archive dependency can be modified to extend the build
         # environment for PROPOSED.
-        ppa = self.makeArchive()
+        ppa = yield self.makeArchive()
         ppa.addArchiveDependency(
             self.ubuntu.main_archive, PackagePublishingPocket.BACKPORTS,
             getUtility(IComponentSet)["multiverse"])
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  "hoary-backports main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_partner(self):
         # Similarly to what happens with PPA builds, partner builds may
         # depend on any component in the primary archive.  This behaviour
@@ -377,15 +433,16 @@
             archive=partner, component="partner",
             status=PackagePublishingStatus.PUBLISHED)
         build = self.makeBuild(archive=partner, component="partner")
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(partner, ["hoary partner"]),
              (primary, [
                  "hoary main restricted universe multiverse",
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_partner_proposed(self):
         # The partner archive's PROPOSED pocket builds against itself, but
         # still uses the default UPDATES dependency for the primary archive
@@ -401,7 +458,7 @@
         build = self.makeBuild(
             archive=partner, component="partner",
             pocket=PackagePublishingPocket.PROPOSED)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(partner, [
                  "hoary partner",
                  "hoary-proposed partner",
@@ -411,19 +468,20 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], [], build)
 
+    @defer.inlineCallbacks
     def test_archive_external_dependencies(self):
         # An archive can be manually given additional external dependencies.
         # If present, "%(series)s" is replaced with the series name for the
         # build being dispatched.
-        ppa = self.makeArchive(publish_binary=True)
+        ppa = yield self.makeArchive(publish_binary=True)
         ppa.external_dependencies = (
             "deb http://user:pass@repository zoing everything\n"
             "deb http://user:pass@repository %(series)s public private\n"
             "deb http://user:pass@repository %(series)s-extra public")
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(ppa, ["hoary main"]),
              ("deb http://user:pass@repository";, [
                  "zoing everything",
@@ -435,16 +493,17 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], ["ppa-sample@xxxxxxxxxxxxx"], build)
 
+    @defer.inlineCallbacks
     def test_build_external_dependencies(self):
         # A single build can be manually given additional external
         # dependencies.
-        ppa = self.makeArchive(publish_binary=True)
+        ppa = yield self.makeArchive(publish_binary=True)
         build = self.makeBuild(archive=ppa)
         build.api_external_dependencies = (
             "deb http://user:pass@repository foo bar")
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(ppa, ["hoary main"]),
              ("deb http://user:pass@repository";, ["foo bar"]),
              (self.ubuntu.main_archive, [
@@ -452,14 +511,15 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build)
+             ], ["ppa-sample@xxxxxxxxxxxxx"], build)
 
+    @defer.inlineCallbacks
     def test_build_tools(self):
         # We can force an extra build tools line to be added to
         # sources.list, which is useful for specialised build types.
-        ppa = self.makeArchive(publish_binary=True)
+        ppa = yield self.makeArchive(publish_binary=True)
         build = self.makeBuild(archive=ppa)
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(ppa, ["hoary main"]),
              ("deb http://example.org";, ["hoary main"]),
              (self.ubuntu.main_archive, [
@@ -467,15 +527,18 @@
                  "hoary-security main restricted universe multiverse",
                  "hoary-updates main restricted universe multiverse",
                  ]),
-             ], build, tools_source="deb http://example.org %(series)s main")
+             ],
+            ["ppa-sample@xxxxxxxxxxxxx"], build,
+            tools_source="deb http://example.org %(series)s main")
 
+    @defer.inlineCallbacks
     def test_build_tools_bad_formatting(self):
         # If tools_source is badly formatted, we log the error but don't
         # blow up.  (Note the missing "s" at the end of "%(series)".)
-        ppa = self.makeArchive(publish_binary=True)
+        ppa = yield self.makeArchive(publish_binary=True)
         build = self.makeBuild(archive=ppa)
         logger = BufferLogger()
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(ppa, ["hoary main"]),
              (self.ubuntu.main_archive, [
                  "hoary main restricted universe multiverse",
@@ -483,11 +546,13 @@
                  "hoary-updates main restricted universe multiverse",
                  ]),
              ],
-            build, tools_source="deb http://example.org %(series) main",
+            ["ppa-sample@xxxxxxxxxxxxx"], build,
+            tools_source="deb http://example.org %(series) main",
             logger=logger)
         self.assertThat(logger.getLogBuffer(), StartsWith(
             "ERROR Exception processing build tools sources.list entry:\n"))
 
+    @defer.inlineCallbacks
     def test_overlay(self):
         # An overlay distroseries is a derived distribution which works like
         # a PPA.  This means that the parent's details gets added to the
@@ -508,10 +573,10 @@
             pocket=PackagePublishingPocket.SECURITY,
             component=getUtility(IComponentSet)["universe"])
         build = self.makeBuild()
-        self.assertSourcesList(
+        yield self.assertSourcesListAndKeys(
             [(self.ubuntu.main_archive, ["hoary main"]),
              (depdistro.main_archive, [
                  "depseries main universe",
                  "depseries-security main universe",
                  ]),
-             ], build)
+             ], [], build)

=== modified file 'lib/lp/soyuz/model/binarypackagebuildbehaviour.py'
--- lib/lp/soyuz/model/binarypackagebuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/model/binarypackagebuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -164,8 +164,9 @@
             args["ogrecomponent"] = (
                 build.current_component.name)
 
-        args['archives'] = yield get_sources_list_for_building(
-            build, das, build.source_package_release.name, logger=logger)
+        args['archives'], args['trusted_keys'] = (
+            yield get_sources_list_for_building(
+                build, das, build.source_package_release.name, logger=logger))
         args['archive_private'] = build.archive.private
         args['build_debug_symbols'] = build.archive.build_debug_symbols
 

=== modified file 'lib/lp/soyuz/model/livefsbuildbehaviour.py'
--- lib/lp/soyuz/model/livefsbuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/model/livefsbuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -90,8 +90,9 @@
         args["pocket"] = build.pocket.name.lower()
         args["arch_tag"] = build.distro_arch_series.architecturetag
         args["datestamp"] = build.version
-        args["archives"] = yield get_sources_list_for_building(
-            build, build.distro_arch_series, None, logger=logger)
+        args["archives"], args["trusted_keys"] = (
+            yield get_sources_list_for_building(
+                build, build.distro_arch_series, None, logger=logger))
         args["archive_private"] = build.archive.private
         defer.returnValue(args)
 

=== modified file 'lib/lp/soyuz/tests/soyuz.py'
--- lib/lp/soyuz/tests/soyuz.py	2015-09-28 17:38:45 +0000
+++ lib/lp/soyuz/tests/soyuz.py	2017-04-29 23:54:40 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Helper functions/classes for Soyuz tests."""
@@ -6,14 +6,22 @@
 __metaclass__ = type
 
 __all__ = [
+    'Base64KeyMatches',
     'SoyuzTestHelper',
     ]
 
+import base64
+
+from testtools.matchers import (
+    Equals,
+    Matcher,
+    )
 from zope.component import getUtility
 
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.gpg.interfaces import IGPGHandler
 from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 from lp.testing.sampledata import (
@@ -94,3 +102,15 @@
         Return True if the lists matches, otherwise False.
         """
         return [p.id for p in expected] == [r.id for r in given]
+
+
+class Base64KeyMatches(Matcher):
+    """Matches if base64-encoded key material has a given fingerprint."""
+
+    def __init__(self, fingerprint):
+        self.fingerprint = fingerprint
+
+    def match(self, encoded_key):
+        key = base64.b64decode(encoded_key)
+        return Equals(self.fingerprint).match(
+            getUtility(IGPGHandler).importPublicKey(key).fingerprint)

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2017-04-29 23:54:40 +0000
@@ -9,6 +9,7 @@
     timedelta,
     )
 import doctest
+import os.path
 
 from pytz import UTC
 from testtools.deferredruntest import AsynchronousDeferredRunTest
@@ -16,6 +17,7 @@
     AllMatch,
     DocTestMatches,
     LessThan,
+    MatchesListwise,
     MatchesPredicate,
     MatchesRegex,
     MatchesStructure,
@@ -29,6 +31,9 @@
 
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.buildmaster.enums import (
     BuildQueueStatus,
     BuildStatus,
@@ -108,6 +113,7 @@
     BinaryPackageReleaseDownloadCount,
     )
 from lp.soyuz.model.component import ComponentSelection
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import (
     admin_logged_in,
@@ -120,6 +126,8 @@
     StormStatementRecorder,
     TestCaseWithFactory,
     )
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -1748,12 +1756,17 @@
 class TestArchiveDependencies(TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer
-    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
 
     @defer.inlineCallbacks
     def test_private_sources_list(self):
         """Entries for private dependencies include credentials."""
         p3a = self.factory.makeArchive(name='p3a', private=True)
+        with InProcessKeyServerFixture() as keyserver:
+            yield keyserver.start()
+            key_path = os.path.join(gpgkeysdir, 'ppa-sample@xxxxxxxxxxxxxxxxx')
+            yield IArchiveSigningKey(p3a).setSigningKey(
+                key_path, async_keyserver=True)
         dependency = self.factory.makeArchive(
             name='dependency', private=True, owner=p3a.owner)
         with person_logged_in(p3a.owner):
@@ -1764,13 +1777,16 @@
                 PackagePublishingPocket.RELEASE)
             build = self.factory.makeBinaryPackageBuild(archive=p3a,
                 distroarchseries=bpph.distroarchseries)
-            sources_list = yield get_sources_list_for_building(
+            sources_list, trusted_keys = yield get_sources_list_for_building(
                 build, build.distro_arch_series,
                 build.source_package_release.name)
             matches = MatchesRegex(
                 "deb http://buildd:sekrit@xxxxxxxxxxxxxxxxxxxxxxxxx/";
                 "person-name-.*/dependency/ubuntu distroseries-.* main")
             self.assertThat(sources_list[0], matches)
+            self.assertThat(trusted_keys, MatchesListwise([
+                Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
+                ]))
 
     def test_invalid_external_dependencies(self):
         """Trying to set invalid external dependencies raises an exception."""
@@ -2018,7 +2034,7 @@
         self._createDep(
             test_publisher, series11, 'series12', 'depdistro4', 'multiverse',
             PackagePublishingPocket.UPDATES)
-        sources_list = yield get_sources_list_for_building(
+        sources_list, trusted_keys = yield get_sources_list_for_building(
             build, build.distro_arch_series, build.source_package_release.name)
 
         self.assertThat(
@@ -2040,6 +2056,7 @@
                 ".../depdistro4 series12-updates "
                     "main restricted universe multiverse\n",
                 doctest.ELLIPSIS))
+        self.assertEqual([], trusted_keys)
 
 
 class TestComponents(TestCaseWithFactory):

=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py'
--- lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -12,6 +12,7 @@
 
 from storm.store import Store
 from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import MatchesListwise
 import transaction
 from twisted.internet import defer
 from twisted.trial.unittest import TestCase as TrialTestCase
@@ -19,6 +20,9 @@
 from zope.security.proxy import removeSecurityProxy
 
 from lp.archivepublisher.diskpool import poolify
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.buildmaster.enums import (
     BuilderCleanStatus,
     BuildStatus,
@@ -55,9 +59,15 @@
 from lp.soyuz.adapters.archivedependencies import (
     get_sources_list_for_building,
     )
-from lp.soyuz.enums import ArchivePurpose
+from lp.soyuz.enums import (
+    ArchivePurpose,
+    PackagePublishingStatus,
+    )
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.testing import TestCaseWithFactory
 from lp.testing.dbuser import switch_dbuser
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
@@ -107,7 +117,7 @@
         das = build.distro_arch_series
         ds_name = das.distroseries.name
         suite = ds_name + pocketsuffix[build.pocket]
-        archives = yield get_sources_list_for_building(
+        archives, trusted_keys = yield get_sources_list_for_building(
             build, das, build.source_package_release.name)
         arch_indep = das.isNominatedArchIndep
         if component is None:
@@ -131,6 +141,7 @@
             'ogrecomponent': component,
             'distribution': das.distroseries.distribution.name,
             'suite': suite,
+            'trusted_keys': trusted_keys,
             }
         build_log = [
             ('build', build.build_cookie, 'binarypackage',
@@ -324,6 +335,23 @@
         extra_args = yield IBuildFarmJobBehaviour(build)._extraBuildArgs(build)
         self.assertTrue(extra_args['arch_indep'])
 
+    @defer.inlineCallbacks
+    def test_extraBuildArgs_archive_trusted_keys(self):
+        # If the archive has a signing key, _extraBuildArgs sends it.
+        yield self.useFixture(InProcessKeyServerFixture()).start()
+        archive = self.factory.makeArchive()
+        key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
+        yield IArchiveSigningKey(archive).setSigningKey(
+            key_path, async_keyserver=True)
+        build = self.factory.makeBinaryPackageBuild(archive=archive)
+        self.factory.makeBinaryPackagePublishingHistory(
+            distroarchseries=build.distro_arch_series, pocket=build.pocket,
+            archive=archive, status=PackagePublishingStatus.PUBLISHED)
+        args = yield IBuildFarmJobBehaviour(build)._extraBuildArgs(build)
+        self.assertThat(args["trusted_keys"], MatchesListwise([
+            Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
+            ]))
+
     def test_verifyBuildRequest(self):
         # Don't allow a virtual build on a non-virtual builder.
         archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)

=== modified file 'lib/lp/soyuz/tests/test_livefsbuildbehaviour.py'
--- lib/lp/soyuz/tests/test_livefsbuildbehaviour.py	2017-04-29 23:54:40 +0000
+++ lib/lp/soyuz/tests/test_livefsbuildbehaviour.py	2017-04-29 23:54:40 +0000
@@ -6,16 +6,21 @@
 __metaclass__ = type
 
 from datetime import datetime
+import os.path
 
 import fixtures
 import pytz
 from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import MatchesListwise
 import transaction
 from twisted.internet import defer
 from twisted.trial.unittest import TestCase as TrialTestCase
 from zope.component import getUtility
 from zope.security.proxy import Proxy
 
+from lp.archivepublisher.interfaces.archivesigningkey import (
+    IArchiveSigningKey,
+    )
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.builder import CannotBuild
 from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
@@ -38,13 +43,17 @@
 from lp.soyuz.adapters.archivedependencies import (
     get_sources_list_for_building,
     )
+from lp.soyuz.enums import PackagePublishingStatus
 from lp.soyuz.interfaces.archive import ArchiveDisabled
 from lp.soyuz.interfaces.livefs import (
     LIVEFS_FEATURE_FLAG,
     LiveFSBuildArchiveOwnerMismatch,
     )
 from lp.soyuz.model.livefsbuildbehaviour import LiveFSBuildBehaviour
+from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.testing import TestCaseWithFactory
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
@@ -56,9 +65,13 @@
         super(TestLiveFSBuildBehaviourBase, self).setUp()
         self.useFixture(FeatureFixture({LIVEFS_FEATURE_FLAG: u"on"}))
 
-    def makeJob(self, pocket=PackagePublishingPocket.RELEASE, **kwargs):
+    def makeJob(self, archive=None, pocket=PackagePublishingPocket.RELEASE,
+                **kwargs):
         """Create a sample `ILiveFSBuildBehaviour`."""
-        distribution = self.factory.makeDistribution(name="distro")
+        if archive is None:
+            distribution = self.factory.makeDistribution(name="distro")
+        else:
+            distribution = archive.distribution
         distroseries = self.factory.makeDistroSeries(
             distribution=distribution, name="unstable")
         processor = getUtility(IProcessorSet).getByName("386")
@@ -66,7 +79,7 @@
             distroseries=distroseries, architecturetag="i386",
             processor=processor)
         build = self.factory.makeLiveFSBuild(
-            distroarchseries=distroarchseries, pocket=pocket,
+            archive=archive, distroarchseries=distroarchseries, pocket=pocket,
             name=u"test-livefs", **kwargs)
         return IBuildFarmJobBehaviour(build)
 
@@ -173,8 +186,9 @@
         job = self.makeJob(
             date_created=datetime(2014, 4, 25, 10, 38, 0, tzinfo=pytz.UTC),
             metadata={"project": "distro", "subproject": "special"})
-        expected_archives = yield get_sources_list_for_building(
-            job.build, job.build.distro_arch_series, None)
+        expected_archives, expected_trusted_keys = (
+            yield get_sources_list_for_building(
+                job.build, job.build.distro_arch_series, None))
         extra_args = yield job._extraBuildArgs()
         self.assertEqual({
             "archive_private": False,
@@ -185,6 +199,7 @@
             "project": "distro",
             "subproject": "special",
             "series": "unstable",
+            "trusted_keys": expected_trusted_keys,
             }, extra_args)
 
     @defer.inlineCallbacks
@@ -209,6 +224,24 @@
         self.assertIsNot(Proxy, type(args["lb_args"]))
 
     @defer.inlineCallbacks
+    def test_extraBuildArgs_archive_trusted_keys(self):
+        # If the archive has a signing key, _extraBuildArgs sends it.
+        yield self.useFixture(InProcessKeyServerFixture()).start()
+        archive = self.factory.makeArchive()
+        key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
+        yield IArchiveSigningKey(archive).setSigningKey(
+            key_path, async_keyserver=True)
+        job = self.makeJob(archive=archive)
+        self.factory.makeBinaryPackagePublishingHistory(
+            distroarchseries=job.build.distro_arch_series,
+            pocket=job.build.pocket, archive=archive,
+            status=PackagePublishingStatus.PUBLISHED)
+        args = yield job._extraBuildArgs()
+        self.assertThat(args["trusted_keys"], MatchesListwise([
+            Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
+            ]))
+
+    @defer.inlineCallbacks
     def test_composeBuildRequest(self):
         job = self.makeJob()
         lfa = self.factory.makeLibraryFileAlias(db_only=True)


Follow ups