← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad-buildd/add-trusted-keys into lp:launchpad-buildd

 

Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/add-trusted-keys into lp:launchpad-buildd.

Commit message:
Write out trusted keys sent by buildd-manager (LP: #1626739).

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

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/add-trusted-keys/+merge/323228

Historically, we've relied (at least for PPAs) on builders having a secure internal transport to the archives they use as build-dependencies, even though this isn't good policy in general.  However, we now need to do better since snapcraft sometimes does its own repository fetching and doesn't provide a way to disable authentication.

The other side of this is in Launchpad and is rather more involved, but it's safe to deploy this side of it in the meantime.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/add-trusted-keys into lp:launchpad-buildd.
=== modified file 'MANIFEST.in'
--- MANIFEST.in	2015-11-04 14:10:28 +0000
+++ MANIFEST.in	2017-04-26 12:28:05 +0000
@@ -1,5 +1,6 @@
 include LICENSE
 include Makefile
+include add-trusted-keys
 include buildd-genconfig
 include buildd-slave.tac
 include buildlivefs

=== modified file 'Makefile'
--- Makefile	2015-07-31 11:54:07 +0000
+++ Makefile	2017-04-26 12:28:05 +0000
@@ -31,6 +31,7 @@
 		   lpbuildd.tests.test_buildd_slave \
 		   lpbuildd.tests.test_buildrecipe \
 		   lpbuildd.tests.test_check_implicit_pointer_functions \
+		   lpbuildd.tests.test_debian \
 		   lpbuildd.tests.test_harness \
 		   lpbuildd.tests.test_livefs \
 		   lpbuildd.tests.test_snap \

=== added file 'add-trusted-keys'
--- add-trusted-keys	1970-01-01 00:00:00 +0000
+++ add-trusted-keys	2017-04-26 12:28:05 +0000
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+# Write out new trusted keys in the chroot.
+
+# Expects the build id as the first argument, and key material on stdin.
+
+set -e
+
+SUDO=/usr/bin/sudo
+CHROOT=/usr/sbin/chroot
+
+BUILDID="$1"
+ROOT="$HOME/build-$BUILDID/chroot-autobuild"
+
+cd $HOME
+cd "$ROOT/etc/apt"
+
+echo "Adding trusted keys to build-$BUILDID"
+
+$SUDO $CHROOT $ROOT apt-key add -
+$SUDO $CHROOT $ROOT apt-key list

=== modified file 'debian/changelog'
--- debian/changelog	2017-02-10 14:55:42 +0000
+++ debian/changelog	2017-04-26 12:28:05 +0000
@@ -1,3 +1,9 @@
+launchpad-buildd (143) UNRELEASED; urgency=medium
+
+  * Write out trusted keys sent by buildd-manager (LP: #1626739).
+
+ -- Colin Watson <cjwatson@xxxxxxxxxx>  Wed, 19 Apr 2017 11:23:20 +0100
+
 launchpad-buildd (142) trusty; urgency=medium
 
   * lpbuildd.binarypackage: Pass DEB_BUILD_OPTIONS=noautodbgsym if we have

=== modified file 'debian/launchpad-buildd.install'
--- debian/launchpad-buildd.install	2015-08-25 09:23:24 +0000
+++ debian/launchpad-buildd.install	2017-04-26 12:28:05 +0000
@@ -3,6 +3,7 @@
 debian/upgrade-config		usr/share/launchpad-buildd
 sbuildrc			usr/share/launchpad-buildd
 template-buildd-slave.conf	usr/share/launchpad-buildd
+add-trusted-keys		usr/share/launchpad-buildd/slavebin
 buildlivefs			usr/share/launchpad-buildd/slavebin
 buildrecipe			usr/share/launchpad-buildd/slavebin
 buildsnap			usr/share/launchpad-buildd/slavebin

=== modified file 'lpbuildd/debian.py'
--- lpbuildd/debian.py	2016-02-18 14:44:09 +0000
+++ lpbuildd/debian.py	2017-04-26 12:28:05 +0000
@@ -8,6 +8,7 @@
 
 __metaclass__ = type
 
+import base64
 import os
 import re
 import signal
@@ -25,6 +26,7 @@
     UNPACK = "UNPACK"
     MOUNT = "MOUNT"
     SOURCES = "SOURCES"
+    KEYS = "KEYS"
     UPDATE = "UPDATE"
     UMOUNT = "UMOUNT"
     CLEANUP = "CLEANUP"
@@ -38,6 +40,7 @@
         self._updatepath = os.path.join(self._slavebin, "update-debian-chroot")
         self._sourcespath = os.path.join(
             self._slavebin, "override-sources-list")
+        self._keyspath = os.path.join(self._slavebin, "add-trusted-keys")
         self._cachepath = slave._config.get("slave", "filecache")
         self._state = DebianBuildState.INIT
         slave.emptyLog()
@@ -52,6 +55,7 @@
 
         self.arch_tag = extra_args.get('arch_tag', self._slave.getArch())
         self.sources_list = extra_args.get('archives')
+        self.trusted_keys = extra_args.get('trusted_keys')
 
         BuildManager.initiate(self, files, chroot, extra_args)
 
@@ -64,6 +68,14 @@
         args.extend(self.sources_list)
         self.runSubProcess(self._sourcespath, args)
 
+    def doTrustedKeys(self):
+        """Add trusted keys."""
+        trusted_keys = b"".join(
+            base64.b64decode(key) for key in self.trusted_keys)
+        self.runSubProcess(
+            self._keyspath, ["add-trusted-keys", self._buildid],
+            stdin=trusted_keys)
+
     def doUpdateChroot(self):
         """Perform the chroot upgrade."""
         self.runSubProcess(
@@ -177,6 +189,9 @@
             if self.sources_list is not None:
                 self._state = DebianBuildState.SOURCES
                 self.doSourcesList()
+            elif self.trusted_keys:
+                self._state = DebianBuildState.KEYS
+                self.doTrustedKeys()
             else:
                 self._state = DebianBuildState.UPDATE
                 self.doUpdateChroot()
@@ -229,6 +244,9 @@
                 self._slave.chrootFail()
                 self.alreadyfailed = True
             self.doReapProcesses(self._state)
+        elif self.trusted_keys:
+            self._state = DebianBuildState.KEYS
+            self.doTrustedKeys()
         else:
             self._state = DebianBuildState.UPDATE
             self.doUpdateChroot()
@@ -238,6 +256,22 @@
         self._state = DebianBuildState.UMOUNT
         self.doUnmounting()
 
+    def iterate_KEYS(self, success):
+        """Just finished adding trusted keys."""
+        if success != 0:
+            if not self.alreadyfailed:
+                self._slave.chrootFail()
+                self.alreadyfailed = True
+            self.doReapProcesses(self._state)
+        else:
+            self._state = DebianBuildState.UPDATE
+            self.doUpdateChroot()
+
+    def iterateReap_KEYS(self, success):
+        """Just finished reaping after failure to add trusted keys."""
+        self._state = DebianBuildState.UMOUNT
+        self.doUnmounting()
+
     def iterate_UPDATE(self, success):
         """Just finished updating the chroot."""
         if success != 0:

=== modified file 'lpbuildd/slave.py'
--- lpbuildd/slave.py	2016-12-09 18:04:00 +0000
+++ lpbuildd/slave.py	2017-04-26 12:28:05 +0000
@@ -52,12 +52,19 @@
 class RunCapture(protocol.ProcessProtocol):
     """Run a command and capture its output to a slave's log"""
 
-    def __init__(self, slave, callback):
+    def __init__(self, slave, callback, stdin=None):
         self.slave = slave
         self.notify = callback
+        self.stdin = stdin
         self.builderFailCall = None
         self.ignore = False
 
+    def connectionMade(self):
+        """Write any stdin data."""
+        if self.stdin is not None:
+            self.transport.write(self.stdin)
+            self.transport.closeStdin()
+
     def outReceived(self, data):
         """Pass on stdout data to the log."""
         self.slave.log(data)
@@ -121,13 +128,17 @@
     def needs_sanitized_logs(self):
         return self.is_archive_private
 
-    def runSubProcess(self, command, args, iterate=None, env=None):
+    def runSubProcess(self, command, args, iterate=None, stdin=None, env=None):
         """Run a sub process capturing the results in the log."""
         if iterate is None:
             iterate = self.iterate
-        self._subprocess = RunCapture(self._slave, iterate)
+        self._subprocess = RunCapture(self._slave, iterate, stdin=stdin)
         self._slave.log("RUN: %s %r\n" % (command, args))
-        childfds = {0: devnull.fileno(), 1: "r", 2: "r"}
+        childfds = {
+            0: devnull.fileno() if stdin is None else "w",
+            1: "r",
+            2: "r",
+            }
         self._reactor.spawnProcess(
             self._subprocess, command, args, env=env,
             path=self.home, childFDs=childfds)

=== modified file 'lpbuildd/tests/fakeslave.py'
--- lpbuildd/tests/fakeslave.py	2013-09-27 10:34:13 +0000
+++ lpbuildd/tests/fakeslave.py	2017-04-26 12:28:05 +0000
@@ -77,7 +77,8 @@
         self.waitingfiles = {}
         for fake_method in (
             "storeFile", "addWaitingFile", "emptyLog", "log",
-            "chrootFail", "buildFail", "builderFail", "depFail",
+            "chrootFail", "buildFail", "builderFail", "depFail", "buildOK",
+            "buildComplete",
             ):
             setattr(self, fake_method, FakeMethod())
 

=== added file 'lpbuildd/tests/test_debian.py'
--- lpbuildd/tests/test_debian.py	1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/test_debian.py	2017-04-26 12:28:05 +0000
@@ -0,0 +1,294 @@
+# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import base64
+import os.path
+import shutil
+import tempfile
+
+from testtools import TestCase
+from twisted.internet.task import Clock
+
+from lpbuildd.debian import (
+    DebianBuildManager,
+    DebianBuildState,
+    )
+from lpbuildd.tests.fakeslave import FakeSlave
+
+
+class MockBuildState(DebianBuildState):
+
+    MAIN = "MAIN"
+
+
+class MockBuildManager(DebianBuildManager):
+
+    initial_build_state = MockBuildState.MAIN
+
+    def __init__(self, *args, **kwargs):
+        super(MockBuildManager, self).__init__(*args, **kwargs)
+        self.commands = []
+        self.iterators = []
+        self.arch_indep = False
+
+    def runSubProcess(self, path, command, iterate=None, stdin=None):
+        self.commands.append(([path] + command, stdin))
+        if iterate is None:
+            iterate = self.iterate
+        self.iterators.append(iterate)
+        return 0
+
+    def doRunBuild(self):
+        self.runSubProcess('/bin/true', ['true'])
+
+    def iterate_MAIN(self, success):
+        if success != 0:
+            if not self.alreadyfailed:
+                self._slave.buildFail()
+            self.alreadyfailed = True
+        self.doReapProcesses(self._state)
+
+    def iterateReap_MAIN(self, success):
+        self._state = DebianBuildState.UMOUNT
+        self.doUnmounting()
+
+
+class TestDebianBuildManagerIteration(TestCase):
+    """Run a generic DebianBuildManager through its iteration steps."""
+
+    def setUp(self):
+        super(TestDebianBuildManagerIteration, self).setUp()
+        self.working_dir = tempfile.mkdtemp()
+        self.addCleanup(lambda: shutil.rmtree(self.working_dir))
+        slave_dir = os.path.join(self.working_dir, 'slave')
+        home_dir = os.path.join(self.working_dir, 'home')
+        for dir in (slave_dir, home_dir):
+            os.mkdir(dir)
+        self.slave = FakeSlave(slave_dir)
+        self.buildid = '123'
+        self.clock = Clock()
+        self.buildmanager = MockBuildManager(
+            self.slave, self.buildid, reactor=self.clock)
+        self.buildmanager.home = home_dir
+        self.buildmanager._cachepath = self.slave._cachepath
+        self.chrootdir = os.path.join(
+            home_dir, 'build-%s' % self.buildid, 'chroot-autobuild')
+
+    def getState(self):
+        """Retrieve build manager's state."""
+        return self.buildmanager._state
+
+    def startBuild(self, extra_args):
+        # The build manager's iterate() kicks off the consecutive states
+        # after INIT.
+        self.buildmanager.initiate({}, 'chroot.tar.gz', extra_args)
+        self.assertEqual(DebianBuildState.INIT, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/slave-prep', 'slave-prep'], None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+    def test_iterate(self):
+        # The build manager iterates a normal build from start to finish.
+        extra_args = {
+            'arch_tag': 'amd64',
+            'archives': [
+                'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main',
+                ],
+            }
+        self.startBuild(extra_args)
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.UNPACK, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/unpack-chroot', 'unpack-chroot',
+              self.buildid,
+              os.path.join(self.buildmanager._cachepath, 'chroot.tar.gz')],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.MOUNT, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/mount-chroot', 'mount-chroot', self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.SOURCES, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/override-sources-list',
+              'override-sources-list', self.buildid,
+              'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main'],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.UPDATE, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/update-debian-chroot',
+              'update-debian-chroot', self.buildid, 'amd64'],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(MockBuildState.MAIN, self.getState())
+        self.assertEqual(
+            (['/bin/true', 'true'], None), self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(MockBuildState.MAIN, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+              self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertNotEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterateReap(self.getState(), 0)
+        self.assertEqual(DebianBuildState.UMOUNT, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/umount-chroot', 'umount-chroot',
+              self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.CLEANUP, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/remove-build', 'remove-build', self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertFalse(self.slave.wasCalled('builderFail'))
+        self.assertFalse(self.slave.wasCalled('chrootFail'))
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+        self.assertFalse(self.slave.wasCalled('depFail'))
+        self.assertTrue(self.slave.wasCalled('buildOK'))
+        self.assertTrue(self.slave.wasCalled('buildComplete'))
+
+    def test_iterate_trusted_keys(self):
+        # The build manager iterates a build with trusted keys from start to
+        # finish.
+        extra_args = {
+            'arch_tag': 'amd64',
+            'archives': [
+                'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main',
+                ],
+            'trusted_keys': [base64.b64encode(b'key material')],
+            }
+        self.startBuild(extra_args)
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.UNPACK, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/unpack-chroot', 'unpack-chroot',
+              self.buildid,
+              os.path.join(self.buildmanager._cachepath, 'chroot.tar.gz')],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.MOUNT, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/mount-chroot', 'mount-chroot', self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.SOURCES, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/override-sources-list',
+              'override-sources-list', self.buildid,
+              'deb http://ppa.launchpad.dev/owner/name/ubuntu xenial main'],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.KEYS, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/add-trusted-keys', 'add-trusted-keys',
+              self.buildid],
+             b'key material'),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.UPDATE, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/update-debian-chroot',
+              'update-debian-chroot', self.buildid, 'amd64'],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(MockBuildState.MAIN, self.getState())
+        self.assertEqual(
+            (['/bin/true', 'true'], None), self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(MockBuildState.MAIN, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/scan-for-processes', 'scan-for-processes',
+              self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertNotEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterateReap(self.getState(), 0)
+        self.assertEqual(DebianBuildState.UMOUNT, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/umount-chroot', 'umount-chroot',
+              self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertEqual(DebianBuildState.CLEANUP, self.getState())
+        self.assertEqual(
+            (['sharepath/slavebin/remove-build', 'remove-build', self.buildid],
+             None),
+            self.buildmanager.commands[-1])
+        self.assertEqual(
+            self.buildmanager.iterate, self.buildmanager.iterators[-1])
+
+        self.buildmanager.iterate(0)
+        self.assertFalse(self.slave.wasCalled('builderFail'))
+        self.assertFalse(self.slave.wasCalled('chrootFail'))
+        self.assertFalse(self.slave.wasCalled('buildFail'))
+        self.assertFalse(self.slave.wasCalled('depFail'))
+        self.assertTrue(self.slave.wasCalled('buildOK'))
+        self.assertTrue(self.slave.wasCalled('buildComplete'))

=== modified file 'sbuildrc'
--- sbuildrc	2015-07-05 12:43:54 +0000
+++ sbuildrc	2017-04-26 12:28:05 +0000
@@ -9,6 +9,9 @@
 # launchpad-buildd does this before sbuild.
 $apt_update = 0;
 $apt_distupgrade = 0;
+# XXX cjwatson 2017-04-26: We should drop this (or at least make it
+# conditional so that it only applies in development setups) once the
+# trusted keys logic is robust.
 $apt_allow_unauthenticated = 1;
 
 $resolve_alternatives = 1;