Merge lp:~cjwatson/launchpad/archive-signing-run-parts into lp:launchpad

Proposed by Colin Watson on 2018-01-19
Status: Merged
Merged at revision: 18575
Proposed branch: lp:~cjwatson/launchpad/archive-signing-run-parts
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/refactor-run-parts-subprocess
Diff against target: 787 lines (+289/-200)
10 files modified
lib/lp/archivepublisher/archivesigningkey.py (+123/-69)
lib/lp/archivepublisher/customupload.py (+3/-3)
lib/lp/archivepublisher/interfaces/archivesigningkey.py (+47/-23)
lib/lp/archivepublisher/publishing.py (+7/-5)
lib/lp/archivepublisher/signing.py (+7/-7)
lib/lp/archivepublisher/tests/archive-signing.txt (+1/-1)
lib/lp/archivepublisher/tests/test_archivesigningkey.py (+85/-14)
lib/lp/archivepublisher/tests/test_publisher.py (+1/-72)
lib/lp/archivepublisher/tests/test_run_parts.py (+5/-4)
lib/lp/archivepublisher/tests/test_signing.py (+10/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/archive-signing-run-parts
Reviewer Review Type Date Requested Status
William Grant code 2018-01-19 Approve on 2018-03-20
Review via email: mp+336373@code.launchpad.net

Commit message

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).

Description of the change

To keep this branch a manageable size, I haven't yet converted the call sites to take advantage of this; that will come in the next branch in this series.

https://code.launchpad.net/~cjwatson/ubuntu-archive-publishing/sign-parts/+merge/336347 will need to be deployed before this.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)
18539. By Colin Watson on 2018-03-21

Merge devel.

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 2017-04-29 15:24:32 +0000
3+++ lib/lp/archivepublisher/archivesigningkey.py 2018-03-21 11:54:11 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """ArchiveSigningKey implementation."""
10@@ -7,12 +7,18 @@
11
12 __all__ = [
13 'ArchiveSigningKey',
14+ 'SignableArchive',
15+ 'SigningMode',
16 ]
17
18
19 import os
20
21 import gpgme
22+from lazr.enum import (
23+ EnumeratedType,
24+ Item,
25+ )
26 from twisted.internet.threads import deferToThread
27 from zope.component import getUtility
28 from zope.interface import implementer
29@@ -24,7 +30,13 @@
30 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
31 from lp.archivepublisher.config import getPubConfig
32 from lp.archivepublisher.interfaces.archivesigningkey import (
33+ CannotSignArchive,
34 IArchiveSigningKey,
35+ ISignableArchive,
36+ )
37+from lp.archivepublisher.run_parts import (
38+ find_run_parts_dir,
39+ run_parts,
40 )
41 from lp.registry.interfaces.gpg import IGPGKeySet
42 from lp.services.config import config
43@@ -32,13 +44,119 @@
44 from lp.services.propertycache import get_property_cache
45
46
47+class SigningMode(EnumeratedType):
48+ """Archive file signing mode."""
49+
50+ DETACHED = Item("Detached signature")
51+ CLEAR = Item("Cleartext signature")
52+
53+
54+@implementer(ISignableArchive)
55+class SignableArchive:
56+ """`IArchive` adapter for operations that involve signing files."""
57+
58+ gpgme_modes = {
59+ SigningMode.DETACHED: gpgme.SIG_MODE_DETACH,
60+ SigningMode.CLEAR: gpgme.SIG_MODE_CLEAR,
61+ }
62+
63+ def __init__(self, archive):
64+ self.archive = archive
65+
66+ @property
67+ def can_sign(self):
68+ """See `ISignableArchive`."""
69+ return (
70+ self.archive.signing_key is not None or
71+ find_run_parts_dir(
72+ self.archive.distribution.name, "sign.d") is not None)
73+
74+ def _makeSignatures(self, signatures, log=None):
75+ """Make a sequence of signatures.
76+
77+ This abstraction is useful in the case where we're using an
78+ in-process `GPGHandler`, since it avoids having to import the secret
79+ key more than once.
80+
81+ :param signatures: A sequence of (input path, output path,
82+ `SigningMode`, suite) tuples.
83+ :param log: An optional logger.
84+ """
85+ if not self.can_sign:
86+ raise CannotSignArchive(
87+ "No signing key available for %s" % self.archive.displayname)
88+
89+ if self.archive.signing_key is not None:
90+ secret_key_path = self.getPathForSecretKey(
91+ self.archive.signing_key)
92+ with open(secret_key_path) as secret_key_file:
93+ secret_key_export = secret_key_file.read()
94+ gpghandler = getUtility(IGPGHandler)
95+ secret_key = gpghandler.importSecretKey(secret_key_export)
96+
97+ for input_path, output_path, mode, suite in signatures:
98+ if self.archive.signing_key is not None:
99+ with open(input_path) as input_file:
100+ input_content = input_file.read()
101+ signature = gpghandler.signContent(
102+ input_content, secret_key, mode=self.gpgme_modes[mode])
103+ with open(output_path, "w") as output_file:
104+ output_file.write(signature)
105+ elif find_run_parts_dir(
106+ self.archive.distribution.name, "sign.d") is not None:
107+ env = {
108+ "INPUT_PATH": input_path,
109+ "OUTPUT_PATH": output_path,
110+ "MODE": mode.name.lower(),
111+ "DISTRIBUTION": self.archive.distribution.name,
112+ "SUITE": suite,
113+ }
114+ run_parts(
115+ self.archive.distribution.name, "sign.d",
116+ log=log, env=env)
117+ else:
118+ raise AssertionError(
119+ "No signing key available for %s" %
120+ self.archive.displayname)
121+
122+ def signRepository(self, suite, log=None):
123+ """See `ISignableArchive`."""
124+ suite_path = os.path.join(self._archive_root_path, 'dists', suite)
125+ release_file_path = os.path.join(suite_path, 'Release')
126+ if not os.path.exists(release_file_path):
127+ raise AssertionError(
128+ "Release file doesn't exist in the repository: %s" %
129+ release_file_path)
130+
131+ self._makeSignatures([
132+ (release_file_path, os.path.join(suite_path, 'Release.gpg'),
133+ SigningMode.DETACHED, suite),
134+ (release_file_path, os.path.join(suite_path, 'InRelease'),
135+ SigningMode.CLEAR, suite),
136+ ], log=log)
137+
138+ def signFile(self, suite, path, log=None):
139+ """See `ISignableArchive`."""
140+ # Allow the passed path to be relative to the archive root.
141+ path = os.path.realpath(os.path.join(self._archive_root_path, path))
142+
143+ # Ensure the resulting path is within the archive root after
144+ # normalisation.
145+ # NOTE: uses os.sep to prevent /var/tmp/../tmpFOO attacks.
146+ archive_root = self._archive_root_path + os.sep
147+ if not path.startswith(archive_root):
148+ raise AssertionError(
149+ "Attempting to sign file (%s) outside archive_root for %s" % (
150+ path, self.archive.displayname))
151+
152+ self._makeSignatures(
153+ [(path, "%s.gpg" % path, SigningMode.DETACHED, suite)], log=log)
154+
155+
156 @implementer(IArchiveSigningKey)
157-class ArchiveSigningKey:
158+class ArchiveSigningKey(SignableArchive):
159 """`IArchive` adapter for manipulating its GPG key."""
160
161- def __init__(self, archive):
162- self.archive = archive
163-
164 @property
165 def _archive_root_path(self):
166 return getPubConfig(self.archive).archiveroot
167@@ -139,67 +257,3 @@
168 else:
169 pub_key = self._uploadPublicSigningKey(secret_key)
170 self._storeSigningKey(pub_key)
171-
172- def signRepository(self, suite):
173- """See `IArchiveSigningKey`."""
174- assert self.archive.signing_key is not None, (
175- "No signing key available for %s" % self.archive.displayname)
176-
177- suite_path = os.path.join(self._archive_root_path, 'dists', suite)
178- release_file_path = os.path.join(suite_path, 'Release')
179- assert os.path.exists(release_file_path), (
180- "Release file doesn't exist in the repository: %s"
181- % release_file_path)
182-
183- secret_key_path = self.getPathForSecretKey(self.archive.signing_key)
184- with open(secret_key_path) as secret_key_file:
185- secret_key_export = secret_key_file.read()
186-
187- gpghandler = getUtility(IGPGHandler)
188- secret_key = gpghandler.importSecretKey(secret_key_export)
189-
190- with open(release_file_path) as release_file:
191- release_file_content = release_file.read()
192- signature = gpghandler.signContent(
193- release_file_content, secret_key, mode=gpgme.SIG_MODE_DETACH)
194-
195- release_signature_path = os.path.join(suite_path, 'Release.gpg')
196- with open(release_signature_path, 'w') as release_signature_file:
197- release_signature_file.write(signature)
198-
199- inline_release = gpghandler.signContent(
200- release_file_content, secret_key, mode=gpgme.SIG_MODE_CLEAR)
201-
202- inline_release_path = os.path.join(suite_path, 'InRelease')
203- with open(inline_release_path, 'w') as inline_release_file:
204- inline_release_file.write(inline_release)
205-
206- def signFile(self, path):
207- """See `IArchiveSigningKey`."""
208- assert self.archive.signing_key is not None, (
209- "No signing key available for %s" % self.archive.displayname)
210-
211- # Allow the passed path to be relative to the archive root.
212- path = os.path.realpath(os.path.join(self._archive_root_path, path))
213-
214- # Ensure the resulting path is within the archive root after
215- # normalisation.
216- # NOTE: uses os.sep to prevent /var/tmp/../tmpFOO attacks.
217- archive_root = self._archive_root_path + os.sep
218- assert path.startswith(archive_root), (
219- "Attempting to sign file (%s) outside archive_root for %s" % (
220- path, self.archive.displayname))
221-
222- secret_key_path = self.getPathForSecretKey(self.archive.signing_key)
223- with open(secret_key_path) as secret_key_file:
224- secret_key_export = secret_key_file.read()
225- gpghandler = getUtility(IGPGHandler)
226- secret_key = gpghandler.importSecretKey(secret_key_export)
227-
228- with open(path) as path_file:
229- file_content = path_file.read()
230- signature = gpghandler.signContent(
231- file_content, secret_key, mode=gpgme.SIG_MODE_DETACH)
232-
233- with open(os.path.join(path + '.gpg'), 'w') as signature_file:
234- signature_file.write(signature)
235
236=== modified file 'lib/lp/archivepublisher/customupload.py'
237--- lib/lp/archivepublisher/customupload.py 2016-06-14 15:02:46 +0000
238+++ lib/lp/archivepublisher/customupload.py 2018-03-21 11:54:11 +0000
239@@ -1,4 +1,4 @@
240-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
241+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
242 # GNU Affero General Public License version 3 (see the file LICENSE).
243
244 """Infrastructure for handling custom uploads.
245@@ -128,7 +128,7 @@
246 self.setTargetDirectory(archive, tarfile_path, suite)
247 self.checkForConflicts()
248 self.extract()
249- self.installFiles()
250+ self.installFiles(archive, suite)
251 self.fixCurrentSymlink()
252 finally:
253 self.cleanup()
254@@ -268,7 +268,7 @@
255 if not os.path.isdir(parentdir):
256 os.makedirs(parentdir, 0o755)
257
258- def installFiles(self):
259+ def installFiles(self, archive, suite):
260 """Install the files from the custom upload to the archive."""
261 assert self.tmpdir is not None, "Must extract tarfile first"
262 extracted = False
263
264=== modified file 'lib/lp/archivepublisher/interfaces/archivesigningkey.py'
265--- lib/lp/archivepublisher/interfaces/archivesigningkey.py 2017-04-29 15:24:32 +0000
266+++ lib/lp/archivepublisher/interfaces/archivesigningkey.py 2018-03-21 11:54:11 +0000
267@@ -1,4 +1,4 @@
268-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
269+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
270 # GNU Affero General Public License version 3 (see the file LICENSE).
271
272 """ArchiveSigningKey interface."""
273@@ -6,17 +6,61 @@
274 __metaclass__ = type
275
276 __all__ = [
277+ 'CannotSignArchive',
278 'IArchiveSigningKey',
279+ 'ISignableArchive',
280 ]
281
282-from zope.interface import Interface
283+from zope.interface import (
284+ Attribute,
285+ Interface,
286+ )
287 from zope.schema import Object
288
289 from lp import _
290 from lp.soyuz.interfaces.archive import IArchive
291
292
293-class IArchiveSigningKey(Interface):
294+class CannotSignArchive(Exception):
295+ """An archive is not set up for signing."""
296+
297+
298+class ISignableArchive(Interface):
299+ """`SignableArchive` interface.
300+
301+ `IArchive` adapter for operations that involve signing files.
302+ """
303+
304+ archive = Object(
305+ title=_('Corresponding IArchive'), required=True, schema=IArchive)
306+
307+ can_sign = Attribute("True if this archive is set up for signing.")
308+
309+ def signRepository(suite, log=None):
310+ """Sign the corresponding repository.
311+
312+ :param suite: suite name to be signed.
313+ :param log: an optional logger.
314+ :raises CannotSignArchive: if the context archive is not set up for
315+ signing.
316+ :raises AssertionError: if there is no Release file in the given
317+ suite.
318+ """
319+
320+ def signFile(suite, path, log=None):
321+ """Sign the corresponding file.
322+
323+ :param suite: name of the suite containing the file to be signed.
324+ :param path: path within dists to sign with the archive key.
325+ :param log: an optional logger.
326+ :raises CannotSignArchive: if the context archive is not set up for
327+ signing.
328+ :raises AssertionError: if the given 'path' is outside of the
329+ archive root.
330+ """
331+
332+
333+class IArchiveSigningKey(ISignableArchive):
334 """`ArchiveSigningKey` interface.
335
336 `IArchive` adapter for operations using its 'signing_key'.
337@@ -25,9 +69,6 @@
338 new signing keys.
339 """
340
341- archive = Object(
342- title=_('Corresponding IArchive'), required=True, schema=IArchive)
343-
344 def getPathForSecretKey(key):
345 """Return the absolute path to access a secret key export.
346
347@@ -80,20 +121,3 @@
348 `signing_key`.
349 :raises AssertionError: if the given 'key_path' does not exist.
350 """
351-
352- def signRepository(suite):
353- """Sign the corresponding repository.
354-
355- :param suite: suite name to be signed.
356- :raises AssertionError: if the context archive has no `signing_key`
357- or there is no Release file in the given suite.
358- """
359-
360- def signFile(path):
361- """Sign the corresponding file.
362-
363- :param path: path within dists to sign with the archive key.
364- :raises AssertionError: if the context archive has no `signing_key`.
365- :raises AssertionError: if the given 'path' is outside of the
366- archive root.
367- """
368
369=== modified file 'lib/lp/archivepublisher/publishing.py'
370--- lib/lp/archivepublisher/publishing.py 2016-06-17 21:17:58 +0000
371+++ lib/lp/archivepublisher/publishing.py 2018-03-21 11:54:11 +0000
372@@ -1,4 +1,4 @@
373-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
374+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
375 # GNU Affero General Public License version 3 (see the file LICENSE).
376
377 __all__ = [
378@@ -1478,10 +1478,9 @@
379 class DirectoryHash:
380 """Represents a directory hierarchy for hashing."""
381
382- def __init__(self, root, tmpdir, signer=None):
383+ def __init__(self, root, tmpdir):
384 self.root = root
385 self.tmpdir = tmpdir
386- self.signer = signer
387 self.checksum_hash = []
388
389 for usable in self._usable_archive_hashes:
390@@ -1501,6 +1500,11 @@
391 if archive_hash.write_directory_hash:
392 yield archive_hash
393
394+ @property
395+ def checksum_paths(self):
396+ for checksum_path, _, _ in self.checksum_hash:
397+ yield checksum_path
398+
399 def add(self, path):
400 """Add a path to be checksummed."""
401 hashes = [
402@@ -1524,5 +1528,3 @@
403 def close(self):
404 for (checksum_path, checksum_file, archive_hash) in self.checksum_hash:
405 checksum_file.close()
406- if self.signer:
407- self.signer.signFile(checksum_path)
408
409=== modified file 'lib/lp/archivepublisher/signing.py'
410--- lib/lp/archivepublisher/signing.py 2017-07-24 16:48:47 +0000
411+++ lib/lp/archivepublisher/signing.py 2018-03-21 11:54:11 +0000
412@@ -1,4 +1,4 @@
413-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
414+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
415 # GNU Affero General Public License version 3 (see the file LICENSE).
416
417 """The processing of Signing tarballs.
418@@ -368,19 +368,19 @@
419 if 'tarball' in self.signing_options:
420 self.convertToTarball()
421
422- def installFiles(self):
423+ def installFiles(self, archive, suite):
424 """After installation hash and sign the installed result."""
425 # Avoid circular import.
426 from lp.archivepublisher.publishing import DirectoryHash
427
428- super(SigningUpload, self).installFiles()
429+ super(SigningUpload, self).installFiles(archive, suite)
430
431 versiondir = os.path.join(self.targetdir, self.version)
432- signer = None
433- if self.archive.signing_key:
434- signer = IArchiveSigningKey(self.archive)
435- with DirectoryHash(versiondir, self.temproot, signer) as hasher:
436+ with DirectoryHash(versiondir, self.temproot) as hasher:
437 hasher.add_dir(versiondir)
438+ for checksum_path in hasher.checksum_paths:
439+ if archive.signing_key is not None:
440+ IArchiveSigningKey(archive).signFile(suite, checksum_path)
441
442 def shouldInstall(self, filename):
443 return filename.startswith("%s/" % self.version)
444
445=== modified file 'lib/lp/archivepublisher/tests/archive-signing.txt'
446--- lib/lp/archivepublisher/tests/archive-signing.txt 2017-08-03 14:26:40 +0000
447+++ lib/lp/archivepublisher/tests/archive-signing.txt 2018-03-21 11:54:11 +0000
448@@ -453,7 +453,7 @@
449 >>> archive_signing_key.signRepository(test_suite)
450 Traceback (most recent call last):
451 ...
452- AssertionError: No signing key available for PPA for Celso Providelo
453+ CannotSignArchive: No signing key available for PPA for Celso Providelo
454
455 We'll purge 'signing_keys_root' and the PPA repository root so that
456 other tests don't choke on it, and shut down the server.
457
458=== modified file 'lib/lp/archivepublisher/tests/test_archivesigningkey.py'
459--- lib/lp/archivepublisher/tests/test_archivesigningkey.py 2018-03-02 18:35:42 +0000
460+++ lib/lp/archivepublisher/tests/test_archivesigningkey.py 2018-03-21 11:54:11 +0000
461@@ -8,7 +8,9 @@
462 __metaclass__ = type
463
464 import os
465+from textwrap import dedent
466
467+from testtools.matchers import FileContains
468 from testtools.twistedsupport import AsynchronousDeferredRunTest
469 from twisted.internet import defer
470 from zope.component import getUtility
471@@ -16,8 +18,10 @@
472 from lp.archivepublisher.config import getPubConfig
473 from lp.archivepublisher.interfaces.archivesigningkey import (
474 IArchiveSigningKey,
475+ ISignableArchive,
476 )
477 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
478+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
479 from lp.services.osutils import write_file
480 from lp.soyuz.enums import ArchivePurpose
481 from lp.testing import TestCaseWithFactory
482@@ -26,14 +30,14 @@
483 from lp.testing.layers import ZopelessDatabaseLayer
484
485
486-class TestArchiveSigningKey(TestCaseWithFactory):
487+class TestSignableArchiveWithSigningKey(TestCaseWithFactory):
488
489 layer = ZopelessDatabaseLayer
490 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
491
492 @defer.inlineCallbacks
493 def setUp(self):
494- super(TestArchiveSigningKey, self).setUp()
495+ super(TestSignableArchiveWithSigningKey, self).setUp()
496 self.temp_dir = self.makeTemporaryDirectory()
497 self.distro = self.factory.makeDistribution()
498 db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
499@@ -50,39 +54,106 @@
500 yield IArchiveSigningKey(self.archive).setSigningKey(
501 key_path, async_keyserver=True)
502
503- def test_signfile_absolute_within_archive(self):
504+ def test_signFile_absolute_within_archive(self):
505 filename = os.path.join(self.archive_root, "signme")
506 write_file(filename, "sign this")
507
508- signer = IArchiveSigningKey(self.archive)
509- signer.signFile(filename)
510+ signer = ISignableArchive(self.archive)
511+ self.assertTrue(signer.can_sign)
512+ signer.signFile(self.suite, filename)
513
514 signature = filename + '.gpg'
515 self.assertTrue(os.path.exists(signature))
516
517- def test_signfile_absolute_outside_archive(self):
518+ def test_signFile_absolute_outside_archive(self):
519 filename = os.path.join(self.temp_dir, "signme")
520 write_file(filename, "sign this")
521
522- signer = IArchiveSigningKey(self.archive)
523- self.assertRaises(AssertionError, lambda: signer.signFile(filename))
524+ signer = ISignableArchive(self.archive)
525+ self.assertTrue(signer.can_sign)
526+ self.assertRaises(
527+ AssertionError, lambda: signer.signFile(self.suite, filename))
528
529- def test_signfile_relative_within_archive(self):
530+ def test_signFile_relative_within_archive(self):
531 filename_relative = "signme"
532 filename = os.path.join(self.archive_root, filename_relative)
533 write_file(filename, "sign this")
534
535- signer = IArchiveSigningKey(self.archive)
536- signer.signFile(filename_relative)
537+ signer = ISignableArchive(self.archive)
538+ self.assertTrue(signer.can_sign)
539+ signer.signFile(self.suite, filename_relative)
540
541 signature = filename + '.gpg'
542 self.assertTrue(os.path.exists(signature))
543
544- def test_signfile_relative_outside_archive(self):
545+ def test_signFile_relative_outside_archive(self):
546 filename_relative = "../signme"
547 filename = os.path.join(self.temp_dir, filename_relative)
548 write_file(filename, "sign this")
549
550- signer = IArchiveSigningKey(self.archive)
551+ signer = ISignableArchive(self.archive)
552+ self.assertTrue(signer.can_sign)
553 self.assertRaises(
554- AssertionError, lambda: signer.signFile(filename_relative))
555+ AssertionError,
556+ lambda: signer.signFile(self.suite, filename_relative))
557+
558+
559+class TestSignableArchiveWithRunParts(RunPartsMixin, TestCaseWithFactory):
560+
561+ layer = ZopelessDatabaseLayer
562+
563+ def setUp(self):
564+ super(TestSignableArchiveWithRunParts, self).setUp()
565+ self.temp_dir = self.makeTemporaryDirectory()
566+ self.distro = self.factory.makeDistribution()
567+ db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
568+ self.distro)
569+ db_pubconf.root_dir = unicode(self.temp_dir)
570+ self.archive = self.factory.makeArchive(
571+ distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
572+ self.archive_root = getPubConfig(self.archive).archiveroot
573+ self.suite = "distroseries"
574+ self.enableRunParts(distribution_name=self.distro.name)
575+ with open(os.path.join(
576+ self.parts_directory, self.distro.name, "sign.d",
577+ "10-sign"), "w") as sign_script:
578+ sign_script.write(dedent("""\
579+ #! /bin/sh
580+ echo "$MODE signature of $INPUT_PATH ($DISTRIBUTION/$SUITE)" \\
581+ >"$OUTPUT_PATH"
582+ """))
583+ os.fchmod(sign_script.fileno(), 0o755)
584+
585+ def test_signRepository_runs_parts(self):
586+ suite_dir = os.path.join(self.archive_root, "dists", self.suite)
587+ release_path = os.path.join(suite_dir, "Release")
588+ write_file(release_path, "Release contents")
589+
590+ signer = ISignableArchive(self.archive)
591+ self.assertTrue(signer.can_sign)
592+ signer.signRepository(self.suite)
593+
594+ self.assertThat(
595+ os.path.join(suite_dir, "Release.gpg"),
596+ FileContains(
597+ "detached signature of %s (%s/%s)\n" %
598+ (release_path, self.distro.name, self.suite)))
599+ self.assertThat(
600+ os.path.join(suite_dir, "InRelease"),
601+ FileContains(
602+ "clear signature of %s (%s/%s)\n" %
603+ (release_path, self.distro.name, self.suite)))
604+
605+ def test_signFile_runs_parts(self):
606+ filename = os.path.join(self.archive_root, "signme")
607+ write_file(filename, "sign this")
608+
609+ signer = ISignableArchive(self.archive)
610+ self.assertTrue(signer.can_sign)
611+ signer.signFile(self.suite, filename)
612+
613+ self.assertThat(
614+ "%s.gpg" % filename,
615+ FileContains(
616+ "detached signature of %s (%s/%s)\n" %
617+ (filename, self.distro.name, self.suite)))
618
619=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
620--- lib/lp/archivepublisher/tests/test_publisher.py 2018-03-02 18:35:42 +0000
621+++ lib/lp/archivepublisher/tests/test_publisher.py 2018-03-21 11:54:11 +0000
622@@ -70,7 +70,6 @@
623 Publisher,
624 )
625 from lp.archivepublisher.utils import RepositoryIndexFile
626-from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
627 from lp.registry.interfaces.distribution import IDistributionSet
628 from lp.registry.interfaces.distroseries import IDistroSeries
629 from lp.registry.interfaces.person import IPersonSet
630@@ -3210,16 +3209,6 @@
631 result[dh_file].append(line.strip().split(' '))
632 return result
633
634- def fetchSigs(self, rootdir):
635- result = defaultdict(list)
636- for dh_file in self.all_hash_files:
637- checksum_sig = os.path.join(rootdir, dh_file) + '.gpg'
638- if os.path.exists(checksum_sig):
639- with open(checksum_sig, "r") as sfd:
640- for line in sfd:
641- result[dh_file].append(line)
642- return result
643-
644
645 class TestDirectoryHash(TestDirectoryHashHelpers):
646 """Unit tests for DirectoryHash object."""
647@@ -3234,7 +3223,7 @@
648 checksum_file = os.path.join(rootdir, dh_file)
649 self.assertFalse(os.path.exists(checksum_file))
650
651- with DirectoryHash(rootdir, tmpdir, None):
652+ with DirectoryHash(rootdir, tmpdir):
653 pass
654
655 for dh_file in self.all_hash_files:
656@@ -3297,63 +3286,3 @@
657 ),
658 }
659 self.assertThat(self.fetchSums(rootdir), MatchesDict(expected))
660-
661-
662-class TestDirectoryHashSigning(TestDirectoryHashHelpers):
663- """Unit tests for DirectoryHash object, signing functionality."""
664-
665- layer = ZopelessDatabaseLayer
666- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
667-
668- @defer.inlineCallbacks
669- def setUp(self):
670- super(TestDirectoryHashSigning, self).setUp()
671- self.temp_dir = self.makeTemporaryDirectory()
672- self.distro = self.factory.makeDistribution()
673- db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
674- self.distro)
675- db_pubconf.root_dir = unicode(self.temp_dir)
676- self.archive = self.factory.makeArchive(
677- distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
678- self.archive_root = getPubConfig(self.archive).archiveroot
679- self.suite = "distroseries"
680-
681- # Setup a keyserver so we can install the archive key.
682- with InProcessKeyServerFixture() as keyserver:
683- yield keyserver.start()
684- key_path = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
685- yield IArchiveSigningKey(self.archive).setSigningKey(
686- key_path, async_keyserver=True)
687-
688- def test_basic_directory_add_signed(self):
689- tmpdir = unicode(self.makeTemporaryDirectory())
690- rootdir = self.archive_root
691- os.makedirs(rootdir)
692-
693- test1_file = os.path.join(rootdir, "test1")
694- test1_hash = self.createTestFile(test1_file, "test1 dir")
695-
696- test2_file = os.path.join(rootdir, "test2")
697- test2_hash = self.createTestFile(test2_file, "test2 dir")
698-
699- os.mkdir(os.path.join(rootdir, "subdir1"))
700-
701- test3_file = os.path.join(rootdir, "subdir1", "test3")
702- test3_hash = self.createTestFile(test3_file, "test3 dir")
703-
704- signer = IArchiveSigningKey(self.archive)
705- with DirectoryHash(rootdir, tmpdir, signer=signer) as dh:
706- dh.add_dir(rootdir)
707-
708- expected = {
709- 'SHA256SUMS': MatchesSetwise(
710- Equals([test1_hash, "*test1"]),
711- Equals([test2_hash, "*test2"]),
712- Equals([test3_hash, "*subdir1/test3"]),
713- ),
714- }
715- self.assertThat(self.fetchSums(rootdir), MatchesDict(expected))
716- sig_content = self.fetchSigs(rootdir)
717- for dh_file in sig_content:
718- self.assertEqual(
719- sig_content[dh_file][0], '-----BEGIN PGP SIGNATURE-----\n')
720
721=== modified file 'lib/lp/archivepublisher/tests/test_run_parts.py'
722--- lib/lp/archivepublisher/tests/test_run_parts.py 2018-01-18 15:31:02 +0000
723+++ lib/lp/archivepublisher/tests/test_run_parts.py 2018-03-21 11:54:11 +0000
724@@ -30,17 +30,18 @@
725 class RunPartsMixin:
726 """Helper for run-parts tests."""
727
728- def enableRunParts(self, parts_directory=None):
729+ def enableRunParts(self, parts_directory=None, distribution_name="ubuntu"):
730 """Set up for run-parts execution.
731
732 :param parts_directory: Base location for the run-parts directories.
733 If omitted, a temporary directory will be used.
734+ :param distribution_name: The name of the distribution to set up.
735 """
736 if parts_directory is None:
737 parts_directory = self.makeTemporaryDirectory()
738- os.makedirs(os.path.join(
739- parts_directory, "ubuntu", "publish-distro.d"))
740- os.makedirs(os.path.join(parts_directory, "ubuntu", "finalize.d"))
741+ for name in ("sign.d", "publish-distro.d", "finalize.d"):
742+ os.makedirs(os.path.join(
743+ parts_directory, distribution_name, name))
744 self.parts_directory = parts_directory
745 self.pushConfig("archivepublisher", run_parts_location=parts_directory)
746
747
748=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
749--- lib/lp/archivepublisher/tests/test_signing.py 2018-03-02 18:35:42 +0000
750+++ lib/lp/archivepublisher/tests/test_signing.py 2018-03-21 11:54:11 +0000
751@@ -14,10 +14,12 @@
752 from fixtures import MonkeyPatch
753 from testtools.matchers import (
754 Contains,
755+ FileContains,
756 Matcher,
757 MatchesAll,
758 Mismatch,
759 Not,
760+ StartsWith,
761 )
762 from testtools.twistedsupport import AsynchronousDeferredRunTest
763 from twisted.internet import defer
764@@ -896,7 +898,10 @@
765 sha256file = os.path.join(self.getSignedPath("test", "amd64"),
766 "1.0", "SHA256SUMS")
767 self.assertTrue(os.path.exists(sha256file))
768- self.assertTrue(os.path.exists(sha256file + '.gpg'))
769+ self.assertThat(
770+ sha256file + '.gpg',
771+ FileContains(
772+ matcher=StartsWith('-----BEGIN PGP SIGNATURE-----\n')))
773
774 @defer.inlineCallbacks
775 def test_checksumming_tree_signed_options_tarball(self):
776@@ -916,7 +921,10 @@
777 sha256file = os.path.join(self.getSignedPath("test", "amd64"),
778 "1.0", "SHA256SUMS")
779 self.assertTrue(os.path.exists(sha256file))
780- self.assertTrue(os.path.exists(sha256file + '.gpg'))
781+ self.assertThat(
782+ sha256file + '.gpg',
783+ FileContains(
784+ matcher=StartsWith('-----BEGIN PGP SIGNATURE-----\n')))
785
786 tarfilename = os.path.join(self.getSignedPath("test", "amd64"),
787 "1.0", "signed.tar.gz")