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

Proposed by Colin Watson on 2018-01-19
Status: Needs review
Proposed branch: lp:~cjwatson/launchpad/archive-signing-run-parts
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/refactor-run-parts-subprocess
Diff against target: 805 lines (+292/-203)
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 (+86/-15)
lib/lp/archivepublisher/tests/test_publisher.py (+2/-73)
lib/lp/archivepublisher/tests/test_run_parts.py (+5/-4)
lib/lp/archivepublisher/tests/test_signing.py (+11/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/archive-signing-run-parts
Reviewer Review Type Date Requested Status
Launchpad code reviewers 2018-01-19 Pending
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.

Unmerged revisions

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 2017-04-29 15:24:32 +0000
3+++ lib/lp/archivepublisher/archivesigningkey.py 2018-01-19 16:32:05 +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-01-19 16:32:05 +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-01-19 16:32:05 +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-01-19 16:32:05 +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-01-19 16:32:05 +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-01-19 16:32:05 +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 2017-04-29 15:24:32 +0000
460+++ lib/lp/archivepublisher/tests/test_archivesigningkey.py 2018-01-19 16:32:05 +0000
461@@ -1,4 +1,4 @@
462-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
463+# Copyright 2016-2018 Canonical Ltd. This software is licensed under the
464 # GNU Affero General Public License version 3 (see the file LICENSE).
465
466 """Test ArchiveSigningKey."""
467@@ -6,16 +6,20 @@
468 __metaclass__ = type
469
470 import os
471+from textwrap import dedent
472
473 from testtools.deferredruntest import AsynchronousDeferredRunTest
474+from testtools.matchers import FileContains
475 from twisted.internet import defer
476 from zope.component import getUtility
477
478 from lp.archivepublisher.config import getPubConfig
479 from lp.archivepublisher.interfaces.archivesigningkey import (
480 IArchiveSigningKey,
481+ ISignableArchive,
482 )
483 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
484+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
485 from lp.services.osutils import write_file
486 from lp.soyuz.enums import ArchivePurpose
487 from lp.testing import TestCaseWithFactory
488@@ -24,14 +28,14 @@
489 from lp.testing.layers import ZopelessDatabaseLayer
490
491
492-class TestArchiveSigningKey(TestCaseWithFactory):
493+class TestSignableArchiveWithSigningKey(TestCaseWithFactory):
494
495 layer = ZopelessDatabaseLayer
496 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
497
498 @defer.inlineCallbacks
499 def setUp(self):
500- super(TestArchiveSigningKey, self).setUp()
501+ super(TestSignableArchiveWithSigningKey, self).setUp()
502 self.temp_dir = self.makeTemporaryDirectory()
503 self.distro = self.factory.makeDistribution()
504 db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
505@@ -48,39 +52,106 @@
506 yield IArchiveSigningKey(self.archive).setSigningKey(
507 key_path, async_keyserver=True)
508
509- def test_signfile_absolute_within_archive(self):
510+ def test_signFile_absolute_within_archive(self):
511 filename = os.path.join(self.archive_root, "signme")
512 write_file(filename, "sign this")
513
514- signer = IArchiveSigningKey(self.archive)
515- signer.signFile(filename)
516+ signer = ISignableArchive(self.archive)
517+ self.assertTrue(signer.can_sign)
518+ signer.signFile(self.suite, filename)
519
520 signature = filename + '.gpg'
521 self.assertTrue(os.path.exists(signature))
522
523- def test_signfile_absolute_outside_archive(self):
524+ def test_signFile_absolute_outside_archive(self):
525 filename = os.path.join(self.temp_dir, "signme")
526 write_file(filename, "sign this")
527
528- signer = IArchiveSigningKey(self.archive)
529- self.assertRaises(AssertionError, lambda: signer.signFile(filename))
530+ signer = ISignableArchive(self.archive)
531+ self.assertTrue(signer.can_sign)
532+ self.assertRaises(
533+ AssertionError, lambda: signer.signFile(self.suite, filename))
534
535- def test_signfile_relative_within_archive(self):
536+ def test_signFile_relative_within_archive(self):
537 filename_relative = "signme"
538 filename = os.path.join(self.archive_root, filename_relative)
539 write_file(filename, "sign this")
540
541- signer = IArchiveSigningKey(self.archive)
542- signer.signFile(filename_relative)
543+ signer = ISignableArchive(self.archive)
544+ self.assertTrue(signer.can_sign)
545+ signer.signFile(self.suite, filename_relative)
546
547 signature = filename + '.gpg'
548 self.assertTrue(os.path.exists(signature))
549
550- def test_signfile_relative_outside_archive(self):
551+ def test_signFile_relative_outside_archive(self):
552 filename_relative = "../signme"
553 filename = os.path.join(self.temp_dir, filename_relative)
554 write_file(filename, "sign this")
555
556- signer = IArchiveSigningKey(self.archive)
557+ signer = ISignableArchive(self.archive)
558+ self.assertTrue(signer.can_sign)
559 self.assertRaises(
560- AssertionError, lambda: signer.signFile(filename_relative))
561+ AssertionError,
562+ lambda: signer.signFile(self.suite, filename_relative))
563+
564+
565+class TestSignableArchiveWithRunParts(RunPartsMixin, TestCaseWithFactory):
566+
567+ layer = ZopelessDatabaseLayer
568+
569+ def setUp(self):
570+ super(TestSignableArchiveWithRunParts, self).setUp()
571+ self.temp_dir = self.makeTemporaryDirectory()
572+ self.distro = self.factory.makeDistribution()
573+ db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
574+ self.distro)
575+ db_pubconf.root_dir = unicode(self.temp_dir)
576+ self.archive = self.factory.makeArchive(
577+ distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
578+ self.archive_root = getPubConfig(self.archive).archiveroot
579+ self.suite = "distroseries"
580+ self.enableRunParts(distribution_name=self.distro.name)
581+ with open(os.path.join(
582+ self.parts_directory, self.distro.name, "sign.d",
583+ "10-sign"), "w") as sign_script:
584+ sign_script.write(dedent("""\
585+ #! /bin/sh
586+ echo "$MODE signature of $INPUT_PATH ($DISTRIBUTION/$SUITE)" \\
587+ >"$OUTPUT_PATH"
588+ """))
589+ os.fchmod(sign_script.fileno(), 0o755)
590+
591+ def test_signRepository_runs_parts(self):
592+ suite_dir = os.path.join(self.archive_root, "dists", self.suite)
593+ release_path = os.path.join(suite_dir, "Release")
594+ write_file(release_path, "Release contents")
595+
596+ signer = ISignableArchive(self.archive)
597+ self.assertTrue(signer.can_sign)
598+ signer.signRepository(self.suite)
599+
600+ self.assertThat(
601+ os.path.join(suite_dir, "Release.gpg"),
602+ FileContains(
603+ "detached signature of %s (%s/%s)\n" %
604+ (release_path, self.distro.name, self.suite)))
605+ self.assertThat(
606+ os.path.join(suite_dir, "InRelease"),
607+ FileContains(
608+ "clear signature of %s (%s/%s)\n" %
609+ (release_path, self.distro.name, self.suite)))
610+
611+ def test_signFile_runs_parts(self):
612+ filename = os.path.join(self.archive_root, "signme")
613+ write_file(filename, "sign this")
614+
615+ signer = ISignableArchive(self.archive)
616+ self.assertTrue(signer.can_sign)
617+ signer.signFile(self.suite, filename)
618+
619+ self.assertThat(
620+ "%s.gpg" % filename,
621+ FileContains(
622+ "detached signature of %s (%s/%s)\n" %
623+ (filename, self.distro.name, self.suite)))
624
625=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
626--- lib/lp/archivepublisher/tests/test_publisher.py 2017-04-29 15:24:32 +0000
627+++ lib/lp/archivepublisher/tests/test_publisher.py 2018-01-19 16:32:05 +0000
628@@ -1,4 +1,4 @@
629-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
630+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
631 # GNU Affero General Public License version 3 (see the file LICENSE).
632
633 """Tests for publisher class."""
634@@ -68,7 +68,6 @@
635 Publisher,
636 )
637 from lp.archivepublisher.utils import RepositoryIndexFile
638-from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
639 from lp.registry.interfaces.distribution import IDistributionSet
640 from lp.registry.interfaces.distroseries import IDistroSeries
641 from lp.registry.interfaces.person import IPersonSet
642@@ -3208,16 +3207,6 @@
643 result[dh_file].append(line.strip().split(' '))
644 return result
645
646- def fetchSigs(self, rootdir):
647- result = defaultdict(list)
648- for dh_file in self.all_hash_files:
649- checksum_sig = os.path.join(rootdir, dh_file) + '.gpg'
650- if os.path.exists(checksum_sig):
651- with open(checksum_sig, "r") as sfd:
652- for line in sfd:
653- result[dh_file].append(line)
654- return result
655-
656
657 class TestDirectoryHash(TestDirectoryHashHelpers):
658 """Unit tests for DirectoryHash object."""
659@@ -3232,7 +3221,7 @@
660 checksum_file = os.path.join(rootdir, dh_file)
661 self.assertFalse(os.path.exists(checksum_file))
662
663- with DirectoryHash(rootdir, tmpdir, None):
664+ with DirectoryHash(rootdir, tmpdir):
665 pass
666
667 for dh_file in self.all_hash_files:
668@@ -3295,63 +3284,3 @@
669 ),
670 }
671 self.assertThat(self.fetchSums(rootdir), MatchesDict(expected))
672-
673-
674-class TestDirectoryHashSigning(TestDirectoryHashHelpers):
675- """Unit tests for DirectoryHash object, signing functionality."""
676-
677- layer = ZopelessDatabaseLayer
678- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
679-
680- @defer.inlineCallbacks
681- def setUp(self):
682- super(TestDirectoryHashSigning, self).setUp()
683- self.temp_dir = self.makeTemporaryDirectory()
684- self.distro = self.factory.makeDistribution()
685- db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
686- self.distro)
687- db_pubconf.root_dir = unicode(self.temp_dir)
688- self.archive = self.factory.makeArchive(
689- distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
690- self.archive_root = getPubConfig(self.archive).archiveroot
691- self.suite = "distroseries"
692-
693- # Setup a keyserver so we can install the archive key.
694- with InProcessKeyServerFixture() as keyserver:
695- yield keyserver.start()
696- key_path = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
697- yield IArchiveSigningKey(self.archive).setSigningKey(
698- key_path, async_keyserver=True)
699-
700- def test_basic_directory_add_signed(self):
701- tmpdir = unicode(self.makeTemporaryDirectory())
702- rootdir = self.archive_root
703- os.makedirs(rootdir)
704-
705- test1_file = os.path.join(rootdir, "test1")
706- test1_hash = self.createTestFile(test1_file, "test1 dir")
707-
708- test2_file = os.path.join(rootdir, "test2")
709- test2_hash = self.createTestFile(test2_file, "test2 dir")
710-
711- os.mkdir(os.path.join(rootdir, "subdir1"))
712-
713- test3_file = os.path.join(rootdir, "subdir1", "test3")
714- test3_hash = self.createTestFile(test3_file, "test3 dir")
715-
716- signer = IArchiveSigningKey(self.archive)
717- with DirectoryHash(rootdir, tmpdir, signer=signer) as dh:
718- dh.add_dir(rootdir)
719-
720- expected = {
721- 'SHA256SUMS': MatchesSetwise(
722- Equals([test1_hash, "*test1"]),
723- Equals([test2_hash, "*test2"]),
724- Equals([test3_hash, "*subdir1/test3"]),
725- ),
726- }
727- self.assertThat(self.fetchSums(rootdir), MatchesDict(expected))
728- sig_content = self.fetchSigs(rootdir)
729- for dh_file in sig_content:
730- self.assertEqual(
731- sig_content[dh_file][0], '-----BEGIN PGP SIGNATURE-----\n')
732
733=== modified file 'lib/lp/archivepublisher/tests/test_run_parts.py'
734--- lib/lp/archivepublisher/tests/test_run_parts.py 2018-01-19 16:32:05 +0000
735+++ lib/lp/archivepublisher/tests/test_run_parts.py 2018-01-19 16:32:05 +0000
736@@ -30,17 +30,18 @@
737 class RunPartsMixin:
738 """Helper for run-parts tests."""
739
740- def enableRunParts(self, parts_directory=None):
741+ def enableRunParts(self, parts_directory=None, distribution_name="ubuntu"):
742 """Set up for run-parts execution.
743
744 :param parts_directory: Base location for the run-parts directories.
745 If omitted, a temporary directory will be used.
746+ :param distribution_name: The name of the distribution to set up.
747 """
748 if parts_directory is None:
749 parts_directory = self.makeTemporaryDirectory()
750- os.makedirs(os.path.join(
751- parts_directory, "ubuntu", "publish-distro.d"))
752- os.makedirs(os.path.join(parts_directory, "ubuntu", "finalize.d"))
753+ for name in ("sign.d", "publish-distro.d", "finalize.d"):
754+ os.makedirs(os.path.join(
755+ parts_directory, distribution_name, name))
756 self.parts_directory = parts_directory
757 self.pushConfig("archivepublisher", run_parts_location=parts_directory)
758
759
760=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
761--- lib/lp/archivepublisher/tests/test_signing.py 2017-08-02 19:13:48 +0000
762+++ lib/lp/archivepublisher/tests/test_signing.py 2018-01-19 16:32:05 +0000
763@@ -1,4 +1,4 @@
764-# Copyright 2012-2017 Canonical Ltd. This software is licensed under the
765+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
766 # GNU Affero General Public License version 3 (see the file LICENSE).
767
768 """Test UEFI custom uploads."""
769@@ -13,10 +13,12 @@
770 from testtools.deferredruntest import AsynchronousDeferredRunTest
771 from testtools.matchers import (
772 Contains,
773+ FileContains,
774 Matcher,
775 MatchesAll,
776 Mismatch,
777 Not,
778+ StartsWith,
779 )
780 from twisted.internet import defer
781 from zope.component import getUtility
782@@ -895,7 +897,10 @@
783 sha256file = os.path.join(self.getSignedPath("test", "amd64"),
784 "1.0", "SHA256SUMS")
785 self.assertTrue(os.path.exists(sha256file))
786- self.assertTrue(os.path.exists(sha256file + '.gpg'))
787+ self.assertThat(
788+ sha256file + '.gpg',
789+ FileContains(
790+ matcher=StartsWith('-----BEGIN PGP SIGNATURE-----\n')))
791
792 @defer.inlineCallbacks
793 def test_checksumming_tree_signed_options_tarball(self):
794@@ -915,7 +920,10 @@
795 sha256file = os.path.join(self.getSignedPath("test", "amd64"),
796 "1.0", "SHA256SUMS")
797 self.assertTrue(os.path.exists(sha256file))
798- self.assertTrue(os.path.exists(sha256file + '.gpg'))
799+ self.assertThat(
800+ sha256file + '.gpg',
801+ FileContains(
802+ matcher=StartsWith('-----BEGIN PGP SIGNATURE-----\n')))
803
804 tarfilename = os.path.join(self.getSignedPath("test", "amd64"),
805 "1.0", "signed.tar.gz")