launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22113
[Merge] lp:~cjwatson/launchpad/refactor-archive-signing into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/refactor-archive-signing into lp:launchpad with lp:~cjwatson/launchpad/archive-signing-run-parts as a prerequisite.
Commit message:
Extend custom uploads and Release file signing to use the new ISignableArchive interface.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/refactor-archive-signing/+merge/336377
This replaces ubuntu-archive-publishing/publish-distro.d/10-sign-releases.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/refactor-archive-signing into lp:launchpad.
=== modified file 'lib/lp/archivepublisher/archivesigningkey.py'
--- lib/lp/archivepublisher/archivesigningkey.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/archivesigningkey.py 2018-01-19 17:56:02 +0000
@@ -119,19 +119,21 @@
"No signing key available for %s" %
self.archive.displayname)
- def signRepository(self, suite, log=None):
+ def signRepository(self, suite, suffix='', log=None):
"""See `ISignableArchive`."""
suite_path = os.path.join(self._archive_root_path, 'dists', suite)
- release_file_path = os.path.join(suite_path, 'Release')
+ release_file_path = os.path.join(suite_path, 'Release' + suffix)
if not os.path.exists(release_file_path):
raise AssertionError(
"Release file doesn't exist in the repository: %s" %
release_file_path)
self._makeSignatures([
- (release_file_path, os.path.join(suite_path, 'Release.gpg'),
+ (release_file_path,
+ os.path.join(suite_path, 'Release.gpg' + suffix),
SigningMode.DETACHED, suite),
- (release_file_path, os.path.join(suite_path, 'InRelease'),
+ (release_file_path,
+ os.path.join(suite_path, 'InRelease' + suffix),
SigningMode.CLEAR, suite),
], log=log)
=== modified file 'lib/lp/archivepublisher/customupload.py'
--- lib/lp/archivepublisher/customupload.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/customupload.py 2018-01-19 17:56:02 +0000
@@ -26,6 +26,7 @@
Version as make_version,
VersionError,
)
+from lp.archivepublisher.interfaces.archivesigningkey import ISignableArchive
from lp.services.librarian.utils import copy_and_close
from lp.soyuz.interfaces.queue import (
CustomUploadError,
@@ -268,6 +269,20 @@
if not os.path.isdir(parentdir):
os.makedirs(parentdir, 0o755)
+ def shouldSign(self, filename):
+ """Returns True if the given filename should be signed."""
+ return False
+
+ def sign(self, archive, suite, filename):
+ """Sign a file.
+
+ For now, we always write a detached signature to the input file name
+ plus ".gpg".
+ """
+ signable_archive = ISignableArchive(archive)
+ if signable_archive.can_sign:
+ signable_archive.signFile(suite, filename, log=self.logger)
+
def installFiles(self, archive, suite):
"""Install the files from the custom upload to the archive."""
assert self.tmpdir is not None, "Must extract tarfile first"
@@ -316,6 +331,8 @@
else:
shutil.copy(sourcepath, destpath)
os.chmod(destpath, 0o644)
+ if self.shouldSign(destpath):
+ self.sign(archive, suite, destpath)
extracted = True
=== modified file 'lib/lp/archivepublisher/debian_installer.py'
--- lib/lp/archivepublisher/debian_installer.py 2016-06-07 17:07:35 +0000
+++ lib/lp/archivepublisher/debian_installer.py 2018-01-19 17:56:02 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""The processing of debian installer tarballs."""
@@ -76,3 +76,7 @@
def shouldInstall(self, filename):
return filename.startswith('%s/' % self.version)
+
+ def shouldSign(self, filename):
+ """Sign checksums files."""
+ return filename.endswith('SUMS')
=== modified file 'lib/lp/archivepublisher/dist_upgrader.py'
--- lib/lp/archivepublisher/dist_upgrader.py 2016-06-07 17:07:35 +0000
+++ lib/lp/archivepublisher/dist_upgrader.py 2018-01-19 17:56:02 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""The processing of dist-upgrader tarballs."""
@@ -81,7 +81,7 @@
return None
def shouldInstall(self, filename):
- """ Install files from a dist-upgrader tarball.
+ """Install files from a dist-upgrader tarball.
It raises DistUpgraderBadVersion if if finds a directory name that
could not be treated as a valid Debian version.
@@ -100,3 +100,7 @@
except BadUpstreamError as exc:
raise DistUpgraderBadVersion(self.tarfile_path, exc)
return version and not filename.startswith('current')
+
+ def shouldSign(self, filename):
+ """Sign *.tar.gz files."""
+ return filename.endswith('.tar.gz')
=== modified file 'lib/lp/archivepublisher/publishing.py'
--- lib/lp/archivepublisher/publishing.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/publishing.py 2018-01-19 17:56:02 +0000
@@ -57,9 +57,7 @@
build_source_stanza_fields,
build_translations_stanza_fields,
)
-from lp.archivepublisher.interfaces.archivesigningkey import (
- IArchiveSigningKey,
- )
+from lp.archivepublisher.interfaces.archivesigningkey import ISignableArchive
from lp.archivepublisher.model.ftparchive import FTPArchiveHandler
from lp.archivepublisher.utils import (
get_ppa_reference,
@@ -1245,20 +1243,23 @@
if distroseries.publish_by_hash:
self._updateByHash(suite, "Release.new")
- os.rename(
- os.path.join(suite_dir, "Release.new"),
- os.path.join(suite_dir, "Release"))
-
- if self.archive.signing_key is not None:
+ signable_archive = ISignableArchive(self.archive)
+ if signable_archive.can_sign:
# Sign the repository.
self.log.debug("Signing Release file for %s" % suite)
- IArchiveSigningKey(self.archive).signRepository(suite)
+ signable_archive.signRepository(suite, suffix=".new", log=self.log)
core_files.add("Release.gpg")
core_files.add("InRelease")
else:
- # Skip signature if the archive signing key is undefined.
+ # Skip signature if the archive is not set up for signing.
self.log.debug("No signing key available, skipping signature.")
+ for name in ("Release", "Release.gpg", "InRelease"):
+ if name in core_files:
+ os.rename(
+ os.path.join(suite_dir, "%s.new" % name),
+ os.path.join(suite_dir, name))
+
# Make sure all the timestamps match, to make it easier to insert
# caching headers on mirrors.
self._syncTimestamps(suite, core_files)
=== modified file 'lib/lp/archivepublisher/signing.py'
--- lib/lp/archivepublisher/signing.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/signing.py 2018-01-19 17:56:02 +0000
@@ -28,9 +28,6 @@
from lp.archivepublisher.config import getPubConfig
from lp.archivepublisher.customupload import CustomUpload
-from lp.archivepublisher.interfaces.archivesigningkey import (
- IArchiveSigningKey,
- )
from lp.services.osutils import remove_if_exists
from lp.soyuz.interfaces.queue import CustomUploadError
@@ -379,12 +376,15 @@
with DirectoryHash(versiondir, self.temproot) as hasher:
hasher.add_dir(versiondir)
for checksum_path in hasher.checksum_paths:
- if archive.signing_key is not None:
- IArchiveSigningKey(archive).signFile(suite, checksum_path)
+ if self.shouldSign(checksum_path):
+ self.sign(archive, suite, checksum_path)
def shouldInstall(self, filename):
return filename.startswith("%s/" % self.version)
+ def shouldSign(self, filename):
+ return filename.endswith("SUMS")
+
class UefiUpload(SigningUpload):
"""Legacy UEFI Signing custom upload.
=== modified file 'lib/lp/archivepublisher/tests/test_customupload.py'
--- lib/lp/archivepublisher/tests/test_customupload.py 2012-05-28 13:13:53 +0000
+++ lib/lp/archivepublisher/tests/test_customupload.py 2018-01-19 17:56:02 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for `CustomUploads`."""
@@ -13,13 +13,40 @@
import tempfile
import unittest
+from fixtures import MonkeyPatch
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from testtools.matchers import (
+ Equals,
+ MatchesDict,
+ Not,
+ PathExists,
+ )
+from twisted.internet import defer
+from zope.component import getUtility
+
+from lp.archivepublisher.config import getPubConfig
from lp.archivepublisher.customupload import (
CustomUpload,
CustomUploadTarballBadFile,
CustomUploadTarballBadSymLink,
CustomUploadTarballInvalidFileType,
)
-from lp.testing import TestCase
+from lp.archivepublisher.interfaces.archivesigningkey import (
+ IArchiveSigningKey,
+ )
+from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
+from lp.services.gpg.interfaces import IGPGHandler
+from lp.services.osutils import write_file
+from lp.soyuz.enums import ArchivePurpose
+from lp.testing import (
+ TestCase,
+ TestCaseWithFactory,
+ )
+from lp.testing.fakemethod import FakeMethod
+from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.keyserver import InProcessKeyServerFixture
+from lp.testing.layers import LaunchpadZopelessLayer
class TestCustomUpload(unittest.TestCase):
@@ -198,3 +225,68 @@
self.custom_processor.extract)
finally:
shutil.rmtree(self.tarfile_path)
+
+
+class TestSigning(TestCaseWithFactory, RunPartsMixin):
+
+ layer = LaunchpadZopelessLayer
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
+
+ def setUp(self):
+ super(TestSigning, self).setUp()
+ self.temp_dir = self.makeTemporaryDirectory()
+ self.distro = self.factory.makeDistribution()
+ db_pubconf = getUtility(IPublisherConfigSet).getByDistribution(
+ self.distro)
+ db_pubconf.root_dir = unicode(self.temp_dir)
+ self.archive = self.factory.makeArchive(
+ distribution=self.distro, purpose=ArchivePurpose.PRIMARY)
+
+ def test_sign_without_signing_key(self):
+ filename = os.path.join(
+ getPubConfig(self.archive).archiveroot, "file")
+ self.assertIsNone(self.archive.signing_key)
+ custom_processor = CustomUpload()
+ custom_processor.sign(self.archive, "suite", filename)
+ self.assertThat("%s.gpg" % filename, Not(PathExists()))
+
+ @defer.inlineCallbacks
+ def test_sign_with_signing_key(self):
+ filename = os.path.join(
+ getPubConfig(self.archive).archiveroot, "file")
+ write_file(filename, "contents")
+ self.assertIsNone(self.archive.signing_key)
+ self.useFixture(InProcessKeyServerFixture()).start()
+ key_path = os.path.join(gpgkeysdir, 'ppa-sample@xxxxxxxxxxxxxxxxx')
+ yield IArchiveSigningKey(self.archive).setSigningKey(
+ key_path, async_keyserver=True)
+ self.assertIsNotNone(self.archive.signing_key)
+ custom_processor = CustomUpload()
+ custom_processor.sign(self.archive, "suite", filename)
+ with open(filename) as cleartext_file:
+ cleartext = cleartext_file.read()
+ with open("%s.gpg" % filename) as signature_file:
+ signature = getUtility(IGPGHandler).getVerifiedSignature(
+ cleartext, signature_file.read())
+ self.assertEqual(
+ self.archive.signing_key.fingerprint, signature.fingerprint)
+
+ def test_sign_with_external_run_parts(self):
+ self.enableRunParts(distribution_name=self.distro.name)
+ filename = os.path.join(
+ getPubConfig(self.archive).archiveroot, "file")
+ write_file(filename, "contents")
+ self.assertIsNone(self.archive.signing_key)
+ run_parts_fixture = self.useFixture(MonkeyPatch(
+ "lp.archivepublisher.archivesigningkey.run_parts", FakeMethod()))
+ custom_processor = CustomUpload()
+ custom_processor.sign(self.archive, "suite", filename)
+ args, kwargs = run_parts_fixture.new_value.calls[0]
+ self.assertEqual((self.distro.name, "sign.d"), args)
+ self.assertThat(kwargs["env"], MatchesDict({
+ "INPUT_PATH": Equals(filename),
+ "OUTPUT_PATH": Equals("%s.gpg" % filename),
+ "MODE": Equals("detached"),
+ "DISTRIBUTION": Equals(self.distro.name),
+ "SUITE": Equals("suite"),
+ }))
=== modified file 'lib/lp/archivepublisher/tests/test_debian_installer.py'
--- lib/lp/archivepublisher/tests/test_debian_installer.py 2016-06-07 17:07:35 +0000
+++ lib/lp/archivepublisher/tests/test_debian_installer.py 2018-01-19 17:56:02 +0000
@@ -1,4 +1,4 @@
-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test debian-installer custom uploads.
@@ -8,7 +8,9 @@
"""
import os
+from textwrap import dedent
+from testtools.matchers import DirContains
from zope.component import getUtility
from lp.archivepublisher.config import getPubConfig
@@ -161,6 +163,26 @@
self.assertEqual(
0o755, os.stat(self.getInstallerPath(directory)).st_mode & 0o777)
+ def test_sign_with_external_run_parts(self):
+ parts_directory = self.makeTemporaryDirectory()
+ sign_directory = os.path.join(
+ parts_directory, self.distro.name, "sign.d")
+ os.makedirs(sign_directory)
+ with open(os.path.join(sign_directory, "10-sign"), "w") as f:
+ f.write(dedent("""\
+ #! /bin/sh
+ touch "$OUTPUT_PATH"
+ """))
+ os.fchmod(f.fileno(), 0o755)
+ self.pushConfig("archivepublisher", run_parts_location=parts_directory)
+ self.openArchive()
+ self.addFile("images/list", "a list")
+ self.addFile("images/SHA256SUMS", "a checksum")
+ self.process()
+ self.assertThat(
+ self.getInstallerPath("images"),
+ DirContains(["list", "SHA256SUMS", "SHA256SUMS.gpg"]))
+
def test_getSeriesKey_extracts_architecture(self):
# getSeriesKey extracts the architecture from an upload's filename.
self.openArchive()
=== modified file 'lib/lp/archivepublisher/tests/test_dist_upgrader.py'
--- lib/lp/archivepublisher/tests/test_dist_upgrader.py 2016-06-07 17:07:35 +0000
+++ lib/lp/archivepublisher/tests/test_dist_upgrader.py 2018-01-19 17:56:02 +0000
@@ -1,4 +1,4 @@
-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2012-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test dist-upgrader custom uploads.
@@ -8,7 +8,9 @@
"""
import os
+from textwrap import dedent
+from testtools.matchers import DirContains
from zope.component import getUtility
from lp.archivepublisher.config import getPubConfig
@@ -109,6 +111,26 @@
self.tarfile.add_file("foobar/foobar/dapper.tar.gz", "")
self.assertRaises(DistUpgraderBadVersion, self.process)
+ def test_sign_with_external_run_parts(self):
+ parts_directory = self.makeTemporaryDirectory()
+ sign_directory = os.path.join(
+ parts_directory, self.distro.name, "sign.d")
+ os.makedirs(sign_directory)
+ with open(os.path.join(sign_directory, "10-sign"), "w") as f:
+ f.write(dedent("""\
+ #! /bin/sh
+ touch "$OUTPUT_PATH"
+ """))
+ os.fchmod(f.fileno(), 0o755)
+ self.pushConfig("archivepublisher", run_parts_location=parts_directory)
+ self.openArchive("20060302.0120")
+ self.tarfile.add_file("20060302.0120/list", "a list")
+ self.tarfile.add_file("20060302.0120/foo.tar.gz", "a tarball")
+ self.process()
+ self.assertThat(
+ os.path.join(self.getUpgraderPath(), "20060302.0120"),
+ DirContains(["list", "foo.tar.gz", "foo.tar.gz.gpg"]))
+
def test_getSeriesKey_extracts_architecture(self):
# getSeriesKey extracts the architecture from an upload's filename.
self.openArchive("20060302.0120")
=== modified file 'lib/lp/archivepublisher/tests/test_publisher.py'
--- lib/lp/archivepublisher/tests/test_publisher.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/tests/test_publisher.py 2018-01-19 17:56:02 +0000
@@ -67,6 +67,7 @@
I18nIndex,
Publisher,
)
+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
from lp.archivepublisher.utils import RepositoryIndexFile
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.distroseries import IDistroSeries
@@ -2925,7 +2926,7 @@
os.rename(temporary_dists, original_dists)
-class TestPublisherRepositorySignatures(TestPublisherBase):
+class TestPublisherRepositorySignatures(RunPartsMixin, TestPublisherBase):
"""Testing `Publisher` signature behaviour."""
run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
@@ -3065,6 +3066,54 @@
self.assertThat(
sync_args[1], ContainsAll(['Release', 'Release.gpg', 'InRelease']))
+ def testRepositorySignatureWithExternalRunParts(self):
+ """Check publisher behaviour when signing repositories.
+
+ When a 'sign.d' run-parts directory is configured for the archive,
+ it is used to sign the Release file.
+ """
+ cprov = getUtility(IPersonSet).getByName('cprov')
+ self.assertIsNone(cprov.archive.signing_key)
+ self.enableRunParts(distribution_name=cprov.archive.distribution.name)
+ sign_directory = os.path.join(
+ self.parts_directory, cprov.archive.distribution.name, 'sign.d')
+ with open(os.path.join(sign_directory, '10-sign'), 'w') as sign_script:
+ sign_script.write(dedent("""\
+ #! /bin/sh
+ echo "$MODE signature of $INPUT_PATH ($DISTRIBUTION/$SUITE)" \\
+ >"$OUTPUT_PATH"
+ """))
+ os.fchmod(sign_script.fileno(), 0o755)
+
+ self.setupPublisher(cprov.archive)
+ self.archive_publisher._syncTimestamps = FakeMethod()
+
+ self._publishArchive(cprov.archive)
+
+ # Release exists.
+ self.assertThat(self.release_file_path, PathExists())
+
+ # Release.gpg and InRelease exist with suitable fake signatures.
+ # Note that the signatures are made before Release.new is renamed to
+ # to Release.
+ self.assertThat(
+ self.release_file_signature_path,
+ FileContains(
+ "detached signature of %s.new (%s/breezy-autotest)\n" %
+ (self.release_file_path, cprov.archive.distribution.name)))
+ self.assertThat(
+ self.inline_release_file_path,
+ FileContains(
+ "clear signature of %s.new (%s/breezy-autotest)\n" %
+ (self.release_file_path, cprov.archive.distribution.name)))
+
+ # The publisher synchronises the various Release file timestamps.
+ self.assertEqual(1, self.archive_publisher._syncTimestamps.call_count)
+ sync_args = self.archive_publisher._syncTimestamps.extract_args()[0]
+ self.assertEqual(self.distroseries.name, sync_args[0])
+ self.assertThat(
+ sync_args[1], ContainsAll(['Release', 'Release.gpg', 'InRelease']))
+
class TestPublisherLite(TestCaseWithFactory):
"""Lightweight unit tests for the publisher."""
=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
--- lib/lp/archivepublisher/tests/test_signing.py 2018-01-19 17:56:02 +0000
+++ lib/lp/archivepublisher/tests/test_signing.py 2018-01-19 17:56:02 +0000
@@ -13,9 +13,11 @@
from testtools.deferredruntest import AsynchronousDeferredRunTest
from testtools.matchers import (
Contains,
+ Equals,
FileContains,
Matcher,
MatchesAll,
+ MatchesDict,
Mismatch,
Not,
StartsWith,
@@ -36,6 +38,7 @@
SigningUpload,
UefiUpload,
)
+from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
from lp.services.osutils import write_file
from lp.services.tarfile_helpers import LaunchpadWriteTarFile
from lp.soyuz.enums import ArchivePurpose
@@ -215,7 +218,7 @@
return os.path.join(pubconf.archiveroot, "dists", self.suite, "main")
-class TestSigning(TestSigningHelpers):
+class TestSigning(RunPartsMixin, TestSigningHelpers):
def getSignedPath(self, loader_type, arch):
return os.path.join(self.getDistsPath(), "signed",
@@ -934,6 +937,36 @@
"1.0/signed.tar.gz",
]]))
+ def test_checksumming_tree_signed_with_external_run_parts(self):
+ # Checksum files can be signed using an external run-parts helper.
+ # We disable subprocess.call because there's just too much going on,
+ # so we can't test this completely, but we can at least test that
+ # run_parts is called.
+ self.enableRunParts(distribution_name=self.distro.name)
+ run_parts_fixture = self.useFixture(MonkeyPatch(
+ "lp.archivepublisher.archivesigningkey.run_parts", FakeMethod()))
+ self.setUpUefiKeys()
+ self.setUpKmodKeys()
+ self.setUpOpalKeys()
+ self.openArchive("test", "1.0", "amd64")
+ self.tarfile.add_file("1.0/empty.efi", "")
+ self.tarfile.add_file("1.0/empty.ko", "")
+ self.tarfile.add_file("1.0/empty.opal", "")
+ self.process_emulate()
+ sha256file = os.path.join(self.getSignedPath("test", "amd64"),
+ "1.0", "SHA256SUMS")
+ self.assertTrue(os.path.exists(sha256file))
+ self.assertEqual(1, run_parts_fixture.new_value.call_count)
+ args, kwargs = run_parts_fixture.new_value.calls[-1]
+ self.assertEqual((self.distro.name, "sign.d"), args)
+ self.assertThat(kwargs["env"], MatchesDict({
+ "INPUT_PATH": Equals(sha256file),
+ "OUTPUT_PATH": Equals("%s.gpg" % sha256file),
+ "MODE": Equals("detached"),
+ "DISTRIBUTION": Equals(self.distro.name),
+ "SUITE": Equals(self.suite),
+ }))
+
class TestUefi(TestSigningHelpers):
Follow ups