Merge lp:~cjwatson/launchpad/refactor-archive-signing into lp:launchpad

Proposed by Colin Watson on 2018-01-19
Status: Needs review
Proposed branch: lp:~cjwatson/launchpad/refactor-archive-signing
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/archive-signing-run-parts
Diff against target: 583 lines (+274/-30)
11 files modified
lib/lp/archivepublisher/archivesigningkey.py (+6/-4)
lib/lp/archivepublisher/customupload.py (+17/-0)
lib/lp/archivepublisher/debian_installer.py (+5/-1)
lib/lp/archivepublisher/dist_upgrader.py (+6/-2)
lib/lp/archivepublisher/publishing.py (+11/-10)
lib/lp/archivepublisher/signing.py (+5/-5)
lib/lp/archivepublisher/tests/test_customupload.py (+94/-2)
lib/lp/archivepublisher/tests/test_debian_installer.py (+23/-2)
lib/lp/archivepublisher/tests/test_dist_upgrader.py (+23/-2)
lib/lp/archivepublisher/tests/test_publisher.py (+50/-1)
lib/lp/archivepublisher/tests/test_signing.py (+34/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/refactor-archive-signing
Reviewer Review Type Date Requested Status
Launchpad code reviewers 2018-01-19 Pending
Review via email: mp+336377@code.launchpad.net

Commit Message

Extend custom uploads and Release file signing to use the new ISignableArchive interface.

Description of the Change

This replaces ubuntu-archive-publishing/publish-distro.d/10-sign-releases.

To post a comment you must log in.
18541. By Colin Watson on 2018-01-20

Use RunPartsMixin in a couple more places.

Unmerged revisions

18541. By Colin Watson on 2018-01-20

Use RunPartsMixin in a couple more places.

18540. By Colin Watson on 2018-01-19

Extend Release file signing to use the new ISignableArchive interface.

18539. By Colin Watson on 2018-01-19

Extend custom uploads to use the new ISignableArchive interface.

debian-installer checksums, dist-upgrader tarballs, and UEFI/etc. checksums
are now GPG-signed.

18538. By Colin Watson on 2018-01-19

Extend IArchiveSigningKey to support signing files using run-parts.

Generic signing methods are now on ISignableArchive, with methods specific
to managed signing keys remaining on IArchiveSigningKey. This allows using
the same interface to sign files with either managed keys (the PPA case) or
external run-parts scripts (the case of the Ubuntu primary archive).

18537. By Colin Watson on 2018-01-19

Move signing logic out of DirectoryHash.

It makes more sense at a higher layer, and this allows for some cleaner
abstractions in the near future.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archivepublisher/archivesigningkey.py'
2--- lib/lp/archivepublisher/archivesigningkey.py 2018-01-26 13:42:52 +0000
3+++ lib/lp/archivepublisher/archivesigningkey.py 2018-01-26 13:42:53 +0000
4@@ -119,19 +119,21 @@
5 "No signing key available for %s" %
6 self.archive.displayname)
7
8- def signRepository(self, suite, log=None):
9+ def signRepository(self, suite, suffix='', log=None):
10 """See `ISignableArchive`."""
11 suite_path = os.path.join(self._archive_root_path, 'dists', suite)
12- release_file_path = os.path.join(suite_path, 'Release')
13+ release_file_path = os.path.join(suite_path, 'Release' + suffix)
14 if not os.path.exists(release_file_path):
15 raise AssertionError(
16 "Release file doesn't exist in the repository: %s" %
17 release_file_path)
18
19 self._makeSignatures([
20- (release_file_path, os.path.join(suite_path, 'Release.gpg'),
21+ (release_file_path,
22+ os.path.join(suite_path, 'Release.gpg' + suffix),
23 SigningMode.DETACHED, suite),
24- (release_file_path, os.path.join(suite_path, 'InRelease'),
25+ (release_file_path,
26+ os.path.join(suite_path, 'InRelease' + suffix),
27 SigningMode.CLEAR, suite),
28 ], log=log)
29
30
31=== modified file 'lib/lp/archivepublisher/customupload.py'
32--- lib/lp/archivepublisher/customupload.py 2018-01-26 13:42:52 +0000
33+++ lib/lp/archivepublisher/customupload.py 2018-01-26 13:42:53 +0000
34@@ -26,6 +26,7 @@
35 Version as make_version,
36 VersionError,
37 )
38+from lp.archivepublisher.interfaces.archivesigningkey import ISignableArchive
39 from lp.services.librarian.utils import copy_and_close
40 from lp.soyuz.interfaces.queue import (
41 CustomUploadError,
42@@ -268,6 +269,20 @@
43 if not os.path.isdir(parentdir):
44 os.makedirs(parentdir, 0o755)
45
46+ def shouldSign(self, filename):
47+ """Returns True if the given filename should be signed."""
48+ return False
49+
50+ def sign(self, archive, suite, filename):
51+ """Sign a file.
52+
53+ For now, we always write a detached signature to the input file name
54+ plus ".gpg".
55+ """
56+ signable_archive = ISignableArchive(archive)
57+ if signable_archive.can_sign:
58+ signable_archive.signFile(suite, filename, log=self.logger)
59+
60 def installFiles(self, archive, suite):
61 """Install the files from the custom upload to the archive."""
62 assert self.tmpdir is not None, "Must extract tarfile first"
63@@ -316,6 +331,8 @@
64 else:
65 shutil.copy(sourcepath, destpath)
66 os.chmod(destpath, 0o644)
67+ if self.shouldSign(destpath):
68+ self.sign(archive, suite, destpath)
69
70 extracted = True
71
72
73=== modified file 'lib/lp/archivepublisher/debian_installer.py'
74--- lib/lp/archivepublisher/debian_installer.py 2016-06-07 17:07:35 +0000
75+++ lib/lp/archivepublisher/debian_installer.py 2018-01-26 13:42:53 +0000
76@@ -1,4 +1,4 @@
77-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
78+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
79 # GNU Affero General Public License version 3 (see the file LICENSE).
80
81 """The processing of debian installer tarballs."""
82@@ -76,3 +76,7 @@
83
84 def shouldInstall(self, filename):
85 return filename.startswith('%s/' % self.version)
86+
87+ def shouldSign(self, filename):
88+ """Sign checksums files."""
89+ return filename.endswith('SUMS')
90
91=== modified file 'lib/lp/archivepublisher/dist_upgrader.py'
92--- lib/lp/archivepublisher/dist_upgrader.py 2016-06-07 17:07:35 +0000
93+++ lib/lp/archivepublisher/dist_upgrader.py 2018-01-26 13:42:53 +0000
94@@ -1,4 +1,4 @@
95-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
96+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
97 # GNU Affero General Public License version 3 (see the file LICENSE).
98
99 """The processing of dist-upgrader tarballs."""
100@@ -81,7 +81,7 @@
101 return None
102
103 def shouldInstall(self, filename):
104- """ Install files from a dist-upgrader tarball.
105+ """Install files from a dist-upgrader tarball.
106
107 It raises DistUpgraderBadVersion if if finds a directory name that
108 could not be treated as a valid Debian version.
109@@ -100,3 +100,7 @@
110 except BadUpstreamError as exc:
111 raise DistUpgraderBadVersion(self.tarfile_path, exc)
112 return version and not filename.startswith('current')
113+
114+ def shouldSign(self, filename):
115+ """Sign *.tar.gz files."""
116+ return filename.endswith('.tar.gz')
117
118=== modified file 'lib/lp/archivepublisher/publishing.py'
119--- lib/lp/archivepublisher/publishing.py 2018-01-26 13:42:52 +0000
120+++ lib/lp/archivepublisher/publishing.py 2018-01-26 13:42:53 +0000
121@@ -57,9 +57,7 @@
122 build_source_stanza_fields,
123 build_translations_stanza_fields,
124 )
125-from lp.archivepublisher.interfaces.archivesigningkey import (
126- IArchiveSigningKey,
127- )
128+from lp.archivepublisher.interfaces.archivesigningkey import ISignableArchive
129 from lp.archivepublisher.model.ftparchive import FTPArchiveHandler
130 from lp.archivepublisher.utils import (
131 get_ppa_reference,
132@@ -1245,20 +1243,23 @@
133 if distroseries.publish_by_hash:
134 self._updateByHash(suite, "Release.new")
135
136- os.rename(
137- os.path.join(suite_dir, "Release.new"),
138- os.path.join(suite_dir, "Release"))
139-
140- if self.archive.signing_key is not None:
141+ signable_archive = ISignableArchive(self.archive)
142+ if signable_archive.can_sign:
143 # Sign the repository.
144 self.log.debug("Signing Release file for %s" % suite)
145- IArchiveSigningKey(self.archive).signRepository(suite)
146+ signable_archive.signRepository(suite, suffix=".new", log=self.log)
147 core_files.add("Release.gpg")
148 core_files.add("InRelease")
149 else:
150- # Skip signature if the archive signing key is undefined.
151+ # Skip signature if the archive is not set up for signing.
152 self.log.debug("No signing key available, skipping signature.")
153
154+ for name in ("Release", "Release.gpg", "InRelease"):
155+ if name in core_files:
156+ os.rename(
157+ os.path.join(suite_dir, "%s.new" % name),
158+ os.path.join(suite_dir, name))
159+
160 # Make sure all the timestamps match, to make it easier to insert
161 # caching headers on mirrors.
162 self._syncTimestamps(suite, core_files)
163
164=== modified file 'lib/lp/archivepublisher/signing.py'
165--- lib/lp/archivepublisher/signing.py 2018-01-26 13:42:52 +0000
166+++ lib/lp/archivepublisher/signing.py 2018-01-26 13:42:53 +0000
167@@ -28,9 +28,6 @@
168
169 from lp.archivepublisher.config import getPubConfig
170 from lp.archivepublisher.customupload import CustomUpload
171-from lp.archivepublisher.interfaces.archivesigningkey import (
172- IArchiveSigningKey,
173- )
174 from lp.services.osutils import remove_if_exists
175 from lp.soyuz.interfaces.queue import CustomUploadError
176
177@@ -379,12 +376,15 @@
178 with DirectoryHash(versiondir, self.temproot) as hasher:
179 hasher.add_dir(versiondir)
180 for checksum_path in hasher.checksum_paths:
181- if archive.signing_key is not None:
182- IArchiveSigningKey(archive).signFile(suite, checksum_path)
183+ if self.shouldSign(checksum_path):
184+ self.sign(archive, suite, checksum_path)
185
186 def shouldInstall(self, filename):
187 return filename.startswith("%s/" % self.version)
188
189+ def shouldSign(self, filename):
190+ return filename.endswith("SUMS")
191+
192
193 class UefiUpload(SigningUpload):
194 """Legacy UEFI Signing custom upload.
195
196=== modified file 'lib/lp/archivepublisher/tests/test_customupload.py'
197--- lib/lp/archivepublisher/tests/test_customupload.py 2012-05-28 13:13:53 +0000
198+++ lib/lp/archivepublisher/tests/test_customupload.py 2018-01-26 13:42:53 +0000
199@@ -1,4 +1,4 @@
200-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
201+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
202 # GNU Affero General Public License version 3 (see the file LICENSE).
203
204 """Tests for `CustomUploads`."""
205@@ -13,13 +13,40 @@
206 import tempfile
207 import unittest
208
209+from fixtures import MonkeyPatch
210+from testtools.deferredruntest import AsynchronousDeferredRunTest
211+from testtools.matchers import (
212+ Equals,
213+ MatchesDict,
214+ Not,
215+ PathExists,
216+ )
217+from twisted.internet import defer
218+from zope.component import getUtility
219+
220+from lp.archivepublisher.config import getPubConfig
221 from lp.archivepublisher.customupload import (
222 CustomUpload,
223 CustomUploadTarballBadFile,
224 CustomUploadTarballBadSymLink,
225 CustomUploadTarballInvalidFileType,
226 )
227-from lp.testing import TestCase
228+from lp.archivepublisher.interfaces.archivesigningkey import (
229+ IArchiveSigningKey,
230+ )
231+from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
232+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
233+from lp.services.gpg.interfaces import IGPGHandler
234+from lp.services.osutils import write_file
235+from lp.soyuz.enums import ArchivePurpose
236+from lp.testing import (
237+ TestCase,
238+ TestCaseWithFactory,
239+ )
240+from lp.testing.fakemethod import FakeMethod
241+from lp.testing.gpgkeys import gpgkeysdir
242+from lp.testing.keyserver import InProcessKeyServerFixture
243+from lp.testing.layers import LaunchpadZopelessLayer
244
245
246 class TestCustomUpload(unittest.TestCase):
247@@ -198,3 +225,68 @@
248 self.custom_processor.extract)
249 finally:
250 shutil.rmtree(self.tarfile_path)
251+
252+
253+class TestSigning(TestCaseWithFactory, RunPartsMixin):
254+
255+ layer = LaunchpadZopelessLayer
256+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
257+
258+ def setUp(self):
259+ super(TestSigning, self).setUp()
260+ self.temp_dir = self.makeTemporaryDirectory()
261+ self.distro = self.factory.makeDistribution()
262+ db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
263+ self.distro)
264+ db_pubconf.root_dir = unicode(self.temp_dir)
265+ self.archive = self.factory.makeArchive(
266+ distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
267+
268+ def test_sign_without_signing_key(self):
269+ filename = os.path.join(
270+ getPubConfig(self.archive).archiveroot, "file")
271+ self.assertIsNone(self.archive.signing_key)
272+ custom_processor = CustomUpload()
273+ custom_processor.sign(self.archive, "suite", filename)
274+ self.assertThat("%s.gpg" % filename, Not(PathExists()))
275+
276+ @defer.inlineCallbacks
277+ def test_sign_with_signing_key(self):
278+ filename = os.path.join(
279+ getPubConfig(self.archive).archiveroot, "file")
280+ write_file(filename, "contents")
281+ self.assertIsNone(self.archive.signing_key)
282+ self.useFixture(InProcessKeyServerFixture()).start()
283+ key_path = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
284+ yield IArchiveSigningKey(self.archive).setSigningKey(
285+ key_path, async_keyserver=True)
286+ self.assertIsNotNone(self.archive.signing_key)
287+ custom_processor = CustomUpload()
288+ custom_processor.sign(self.archive, "suite", filename)
289+ with open(filename) as cleartext_file:
290+ cleartext = cleartext_file.read()
291+ with open("%s.gpg" % filename) as signature_file:
292+ signature = getUtility(IGPGHandler).getVerifiedSignature(
293+ cleartext, signature_file.read())
294+ self.assertEqual(
295+ self.archive.signing_key.fingerprint, signature.fingerprint)
296+
297+ def test_sign_with_external_run_parts(self):
298+ self.enableRunParts(distribution_name=self.distro.name)
299+ filename = os.path.join(
300+ getPubConfig(self.archive).archiveroot, "file")
301+ write_file(filename, "contents")
302+ self.assertIsNone(self.archive.signing_key)
303+ run_parts_fixture = self.useFixture(MonkeyPatch(
304+ "lp.archivepublisher.archivesigningkey.run_parts", FakeMethod()))
305+ custom_processor = CustomUpload()
306+ custom_processor.sign(self.archive, "suite", filename)
307+ args, kwargs = run_parts_fixture.new_value.calls[0]
308+ self.assertEqual((self.distro.name, "sign.d"), args)
309+ self.assertThat(kwargs["env"], MatchesDict({
310+ "INPUT_PATH": Equals(filename),
311+ "OUTPUT_PATH": Equals("%s.gpg" % filename),
312+ "MODE": Equals("detached"),
313+ "DISTRIBUTION": Equals(self.distro.name),
314+ "SUITE": Equals("suite"),
315+ }))
316
317=== modified file 'lib/lp/archivepublisher/tests/test_debian_installer.py'
318--- lib/lp/archivepublisher/tests/test_debian_installer.py 2016-06-07 17:07:35 +0000
319+++ lib/lp/archivepublisher/tests/test_debian_installer.py 2018-01-26 13:42:53 +0000
320@@ -1,4 +1,4 @@
321-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
322+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
323 # GNU Affero General Public License version 3 (see the file LICENSE).
324
325 """Test debian-installer custom uploads.
326@@ -8,7 +8,9 @@
327 """
328
329 import os
330+from textwrap import dedent
331
332+from testtools.matchers import DirContains
333 from zope.component import getUtility
334
335 from lp.archivepublisher.config import getPubConfig
336@@ -18,13 +20,14 @@
337 )
338 from lp.archivepublisher.debian_installer import DebianInstallerUpload
339 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
340+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
341 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
342 from lp.soyuz.enums import ArchivePurpose
343 from lp.testing import TestCaseWithFactory
344 from lp.testing.layers import ZopelessDatabaseLayer
345
346
347-class TestDebianInstaller(TestCaseWithFactory):
348+class TestDebianInstaller(RunPartsMixin, TestCaseWithFactory):
349
350 layer = ZopelessDatabaseLayer
351
352@@ -161,6 +164,24 @@
353 self.assertEqual(
354 0o755, os.stat(self.getInstallerPath(directory)).st_mode & 0o777)
355
356+ def test_sign_with_external_run_parts(self):
357+ self.enableRunParts(distribution_name=self.distro.name)
358+ with open(os.path.join(
359+ self.parts_directory, self.distro.name, "sign.d",
360+ "10-sign"), "w") as f:
361+ f.write(dedent("""\
362+ #! /bin/sh
363+ touch "$OUTPUT_PATH"
364+ """))
365+ os.fchmod(f.fileno(), 0o755)
366+ self.openArchive()
367+ self.addFile("images/list", "a list")
368+ self.addFile("images/SHA256SUMS", "a checksum")
369+ self.process()
370+ self.assertThat(
371+ self.getInstallerPath("images"),
372+ DirContains(["list", "SHA256SUMS", "SHA256SUMS.gpg"]))
373+
374 def test_getSeriesKey_extracts_architecture(self):
375 # getSeriesKey extracts the architecture from an upload's filename.
376 self.openArchive()
377
378=== modified file 'lib/lp/archivepublisher/tests/test_dist_upgrader.py'
379--- lib/lp/archivepublisher/tests/test_dist_upgrader.py 2016-06-07 17:07:35 +0000
380+++ lib/lp/archivepublisher/tests/test_dist_upgrader.py 2018-01-26 13:42:53 +0000
381@@ -1,4 +1,4 @@
382-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
383+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
384 # GNU Affero General Public License version 3 (see the file LICENSE).
385
386 """Test dist-upgrader custom uploads.
387@@ -8,7 +8,9 @@
388 """
389
390 import os
391+from textwrap import dedent
392
393+from testtools.matchers import DirContains
394 from zope.component import getUtility
395
396 from lp.archivepublisher.config import getPubConfig
397@@ -21,6 +23,7 @@
398 DistUpgraderUpload,
399 )
400 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
401+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
402 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
403 from lp.soyuz.enums import ArchivePurpose
404 from lp.testing import TestCaseWithFactory
405@@ -33,7 +36,7 @@
406 self.archiveroot = archiveroot
407
408
409-class TestDistUpgrader(TestCaseWithFactory):
410+class TestDistUpgrader(RunPartsMixin, TestCaseWithFactory):
411
412 layer = ZopelessDatabaseLayer
413
414@@ -109,6 +112,24 @@
415 self.tarfile.add_file("foobar/foobar/dapper.tar.gz", "")
416 self.assertRaises(DistUpgraderBadVersion, self.process)
417
418+ def test_sign_with_external_run_parts(self):
419+ self.enableRunParts(distribution_name=self.distro.name)
420+ with open(os.path.join(
421+ self.parts_directory, self.distro.name, "sign.d",
422+ "10-sign"), "w") as f:
423+ f.write(dedent("""\
424+ #! /bin/sh
425+ touch "$OUTPUT_PATH"
426+ """))
427+ os.fchmod(f.fileno(), 0o755)
428+ self.openArchive("20060302.0120")
429+ self.tarfile.add_file("20060302.0120/list", "a list")
430+ self.tarfile.add_file("20060302.0120/foo.tar.gz", "a tarball")
431+ self.process()
432+ self.assertThat(
433+ os.path.join(self.getUpgraderPath(), "20060302.0120"),
434+ DirContains(["list", "foo.tar.gz", "foo.tar.gz.gpg"]))
435+
436 def test_getSeriesKey_extracts_architecture(self):
437 # getSeriesKey extracts the architecture from an upload's filename.
438 self.openArchive("20060302.0120")
439
440=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
441--- lib/lp/archivepublisher/tests/test_publisher.py 2018-01-26 13:42:52 +0000
442+++ lib/lp/archivepublisher/tests/test_publisher.py 2018-01-26 13:42:53 +0000
443@@ -67,6 +67,7 @@
444 I18nIndex,
445 Publisher,
446 )
447+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
448 from lp.archivepublisher.utils import RepositoryIndexFile
449 from lp.registry.interfaces.distribution import IDistributionSet
450 from lp.registry.interfaces.distroseries import IDistroSeries
451@@ -2925,7 +2926,7 @@
452 os.rename(temporary_dists, original_dists)
453
454
455-class TestPublisherRepositorySignatures(TestPublisherBase):
456+class TestPublisherRepositorySignatures(RunPartsMixin, TestPublisherBase):
457 """Testing `Publisher` signature behaviour."""
458
459 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
460@@ -3065,6 +3066,54 @@
461 self.assertThat(
462 sync_args[1], ContainsAll(['Release', 'Release.gpg', 'InRelease']))
463
464+ def testRepositorySignatureWithExternalRunParts(self):
465+ """Check publisher behaviour when signing repositories.
466+
467+ When a 'sign.d' run-parts directory is configured for the archive,
468+ it is used to sign the Release file.
469+ """
470+ cprov = getUtility(IPersonSet).getByName('cprov')
471+ self.assertIsNone(cprov.archive.signing_key)
472+ self.enableRunParts(distribution_name=cprov.archive.distribution.name)
473+ sign_directory = os.path.join(
474+ self.parts_directory, cprov.archive.distribution.name, 'sign.d')
475+ with open(os.path.join(sign_directory, '10-sign'), 'w') as sign_script:
476+ sign_script.write(dedent("""\
477+ #! /bin/sh
478+ echo "$MODE signature of $INPUT_PATH ($DISTRIBUTION/$SUITE)" \\
479+ >"$OUTPUT_PATH"
480+ """))
481+ os.fchmod(sign_script.fileno(), 0o755)
482+
483+ self.setupPublisher(cprov.archive)
484+ self.archive_publisher._syncTimestamps = FakeMethod()
485+
486+ self._publishArchive(cprov.archive)
487+
488+ # Release exists.
489+ self.assertThat(self.release_file_path, PathExists())
490+
491+ # Release.gpg and InRelease exist with suitable fake signatures.
492+ # Note that the signatures are made before Release.new is renamed to
493+ # to Release.
494+ self.assertThat(
495+ self.release_file_signature_path,
496+ FileContains(
497+ "detached signature of %s.new (%s/breezy-autotest)\n" %
498+ (self.release_file_path, cprov.archive.distribution.name)))
499+ self.assertThat(
500+ self.inline_release_file_path,
501+ FileContains(
502+ "clear signature of %s.new (%s/breezy-autotest)\n" %
503+ (self.release_file_path, cprov.archive.distribution.name)))
504+
505+ # The publisher synchronises the various Release file timestamps.
506+ self.assertEqual(1, self.archive_publisher._syncTimestamps.call_count)
507+ sync_args = self.archive_publisher._syncTimestamps.extract_args()[0]
508+ self.assertEqual(self.distroseries.name, sync_args[0])
509+ self.assertThat(
510+ sync_args[1], ContainsAll(['Release', 'Release.gpg', 'InRelease']))
511+
512
513 class TestPublisherLite(TestCaseWithFactory):
514 """Lightweight unit tests for the publisher."""
515
516=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
517--- lib/lp/archivepublisher/tests/test_signing.py 2018-01-26 13:42:52 +0000
518+++ lib/lp/archivepublisher/tests/test_signing.py 2018-01-26 13:42:53 +0000
519@@ -13,9 +13,11 @@
520 from testtools.deferredruntest import AsynchronousDeferredRunTest
521 from testtools.matchers import (
522 Contains,
523+ Equals,
524 FileContains,
525 Matcher,
526 MatchesAll,
527+ MatchesDict,
528 Mismatch,
529 Not,
530 StartsWith,
531@@ -36,6 +38,7 @@
532 SigningUpload,
533 UefiUpload,
534 )
535+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
536 from lp.services.osutils import write_file
537 from lp.services.tarfile_helpers import LaunchpadWriteTarFile
538 from lp.soyuz.enums import ArchivePurpose
539@@ -215,7 +218,7 @@
540 return os.path.join(pubconf.archiveroot, "dists", self.suite, "main")
541
542
543-class TestSigning(TestSigningHelpers):
544+class TestSigning(RunPartsMixin, TestSigningHelpers):
545
546 def getSignedPath(self, loader_type, arch):
547 return os.path.join(self.getDistsPath(), "signed",
548@@ -934,6 +937,36 @@
549 "1.0/signed.tar.gz",
550 ]]))
551
552+ def test_checksumming_tree_signed_with_external_run_parts(self):
553+ # Checksum files can be signed using an external run-parts helper.
554+ # We disable subprocess.call because there's just too much going on,
555+ # so we can't test this completely, but we can at least test that
556+ # run_parts is called.
557+ self.enableRunParts(distribution_name=self.distro.name)
558+ run_parts_fixture = self.useFixture(MonkeyPatch(
559+ "lp.archivepublisher.archivesigningkey.run_parts", FakeMethod()))
560+ self.setUpUefiKeys()
561+ self.setUpKmodKeys()
562+ self.setUpOpalKeys()
563+ self.openArchive("test", "1.0", "amd64")
564+ self.tarfile.add_file("1.0/empty.efi", "")
565+ self.tarfile.add_file("1.0/empty.ko", "")
566+ self.tarfile.add_file("1.0/empty.opal", "")
567+ self.process_emulate()
568+ sha256file = os.path.join(self.getSignedPath("test", "amd64"),
569+ "1.0", "SHA256SUMS")
570+ self.assertTrue(os.path.exists(sha256file))
571+ self.assertEqual(1, run_parts_fixture.new_value.call_count)
572+ args, kwargs = run_parts_fixture.new_value.calls[-1]
573+ self.assertEqual((self.distro.name, "sign.d"), args)
574+ self.assertThat(kwargs["env"], MatchesDict({
575+ "INPUT_PATH": Equals(sha256file),
576+ "OUTPUT_PATH": Equals("%s.gpg" % sha256file),
577+ "MODE": Equals("detached"),
578+ "DISTRIBUTION": Equals(self.distro.name),
579+ "SUITE": Equals(self.suite),
580+ }))
581+
582
583 class TestUefi(TestSigningHelpers):
584