Merge ~pappacena/launchpad:inject-lp-signing-generated-keys into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 00e44a212547fc747080ec3266508faee1a25fcc
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:inject-lp-signing-generated-keys
Merge into: launchpad:master
Diff against target: 433 lines (+255/-6)
6 files modified
lib/lp/archivepublisher/signing.py (+77/-1)
lib/lp/archivepublisher/tests/test_signing.py (+111/-3)
lib/lp/services/signing/interfaces/signingkey.py (+4/-1)
lib/lp/services/signing/model/signingkey.py (+6/-1)
lib/lp/services/signing/tests/helpers.py (+10/-0)
lib/lp/services/signing/tests/test_signingkey.py (+47/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+382779@code.launchpad.net

Commit message

Adding the possibility to inject into lp-signing the locally generated signing keys.

It is possible to control which key types to inject when auto-generating them by setting the feature flag `archivepublisher.signing_service.injection.enabled` with a list of key types (separated by spaces). Eg.: "KMOD UEFI".

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Information
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes. Let me know if you need another round of review, cjwatson.

Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

cjwatson, I'll push some of the requested changes, but I would like further clarification on some of your previous comments.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes. It might be good to have another round of review on the duplicated ArchiveSigningKey check.

Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes (with some comments, cjwatson).

Revision history for this message
Thiago F. Pappacena (pappacena) :
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/archivepublisher/signing.py b/lib/lp/archivepublisher/signing.py
index 9a175db..1eca0a4 100644
--- a/lib/lp/archivepublisher/signing.py
+++ b/lib/lp/archivepublisher/signing.py
@@ -18,6 +18,7 @@ __all__ = [
18 "UefiUpload",18 "UefiUpload",
19 ]19 ]
2020
21from datetime import datetime
21from functools import partial22from functools import partial
22import os23import os
23import shutil24import shutil
@@ -27,6 +28,7 @@ import tarfile
27import tempfile28import tempfile
28import textwrap29import textwrap
2930
31from pytz import utc
30import scandir32import scandir
31from zope.component import getUtility33from zope.component import getUtility
3234
@@ -42,6 +44,8 @@ from lp.soyuz.interfaces.queue import CustomUploadError
4244
43PUBLISHER_USES_SIGNING_SERVICE = (45PUBLISHER_USES_SIGNING_SERVICE = (
44 'archivepublisher.signing_service.enabled')46 'archivepublisher.signing_service.enabled')
47PUBLISHER_SIGNING_SERVICE_INJECTS_KEYS = (
48 'archivepublisher.signing_service.injection.enabled')
4549
4650
47class SigningUploadPackError(CustomUploadError):51class SigningUploadPackError(CustomUploadError):
@@ -59,6 +63,10 @@ class SigningServiceError(Exception):
59 pass63 pass
6064
6165
66class SigningKeyConflict(Exception):
67 pass
68
69
62class SigningUpload(CustomUpload):70class SigningUpload(CustomUpload):
63 """Signing custom upload.71 """Signing custom upload.
6472
@@ -260,7 +268,7 @@ class SigningUpload(CustomUpload):
260 # tend to make the publisher rather upset.268 # tend to make the publisher rather upset.
261 if self.logger is not None:269 if self.logger is not None:
262 self.logger.warning("%s Failed (cmd='%s')" %270 self.logger.warning("%s Failed (cmd='%s')" %
263 (description, " ".join(cmdl)))271 (description, " ".join(cmdl)))
264 return status272 return status
265273
266 def findSigningHandlers(self):274 def findSigningHandlers(self):
@@ -421,6 +429,56 @@ class SigningUpload(CustomUpload):
421 return [None for k in keynames]429 return [None for k in keynames]
422 return keynames430 return keynames
423431
432 def injectIntoSigningService(
433 self, key_type, private_key_file, public_key_file):
434 """Injects the given key pair into signing service for current
435 archive.
436
437 Note that this injection should only be used for freshly
438 autogenerated keys, always injecting the key for the archive in
439 general (not setting earliest_distro_series).
440 """
441 if key_type not in SigningKeyType:
442 raise ValueError("%s is not a valid key type to inject" % key_type)
443
444 feature_flag = (
445 getFeatureFlag(PUBLISHER_SIGNING_SERVICE_INJECTS_KEYS) or '')
446 key_types_to_inject = [i.strip() for i in feature_flag.split()]
447
448 if key_type.name not in key_types_to_inject:
449 if self.logger:
450 self.logger.info(
451 "Skipping injection for key type %s: not in %s",
452 key_type, key_types_to_inject)
453 return
454
455 key_set = getUtility(IArchiveSigningKeySet)
456 current_key = key_set.getSigningKey(
457 key_type, self.archive, None, exact_match=True)
458 if current_key is not None:
459 self.logger.info("Skipping injection for key type %s: archive "
460 "already has a key on lp-signing.", key_type)
461 raise SigningKeyConflict(
462 "Archive %s already has a signing key type %s on lp-signing."
463 % (self.archive.reference, key_type))
464
465 if self.logger:
466 self.logger.info(
467 "Injecting key_type %s for archive %s into signing service",
468 key_type, self.archive.name)
469
470 with open(private_key_file, 'rb') as fd:
471 private_key = fd.read()
472 with open(public_key_file, 'rb') as fd:
473 public_key = fd.read()
474
475 now = datetime.now().replace(tzinfo=utc)
476 description = (
477 u"%s key for %s" % (key_type.name, self.archive.reference))
478 key_set.inject(
479 key_type, private_key, public_key,
480 description, now, self.archive, earliest_distro_series=None)
481
424 def generateKeyCommonName(self, owner, archive, suffix=''):482 def generateKeyCommonName(self, owner, archive, suffix=''):
425 # PPA <owner> <archive> <suffix>483 # PPA <owner> <archive> <suffix>
426 # truncate <owner> <archive> to ensure the overall form is shorter484 # truncate <owner> <archive> to ensure the overall form is shorter
@@ -454,6 +512,15 @@ class SigningUpload(CustomUpload):
454 if os.path.exists(cert_filename):512 if os.path.exists(cert_filename):
455 os.chmod(cert_filename, 0o644)513 os.chmod(cert_filename, 0o644)
456514
515 signing_key_type = getattr(SigningKeyType, key_type.upper())
516 try:
517 self.injectIntoSigningService(
518 signing_key_type, key_filename, cert_filename)
519 except SigningKeyConflict:
520 os.unlink(key_filename)
521 os.unlink(cert_filename)
522 raise
523
457 def generateUefiKeys(self):524 def generateUefiKeys(self):
458 """Generate new UEFI Keys for this archive."""525 """Generate new UEFI Keys for this archive."""
459 self.generateKeyCrtPair("UEFI", self.uefi_key, self.uefi_cert)526 self.generateKeyCrtPair("UEFI", self.uefi_key, self.uefi_cert)
@@ -541,6 +608,15 @@ class SigningUpload(CustomUpload):
541 if os.path.exists(x509_filename):608 if os.path.exists(x509_filename):
542 os.chmod(x509_filename, 0o644)609 os.chmod(x509_filename, 0o644)
543610
611 signing_key_type = getattr(SigningKeyType, key_type.upper())
612 try:
613 self.injectIntoSigningService(
614 signing_key_type, pem_filename, x509_filename)
615 except SigningKeyConflict:
616 os.unlink(pem_filename)
617 os.unlink(x509_filename)
618 raise
619
544 def generateKmodKeys(self):620 def generateKmodKeys(self):
545 """Generate new Kernel Signing Keys for this archive."""621 """Generate new Kernel Signing Keys for this archive."""
546 config = self.generateOpensslConfig("Kmod", self.openssl_config_kmod)622 config = self.generateOpensslConfig("Kmod", self.openssl_config_kmod)
diff --git a/lib/lp/archivepublisher/tests/test_signing.py b/lib/lp/archivepublisher/tests/test_signing.py
index 87cf391..e51bb25 100644
--- a/lib/lp/archivepublisher/tests/test_signing.py
+++ b/lib/lp/archivepublisher/tests/test_signing.py
@@ -7,14 +7,19 @@ from __future__ import absolute_import, print_function, unicode_literals
77
8__metaclass__ = type8__metaclass__ = type
99
10from datetime import datetime
10import os11import os
11import re12import re
12import shutil13import shutil
13import stat14import stat
14import tarfile15import tarfile
1516
16from fixtures import MonkeyPatch17from fixtures import (
18 MockPatch,
19 MonkeyPatch,
20 )
17from mock import call21from mock import call
22from pytz import utc
18import scandir23import scandir
19from testtools.matchers import (24from testtools.matchers import (
20 Contains,25 Contains,
@@ -42,12 +47,15 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import (
42 )47 )
43from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet48from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
44from lp.archivepublisher.signing import (49from lp.archivepublisher.signing import (
50 PUBLISHER_SIGNING_SERVICE_INJECTS_KEYS,
45 PUBLISHER_USES_SIGNING_SERVICE,51 PUBLISHER_USES_SIGNING_SERVICE,
52 SigningKeyConflict,
46 SigningUpload,53 SigningUpload,
47 UefiUpload,54 UefiUpload,
48 )55 )
49from lp.archivepublisher.tests.test_run_parts import RunPartsMixin56from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
50from lp.services.features.testing import FeatureFixture57from lp.services.features.testing import FeatureFixture
58from lp.services.log.logger import BufferLogger
51from lp.services.osutils import write_file59from lp.services.osutils import write_file
52from lp.services.signing.enums import SigningMode60from lp.services.signing.enums import SigningMode
53from lp.services.signing.proxy import SigningKeyType61from lp.services.signing.proxy import SigningKeyType
@@ -1851,7 +1859,8 @@ class TestSigningUploadWithSigningService(TestSigningHelpers):
18511859
1852 self.openArchive("test", "1.0", "amd64")1860 self.openArchive("test", "1.0", "amd64")
1853 for filename in filenames:1861 for filename in filenames:
1854 self.tarfile.add_file(filename, b"data - %s" % filename)1862 self.tarfile.add_file(
1863 filename, b"data - %s" % filename.encode("UTF-8"))
18551864
1856 self.tarfile.close()1865 self.tarfile.close()
1857 self.buffer.close()1866 self.buffer.close()
@@ -1965,7 +1974,8 @@ class TestSigningUploadWithSigningService(TestSigningHelpers):
19651974
1966 self.openArchive("test", "1.0", "amd64")1975 self.openArchive("test", "1.0", "amd64")
1967 for filename in filenames:1976 for filename in filenames:
1968 self.tarfile.add_file(filename, b"data - %s" % filename)1977 self.tarfile.add_file(
1978 filename, b"data - %s" % filename.encode("UTF-8"))
19691979
1970 self.tarfile.close()1980 self.tarfile.close()
1971 self.buffer.close()1981 self.buffer.close()
@@ -2009,3 +2019,101 @@ class TestSigningUploadWithSigningService(TestSigningHelpers):
2009 self.assertEqual(2019 self.assertEqual(
2010 [(os.path.join(upload.tmpdir_used, "1.0/empty.efi"),)],2020 [(os.path.join(upload.tmpdir_used, "1.0/empty.efi"),)],
2011 upload.signUefi.extract_args())2021 upload.signUefi.extract_args())
2022
2023 def test_fallback_injects_key(self):
2024 self.useFixture(FeatureFixture({PUBLISHER_USES_SIGNING_SERVICE: ''}))
2025 self.useFixture(FeatureFixture({
2026 PUBLISHER_SIGNING_SERVICE_INJECTS_KEYS: 'SIPL OPAL'}))
2027
2028 now = datetime.now()
2029 mock_datetime = self.useFixture(MockPatch(
2030 'lp.archivepublisher.signing.datetime')).mock
2031 mock_datetime.now = lambda: now
2032
2033 logger = BufferLogger()
2034 upload = SigningUpload(logger=logger)
2035
2036 # Setup PPA to ensure it auto-generates keys.
2037 self.setUpPPA()
2038
2039 filenames = ["1.0/empty.efi", "1.0/empty.opal"]
2040
2041 self.openArchive("test", "1.0", "amd64")
2042 for filename in filenames:
2043 self.tarfile.add_file(
2044 filename, b"data - %s" % filename.encode("UTF-8"))
2045 self.tarfile.close()
2046 self.buffer.close()
2047
2048 upload.process(self.archive, self.path, self.suite)
2049 self.assertTrue(upload.autokey)
2050
2051 # Read the key file content
2052 with open(upload.opal_pem, 'rb') as fd:
2053 private_key = fd.read()
2054 with open(upload.opal_x509, 'rb') as fd:
2055 public_key = fd.read()
2056
2057 # Check if we called lp-signing's /inject endpoint correctly
2058 self.assertEqual(1, self.signing_service_client.inject.call_count)
2059 self.assertEqual(
2060 (SigningKeyType.OPAL, private_key, public_key,
2061 u"OPAL key for %s" % self.archive.reference,
2062 now.replace(tzinfo=utc)),
2063 self.signing_service_client.inject.call_args[0])
2064
2065 log_content = logger.content.as_text()
2066 self.assertIn(
2067 "INFO Injecting key_type OPAL for archive %s into signing "
2068 "service" % (self.archive.name),
2069 log_content)
2070
2071 self.assertIn(
2072 "INFO Skipping injection for key type UEFI: "
2073 "not in [u'SIPL', u'OPAL']",
2074 log_content)
2075
2076 def test_fallback_skips_key_injection_for_existing_keys(self):
2077 self.useFixture(FeatureFixture({PUBLISHER_USES_SIGNING_SERVICE: ''}))
2078 self.useFixture(FeatureFixture({
2079 PUBLISHER_SIGNING_SERVICE_INJECTS_KEYS: 'UEFI'}))
2080
2081 now = datetime.now()
2082 mock_datetime = self.useFixture(MockPatch(
2083 'lp.archivepublisher.signing.datetime')).mock
2084 mock_datetime.now = lambda: now
2085
2086 # Setup PPA to ensure it auto-generates keys.
2087 self.setUpPPA()
2088
2089 signing_key = self.factory.makeSigningKey(key_type=SigningKeyType.UEFI)
2090 self.factory.makeArchiveSigningKey(
2091 archive=self.archive, signing_key=signing_key)
2092
2093 logger = BufferLogger()
2094 upload = SigningUpload(logger=logger)
2095
2096 filenames = ["1.0/empty.efi"]
2097
2098 self.openArchive("test", "1.0", "amd64")
2099 for filename in filenames:
2100 self.tarfile.add_file(
2101 filename, b"data - %s" % filename.encode("UTF-8"))
2102 self.tarfile.close()
2103 self.buffer.close()
2104
2105 self.assertRaises(SigningKeyConflict,
2106 upload.process, self.archive, self.path, self.suite)
2107 self.assertTrue(upload.autokey)
2108
2109 # Make sure we deleted the locally generated keys.
2110 self.assertFalse(os.path.exists(upload.uefi_cert))
2111 self.assertFalse(os.path.exists(upload.uefi_key))
2112
2113 # Make sure we didn't call lp-signing's /inject endpoint
2114 self.assertEqual(0, self.signing_service_client.inject.call_count)
2115 log_content = logger.content.as_text()
2116 self.assertIn(
2117 "INFO Skipping injection for key type %s: archive "
2118 "already has a key on lp-signing." % SigningKeyType.UEFI,
2119 log_content)
diff --git a/lib/lp/services/signing/interfaces/signingkey.py b/lib/lp/services/signing/interfaces/signingkey.py
index 587bbb0..eccf2a0 100644
--- a/lib/lp/services/signing/interfaces/signingkey.py
+++ b/lib/lp/services/signing/interfaces/signingkey.py
@@ -117,10 +117,13 @@ class IArchiveSigningKeySet(Interface):
117 False if it was updated).117 False if it was updated).
118 """118 """
119119
120 def getSigningKey(key_type, archive, distro_series):120 def getSigningKey(key_type, archive, distro_series, exact_match=False):
121 """Get the most suitable key for a given archive / distro series121 """Get the most suitable key for a given archive / distro series
122 pair.122 pair.
123123
124 :param exact_match: If True, returns the ArchiveSigningKey matching
125 exactly the given key_type, archive and
126 distro_series. If False, gets the best match.
124 :return: The most suitable key127 :return: The most suitable key
125 """128 """
126129
diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py
index 0d5e137..0479b61 100644
--- a/lib/lp/services/signing/model/signingkey.py
+++ b/lib/lp/services/signing/model/signingkey.py
@@ -167,7 +167,8 @@ class ArchiveSigningKeySet:
167 return obj167 return obj
168168
169 @classmethod169 @classmethod
170 def getSigningKey(cls, key_type, archive, distro_series):170 def getSigningKey(cls, key_type, archive, distro_series,
171 exact_match=False):
171 store = IStore(ArchiveSigningKey)172 store = IStore(ArchiveSigningKey)
172 # Gets all the keys of the given key_type available for the archive173 # Gets all the keys of the given key_type available for the archive
173 rs = store.find(ArchiveSigningKey,174 rs = store.find(ArchiveSigningKey,
@@ -176,6 +177,10 @@ class ArchiveSigningKeySet:
176 ArchiveSigningKey.key_type == key_type,177 ArchiveSigningKey.key_type == key_type,
177 ArchiveSigningKey.archive == archive)178 ArchiveSigningKey.archive == archive)
178179
180 if exact_match:
181 rs = rs.find(
182 ArchiveSigningKey.earliest_distro_series == distro_series)
183
179 # prefetch related signing keys to avoid extra queries.184 # prefetch related signing keys to avoid extra queries.
180 signing_keys = store.find(SigningKey, [185 signing_keys = store.find(SigningKey, [
181 SigningKey.id.is_in([i.signing_key_id for i in rs])])186 SigningKey.id.is_in([i.signing_key_id for i in rs])])
diff --git a/lib/lp/services/signing/tests/helpers.py b/lib/lp/services/signing/tests/helpers.py
index e01730f..5675bc6 100644
--- a/lib/lp/services/signing/tests/helpers.py
+++ b/lib/lp/services/signing/tests/helpers.py
@@ -39,8 +39,12 @@ class SigningServiceClientFixture(fixtures.Fixture):
39 self.sign = mock.Mock()39 self.sign = mock.Mock()
40 self.sign.side_effect = self._sign40 self.sign.side_effect = self._sign
4141
42 self.inject = mock.Mock()
43 self.inject.side_effect = self._inject
44
42 self.generate_returns = []45 self.generate_returns = []
43 self.sign_returns = []46 self.sign_returns = []
47 self.inject_returns = []
4448
45 def _generate(self, key_type, description):49 def _generate(self, key_type, description):
46 key = bytes(PrivateKey.generate().public_key)50 key = bytes(PrivateKey.generate().public_key)
@@ -59,6 +63,12 @@ class SigningServiceClientFixture(fixtures.Fixture):
59 self.sign_returns.append((key_type, data))63 self.sign_returns.append((key_type, data))
60 return data64 return data
6165
66 def _inject(self, key_type, private_key, public_key, description,
67 created_at):
68 data = {'fingerprint': text_type(self.factory.getUniqueHexString(40))}
69 self.inject_returns.append(data)
70 return data
71
62 def _setUp(self):72 def _setUp(self):
63 self.useFixture(ZopeUtilityFixture(self, ISigningServiceClient))73 self.useFixture(ZopeUtilityFixture(self, ISigningServiceClient))
6474
diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py
index 3610303..8613dd7 100644
--- a/lib/lp/services/signing/tests/test_signingkey.py
+++ b/lib/lp/services/signing/tests/test_signingkey.py
@@ -244,6 +244,53 @@ class TestArchiveSigningKey(TestCaseWithFactory):
244 arch_kmod_key.signing_key,244 arch_kmod_key.signing_key,
245 arch_signing_key_set.getSigningKey(KMOD, archive, distro_series))245 arch_signing_key_set.getSigningKey(KMOD, archive, distro_series))
246246
247 def test_get_signing_key_exact_match(self):
248 UEFI = SigningKeyType.UEFI
249 KMOD = SigningKeyType.KMOD
250
251 archive = self.factory.makeArchive()
252 distro_series1 = archive.distribution.series[0]
253 distro_series2 = archive.distribution.series[1]
254 uefi_key = self.factory.makeSigningKey(
255 key_type=SigningKeyType.UEFI)
256 kmod_key = self.factory.makeSigningKey(
257 key_type=SigningKeyType.KMOD)
258
259 arch_signing_key_set = getUtility(IArchiveSigningKeySet)
260
261 # Create a key for the first distro series
262 series1_uefi_key = arch_signing_key_set.create(
263 archive, distro_series1, uefi_key)
264
265 # Create a key for the archive
266 arch_kmod_key = arch_signing_key_set.create(
267 archive, None, kmod_key)
268
269 # Should get the UEFI key for distro_series1
270 self.assertEqual(
271 series1_uefi_key.signing_key,
272 arch_signing_key_set.getSigningKey(
273 UEFI, archive, distro_series1, exact_match=True)
274 )
275 # Should get the archive's KMOD key.
276 self.assertEqual(
277 arch_kmod_key.signing_key,
278 arch_signing_key_set.getSigningKey(
279 KMOD, archive, None, exact_match=True)
280 )
281 # distro_series1 has no KMOD key.
282 self.assertEqual(
283 None,
284 arch_signing_key_set.getSigningKey(
285 KMOD, archive, distro_series1, exact_match=True)
286 )
287 # distro_series2 has no key at all.
288 self.assertEqual(
289 None,
290 arch_signing_key_set.getSigningKey(
291 KMOD, archive, distro_series2, exact_match=True)
292 )
293
247 def test_get_signing_keys_with_distro_series_configured(self):294 def test_get_signing_keys_with_distro_series_configured(self):
248 UEFI = SigningKeyType.UEFI295 UEFI = SigningKeyType.UEFI
249 KMOD = SigningKeyType.KMOD296 KMOD = SigningKeyType.KMOD