Merge ~pappacena/launchpad:unrevert-lp-signing-integration into launchpad:master
- Git
- lp:~pappacena/launchpad
- unrevert-lp-signing-integration
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | bc4d26955af50bf15869baf8599a80b9323d8ce8 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:unrevert-lp-signing-integration |
Merge into: | launchpad:master |
Diff against target: |
2871 lines (+2098/-98) 23 files modified
configs/development/launchpad-lazr.conf (+5/-0) lib/lp/archivepublisher/archivegpgsigningkey.py (+4/-13) lib/lp/archivepublisher/signing.py (+206/-30) lib/lp/archivepublisher/tests/test_publisher.py (+4/-2) lib/lp/archivepublisher/tests/test_signing.py (+550/-45) lib/lp/services/compat.py (+7/-0) lib/lp/services/config/schema-lazr.conf (+12/-6) lib/lp/services/configure.zcml (+2/-1) lib/lp/services/features/flags.py (+7/-1) lib/lp/services/signing/__init__.py (+0/-0) lib/lp/services/signing/configure.zcml (+27/-0) lib/lp/services/signing/enums.py (+65/-0) lib/lp/services/signing/interfaces/__init__.py (+0/-0) lib/lp/services/signing/interfaces/signingkey.py (+126/-0) lib/lp/services/signing/interfaces/signingserviceclient.py (+47/-0) lib/lp/services/signing/model/__init__.py (+0/-0) lib/lp/services/signing/model/signingkey.py (+196/-0) lib/lp/services/signing/proxy.py (+182/-0) lib/lp/services/signing/tests/__init__.py (+0/-0) lib/lp/services/signing/tests/helpers.py (+67/-0) lib/lp/services/signing/tests/test_proxy.py (+318/-0) lib/lp/services/signing/tests/test_signingkey.py (+244/-0) lib/lp/testing/factory.py (+29/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+381927@code.launchpad.net |
Commit message
[HOLD] Adding back the lp-signing integration, reverted due to the fact that we didn't deploy yet the database changes.
Description of the change
We should wait until https:/
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/configs/development/launchpad-lazr.conf b/configs/development/launchpad-lazr.conf |
2 | index a0b8f0e..7b01979 100644 |
3 | --- a/configs/development/launchpad-lazr.conf |
4 | +++ b/configs/development/launchpad-lazr.conf |
5 | @@ -182,6 +182,11 @@ tools_source: deb http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu %(s |
6 | global_suggestions_enabled: True |
7 | generate_templates: True |
8 | |
9 | +[signing] |
10 | +signing_endpoint = http://signing.launchpad.test:8000 |
11 | +client_private_key = O73bJzd3hybyBxUKk0FaR6K9CbbmxBYkw6vCrIWZkSY= |
12 | +client_public_key = xEtwSS7kdGmo0ElcN2fR/mcHS0A42zhYbo/+5KV4xRs= |
13 | + |
14 | [profiling] |
15 | profiling_allowed: True |
16 | |
17 | diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py |
18 | index 2b14365..7439a32 100644 |
19 | --- a/lib/lp/archivepublisher/archivegpgsigningkey.py |
20 | +++ b/lib/lp/archivepublisher/archivegpgsigningkey.py |
21 | @@ -1,4 +1,4 @@ |
22 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
23 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
24 | # GNU Affero General Public License version 3 (see the file LICENSE). |
25 | |
26 | """ArchiveGPGSigningKey implementation.""" |
27 | @@ -8,17 +8,12 @@ __metaclass__ = type |
28 | __all__ = [ |
29 | 'ArchiveGPGSigningKey', |
30 | 'SignableArchive', |
31 | - 'SigningMode', |
32 | ] |
33 | |
34 | |
35 | import os |
36 | |
37 | import gpgme |
38 | -from lazr.enum import ( |
39 | - EnumeratedType, |
40 | - Item, |
41 | - ) |
42 | from twisted.internet.threads import deferToThread |
43 | from zope.component import getUtility |
44 | from zope.interface import implementer |
45 | @@ -43,13 +38,7 @@ from lp.services.config import config |
46 | from lp.services.gpg.interfaces import IGPGHandler |
47 | from lp.services.osutils import remove_if_exists |
48 | from lp.services.propertycache import get_property_cache |
49 | - |
50 | - |
51 | -class SigningMode(EnumeratedType): |
52 | - """Archive file signing mode.""" |
53 | - |
54 | - DETACHED = Item("Detached signature") |
55 | - CLEAR = Item("Cleartext signature") |
56 | +from lp.services.signing.enums import SigningMode |
57 | |
58 | |
59 | @implementer(ISignableArchive) |
60 | @@ -100,6 +89,8 @@ class SignableArchive: |
61 | |
62 | output_paths = [] |
63 | for input_path, output_path, mode, suite in signatures: |
64 | + if mode not in {SigningMode.DETACHED, SigningMode.CLEAR}: |
65 | + raise ValueError('Invalid signature mode for GPG: %s' % mode) |
66 | if self.archive.signing_key is not None: |
67 | with open(input_path) as input_file: |
68 | input_content = input_file.read() |
69 | diff --git a/lib/lp/archivepublisher/signing.py b/lib/lp/archivepublisher/signing.py |
70 | index b685a61..e59ddc6 100644 |
71 | --- a/lib/lp/archivepublisher/signing.py |
72 | +++ b/lib/lp/archivepublisher/signing.py |
73 | @@ -1,4 +1,4 @@ |
74 | -# Copyright 2012-2018 Canonical Ltd. This software is licensed under the |
75 | +# Copyright 2012-2020 Canonical Ltd. This software is licensed under the |
76 | # GNU Affero General Public License version 3 (see the file LICENSE). |
77 | |
78 | """The processing of Signing tarballs. |
79 | @@ -18,6 +18,7 @@ __all__ = [ |
80 | "UefiUpload", |
81 | ] |
82 | |
83 | +from functools import partial |
84 | import os |
85 | import shutil |
86 | import stat |
87 | @@ -27,13 +28,22 @@ import tempfile |
88 | import textwrap |
89 | |
90 | import scandir |
91 | +from zope.component import getUtility |
92 | |
93 | from lp.archivepublisher.config import getPubConfig |
94 | from lp.archivepublisher.customupload import CustomUpload |
95 | +from lp.registry.interfaces.distroseries import IDistroSeriesSet |
96 | +from lp.services.features import getFeatureFlag |
97 | from lp.services.osutils import remove_if_exists |
98 | +from lp.services.signing.enums import SigningKeyType |
99 | +from lp.services.signing.interfaces.signingkey import IArchiveSigningKeySet |
100 | from lp.soyuz.interfaces.queue import CustomUploadError |
101 | |
102 | |
103 | +PUBLISHER_USES_SIGNING_SERVICE = ( |
104 | + 'archivepublisher.signing_service.enabled') |
105 | + |
106 | + |
107 | class SigningUploadPackError(CustomUploadError): |
108 | def __init__(self, tarfile_path, exc): |
109 | message = "Problem building tarball '%s': %s" % ( |
110 | @@ -41,6 +51,14 @@ class SigningUploadPackError(CustomUploadError): |
111 | CustomUploadError.__init__(self, message) |
112 | |
113 | |
114 | +class NoSigningKeyError(Exception): |
115 | + pass |
116 | + |
117 | + |
118 | +class SigningServiceError(Exception): |
119 | + pass |
120 | + |
121 | + |
122 | class SigningUpload(CustomUpload): |
123 | """Signing custom upload. |
124 | |
125 | @@ -65,6 +83,16 @@ class SigningUpload(CustomUpload): |
126 | Signing keys may be installed in the "signingroot" directory specified in |
127 | publisher configuration. In this directory, the private key is |
128 | "uefi.key" and the certificate is "uefi.crt". |
129 | + |
130 | + This class is already prepared to use signing service. There are |
131 | + basically two places interacting with it: |
132 | + - findSigningHandlers(), that provides a handler to call signing |
133 | + service to sign each file (together with a fallback handler, |
134 | + that signs the file locally). |
135 | + |
136 | + - copyPublishedPublicKeys(), that accepts both ways of saving public |
137 | + keys: by copying from local file system (old way) or saving the |
138 | + public key stored at signing service (new way). |
139 | """ |
140 | custom_type = "signing" |
141 | |
142 | @@ -105,6 +133,13 @@ class SigningUpload(CustomUpload): |
143 | |
144 | def setTargetDirectory(self, archive, tarfile_path, suite): |
145 | self.archive = archive |
146 | + |
147 | + if suite: |
148 | + self.distro_series, _ = getUtility(IDistroSeriesSet).fromSuite( |
149 | + self.archive.distribution, suite) |
150 | + else: |
151 | + self.distro_series = None |
152 | + |
153 | pubconf = getPubConfig(archive) |
154 | if pubconf.signingroot is None: |
155 | if self.logger is not None: |
156 | @@ -122,7 +157,7 @@ class SigningUpload(CustomUpload): |
157 | self.fit_cert = None |
158 | self.autokey = False |
159 | else: |
160 | - signing_for = suite.split('-')[0] |
161 | + signing_for = self.distro_series.name if self.distro_series else '' |
162 | self.uefi_key = self.getSeriesPath( |
163 | pubconf, "uefi.key", archive, signing_for) |
164 | self.uefi_cert = self.getSeriesPath( |
165 | @@ -165,26 +200,37 @@ class SigningUpload(CustomUpload): |
166 | self.archiveroot = pubconf.archiveroot |
167 | self.temproot = pubconf.temproot |
168 | |
169 | - self.public_keys = set() |
170 | + self.public_keys = {} |
171 | |
172 | - def publishPublicKey(self, key): |
173 | - """Record this key as having been used in this upload.""" |
174 | - self.public_keys.add(key) |
175 | + def publishPublicKey(self, key, content=None): |
176 | + """Record this key as having been used in this upload. |
177 | |
178 | - def copyPublishedPublicKeys(self): |
179 | - """Copy out published keys into the custom upload.""" |
180 | - keydir = os.path.join(self.tmpdir, self.version, "control") |
181 | - if not os.path.exists(keydir): |
182 | - os.makedirs(keydir) |
183 | - for key in self.public_keys: |
184 | - # Ensure we only emit files which are world readable. |
185 | + :param key: Key file name |
186 | + :param content: Key file content (if None, try to read it from local |
187 | + filesystem) |
188 | + """ |
189 | + if content is not None: |
190 | + self.public_keys[key] = content |
191 | + elif key not in self.public_keys: |
192 | + # Ensure we only emit files which are world-readable. |
193 | if stat.S_IMODE(os.stat(key).st_mode) & stat.S_IROTH: |
194 | - shutil.copy(key, os.path.join(keydir, os.path.basename(key))) |
195 | + with open(key, "rb") as f: |
196 | + self.public_keys[key] = f.read() |
197 | else: |
198 | if self.logger is not None: |
199 | self.logger.warning( |
200 | "%s: public key not world readable" % key) |
201 | |
202 | + def copyPublishedPublicKeys(self): |
203 | + """Copy out published keys into the custom upload.""" |
204 | + keydir = os.path.join(self.tmpdir, self.version, "control") |
205 | + if not os.path.exists(keydir): |
206 | + os.makedirs(keydir) |
207 | + for filename, content in self.public_keys.items(): |
208 | + file_path = os.path.join(keydir, os.path.basename(filename)) |
209 | + with open(file_path, 'wb') as fd: |
210 | + fd.write(content) |
211 | + |
212 | def setSigningOptions(self): |
213 | """Find and extract raw-signing options from the tarball.""" |
214 | self.signing_options = {} |
215 | @@ -219,22 +265,145 @@ class SigningUpload(CustomUpload): |
216 | |
217 | def findSigningHandlers(self): |
218 | """Find all the signable files in an extracted tarball.""" |
219 | + use_signing_service = bool( |
220 | + getFeatureFlag(PUBLISHER_USES_SIGNING_SERVICE)) |
221 | + |
222 | + fallback_handlers = { |
223 | + SigningKeyType.UEFI: self.signUefi, |
224 | + SigningKeyType.KMOD: self.signKmod, |
225 | + SigningKeyType.OPAL: self.signOpal, |
226 | + SigningKeyType.SIPL: self.signSipl, |
227 | + SigningKeyType.FIT: self.signFit, |
228 | + } |
229 | + |
230 | for dirpath, dirnames, filenames in scandir.walk(self.tmpdir): |
231 | for filename in filenames: |
232 | + file_path = os.path.join(dirpath, filename) |
233 | if filename.endswith(".efi"): |
234 | - yield (os.path.join(dirpath, filename), self.signUefi) |
235 | + key_type = SigningKeyType.UEFI |
236 | elif filename.endswith(".ko"): |
237 | - yield (os.path.join(dirpath, filename), self.signKmod) |
238 | + key_type = SigningKeyType.KMOD |
239 | elif filename.endswith(".opal"): |
240 | - yield (os.path.join(dirpath, filename), self.signOpal) |
241 | + key_type = SigningKeyType.OPAL |
242 | elif filename.endswith(".sipl"): |
243 | - yield (os.path.join(dirpath, filename), self.signSipl) |
244 | + key_type = SigningKeyType.SIPL |
245 | elif filename.endswith(".fit"): |
246 | - yield (os.path.join(dirpath, filename), self.signFit) |
247 | + key_type = SigningKeyType.FIT |
248 | + else: |
249 | + continue |
250 | + |
251 | + if use_signing_service: |
252 | + key = getUtility(IArchiveSigningKeySet).getSigningKey( |
253 | + key_type, self.archive, self.distro_series) |
254 | + handler = partial( |
255 | + self.signUsingSigningService, key_type, key) |
256 | + fallback_handler = partial( |
257 | + self.signUsingLocalKey, key_type, |
258 | + fallback_handlers.get(key_type)) |
259 | + yield file_path, handler, fallback_handler |
260 | + else: |
261 | + yield file_path, fallback_handlers.get(key_type), None |
262 | + |
263 | + def signUsingLocalKey(self, key_type, handler, filename): |
264 | + """Sign the given filename using using handler if the local |
265 | + key files exists. If the local key files does not exist, raises |
266 | + IOError. |
267 | + |
268 | + Note that this method should only be used as a fallback to signing |
269 | + service, since it will not try to generate local keys. |
270 | + |
271 | + :param key_type: One of the SigningKeyType items. |
272 | + :param handler: One of the local signing handlers (self.signUefi, |
273 | + self.signKmod, etc). |
274 | + :param filename: The filename to be signed. |
275 | + """ |
276 | + |
277 | + if not self.keyFilesExist(key_type): |
278 | + raise IOError( |
279 | + "Could not fallback to local signing keys: the key files " |
280 | + "were not found.") |
281 | + return handler(filename) |
282 | + |
283 | + def keyFilesExist(self, key_type): |
284 | + """Checks if all needed key files exists in the local filesystem |
285 | + for the given key type. |
286 | + """ |
287 | + fallback_keys = { |
288 | + SigningKeyType.UEFI: [self.uefi_cert, self.uefi_key], |
289 | + SigningKeyType.KMOD: [self.kmod_pem, self.kmod_x509], |
290 | + SigningKeyType.OPAL: [self.opal_pem, self.opal_x509], |
291 | + SigningKeyType.SIPL: [self.sipl_pem, self.sipl_x509], |
292 | + SigningKeyType.FIT: [self.fit_cert, self.fit_key], |
293 | + } |
294 | + # If we are missing local key files, do not proceed. |
295 | + key_files = [i for i in fallback_keys[key_type] if i] |
296 | + return all(os.path.exists(key_file) for key_file in key_files) |
297 | + |
298 | + def signUsingSigningService(self, key_type, signing_key, filename): |
299 | + """Sign the given filename using a certain key hosted on signing |
300 | + service, writes the signed content back to the filesystem and |
301 | + publishes the public key to self.public_keys. |
302 | + |
303 | + If the given key is None and self.autokey is set to True, this method |
304 | + generates a key on signing service and associates it with the current |
305 | + archive. |
306 | + |
307 | + :param key_type: One of the SigningKeyType enum items |
308 | + :param signing_key: The SigningKey to be used (or None, |
309 | + to autogenerate a key if possible). |
310 | + :param filename: The filename to be signed. |
311 | + :return: Boolean. True if signed, or raises SigningServiceError |
312 | + on failure. |
313 | + """ |
314 | + if signing_key is None: |
315 | + if not self.autokey: |
316 | + raise NoSigningKeyError("No signing key for %s" % filename) |
317 | + description = ( |
318 | + u"%s key for %s" % (key_type.name, self.archive.reference)) |
319 | + try: |
320 | + signing_key = getUtility(IArchiveSigningKeySet).generate( |
321 | + key_type, self.archive, description=description |
322 | + ).signing_key |
323 | + except Exception as e: |
324 | + if self.logger: |
325 | + self.logger.exception( |
326 | + "Error generating signing key for %s: %s %s" % |
327 | + (self.archive.reference, e.__class__.__name__, e)) |
328 | + raise SigningServiceError( |
329 | + "Could not generate key %s: %s" % (key_type, e)) |
330 | + |
331 | + with open(filename, "rb") as fd: |
332 | + content = fd.read() |
333 | + |
334 | + try: |
335 | + signed_content = signing_key.sign( |
336 | + content, message_name=os.path.basename(filename)) |
337 | + except Exception as e: |
338 | + if self.logger: |
339 | + self.logger.exception( |
340 | + "Error signing %s on signing service: %s %s" % |
341 | + (filename, e.__class__.__name__, e)) |
342 | + raise SigningServiceError( |
343 | + "Could not sign message with key %s: %s" % (signing_key, e)) |
344 | + |
345 | + if key_type in (SigningKeyType.UEFI, SigningKeyType.FIT): |
346 | + file_suffix = ".signed" |
347 | + public_key_suffix = ".crt" |
348 | + else: |
349 | + file_suffix = ".sig" |
350 | + public_key_suffix = ".x509" |
351 | + |
352 | + signed_filename = filename + file_suffix |
353 | + public_key_filename = key_type.name.lower() + public_key_suffix |
354 | + |
355 | + with open(signed_filename, 'wb') as fd: |
356 | + fd.write(signed_content) |
357 | + |
358 | + self.publishPublicKey(public_key_filename, signing_key.public_key) |
359 | + return True |
360 | |
361 | def getKeys(self, which, generate, *keynames): |
362 | """Validate and return the uefi key and cert for encryption.""" |
363 | - |
364 | if self.autokey: |
365 | for keyfile in keynames: |
366 | if keyfile and not os.path.exists(keyfile): |
367 | @@ -299,7 +468,7 @@ class SigningUpload(CustomUpload): |
368 | return |
369 | self.publishPublicKey(cert) |
370 | cmdl = ["sbsign", "--key", key, "--cert", cert, image] |
371 | - return self.callLog("UEFI signing", cmdl) |
372 | + return self.callLog("UEFI signing", cmdl) == 0 |
373 | |
374 | openssl_config_base = textwrap.dedent("""\ |
375 | [ req ] |
376 | @@ -387,7 +556,7 @@ class SigningUpload(CustomUpload): |
377 | return |
378 | self.publishPublicKey(cert) |
379 | cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"] |
380 | - return self.callLog("Kmod signing", cmdl) |
381 | + return self.callLog("Kmod signing", cmdl) == 0 |
382 | |
383 | def generateOpalKeys(self): |
384 | """Generate new Opal Signing Keys for this archive.""" |
385 | @@ -403,7 +572,7 @@ class SigningUpload(CustomUpload): |
386 | return |
387 | self.publishPublicKey(cert) |
388 | cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"] |
389 | - return self.callLog("Opal signing", cmdl) |
390 | + return self.callLog("Opal signing", cmdl) == 0 |
391 | |
392 | def generateSiplKeys(self): |
393 | """Generate new Sipl Signing Keys for this archive.""" |
394 | @@ -419,7 +588,7 @@ class SigningUpload(CustomUpload): |
395 | return |
396 | self.publishPublicKey(cert) |
397 | cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"] |
398 | - return self.callLog("SIPL signing", cmdl) |
399 | + return self.callLog("SIPL signing", cmdl) == 0 |
400 | |
401 | def generateFitKeys(self): |
402 | """Generate new FIT Keys for this archive.""" |
403 | @@ -439,7 +608,7 @@ class SigningUpload(CustomUpload): |
404 | shutil.copy(image, image_signed) |
405 | cmdl = ["mkimage", "-F", "-k", os.path.dirname(key), "-r", |
406 | image_signed] |
407 | - return self.callLog("FIT signing", cmdl) |
408 | + return self.callLog("FIT signing", cmdl) == 0 |
409 | |
410 | def convertToTarball(self): |
411 | """Convert unpacked output to signing tarball.""" |
412 | @@ -467,10 +636,18 @@ class SigningUpload(CustomUpload): |
413 | """ |
414 | super(SigningUpload, self).extract() |
415 | self.setSigningOptions() |
416 | - filehandlers = list(self.findSigningHandlers()) |
417 | - for (filename, handler) in filehandlers: |
418 | - if (handler(filename) == 0 and |
419 | - 'signed-only' in self.signing_options): |
420 | + for filename, handler, fallback_handler in self.findSigningHandlers(): |
421 | + try: |
422 | + was_signed = handler(filename) |
423 | + except (NoSigningKeyError, SigningServiceError) as e: |
424 | + if fallback_handler is not None and self.logger: |
425 | + self.logger.warning( |
426 | + "Signing service will try to fallback to local key. " |
427 | + "Reason: %s (%s)" % (e.__class__.__name__, e)) |
428 | + was_signed = False |
429 | + if not was_signed and fallback_handler is not None: |
430 | + was_signed = fallback_handler(filename) |
431 | + if was_signed and 'signed-only' in self.signing_options: |
432 | os.unlink(filename) |
433 | |
434 | # Copy out the public keys where they were used. |
435 | @@ -514,5 +691,4 @@ class UefiUpload(SigningUpload): |
436 | packages are converted to the new form and location. |
437 | """ |
438 | custom_type = "uefi" |
439 | - |
440 | dists_directory = "uefi" |
441 | diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py |
442 | index d94fd34..1a2b664 100644 |
443 | --- a/lib/lp/archivepublisher/tests/test_publisher.py |
444 | +++ b/lib/lp/archivepublisher/tests/test_publisher.py |
445 | @@ -1,4 +1,4 @@ |
446 | -# Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
447 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
448 | # GNU Affero General Public License version 3 (see the file LICENSE). |
449 | |
450 | """Tests for publisher class.""" |
451 | @@ -32,11 +32,13 @@ import time |
452 | |
453 | from debian.deb822 import Release |
454 | from fixtures import MonkeyPatch |
455 | + |
456 | + |
457 | try: |
458 | import lzma |
459 | except ImportError: |
460 | from backports import lzma |
461 | -import mock |
462 | +from lp.services.compat import mock |
463 | import pytz |
464 | import scandir |
465 | from testscenarios import ( |
466 | diff --git a/lib/lp/archivepublisher/tests/test_signing.py b/lib/lp/archivepublisher/tests/test_signing.py |
467 | index efe4ae6..8295aef 100644 |
468 | --- a/lib/lp/archivepublisher/tests/test_signing.py |
469 | +++ b/lib/lp/archivepublisher/tests/test_signing.py |
470 | @@ -1,4 +1,4 @@ |
471 | -# Copyright 2012-2019 Canonical Ltd. This software is licensed under the |
472 | +# Copyright 2012-2020 Canonical Ltd. This software is licensed under the |
473 | # GNU Affero General Public License version 3 (see the file LICENSE). |
474 | |
475 | """Test UEFI custom uploads.""" |
476 | @@ -9,10 +9,12 @@ __metaclass__ = type |
477 | |
478 | import os |
479 | import re |
480 | +import shutil |
481 | import stat |
482 | import tarfile |
483 | |
484 | from fixtures import MonkeyPatch |
485 | +from mock import call |
486 | import scandir |
487 | from testtools.matchers import ( |
488 | Contains, |
489 | @@ -21,6 +23,7 @@ from testtools.matchers import ( |
490 | Matcher, |
491 | MatchesAll, |
492 | MatchesDict, |
493 | + MatchesStructure, |
494 | Mismatch, |
495 | Not, |
496 | StartsWith, |
497 | @@ -39,11 +42,16 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import ( |
498 | ) |
499 | from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
500 | from lp.archivepublisher.signing import ( |
501 | + PUBLISHER_USES_SIGNING_SERVICE, |
502 | SigningUpload, |
503 | UefiUpload, |
504 | ) |
505 | from lp.archivepublisher.tests.test_run_parts import RunPartsMixin |
506 | +from lp.services.features.testing import FeatureFixture |
507 | from lp.services.osutils import write_file |
508 | +from lp.services.signing.enums import SigningMode |
509 | +from lp.services.signing.proxy import SigningKeyType |
510 | +from lp.services.signing.tests.helpers import SigningServiceClientFixture |
511 | from lp.services.tarfile_helpers import LaunchpadWriteTarFile |
512 | from lp.soyuz.enums import ArchivePurpose |
513 | from lp.testing import TestCaseWithFactory |
514 | @@ -184,7 +192,9 @@ class TestSigningHelpers(TestCaseWithFactory): |
515 | distribution=self.distro, purpose=ArchivePurpose.PRIMARY) |
516 | self.signing_dir = os.path.join( |
517 | self.temp_dir, self.distro.name + "-signing") |
518 | - self.suite = "distroseries" |
519 | + self.distroseries = self.factory.makeDistroSeries( |
520 | + distribution=self.distro) |
521 | + self.suite = self.distroseries.name |
522 | pubconf = getPubConfig(self.archive) |
523 | if not os.path.exists(pubconf.temproot): |
524 | os.makedirs(pubconf.temproot) |
525 | @@ -267,7 +277,7 @@ class TestSigningHelpers(TestCaseWithFactory): |
526 | return os.path.join(pubconf.archiveroot, "dists", self.suite, "main") |
527 | |
528 | |
529 | -class TestSigning(RunPartsMixin, TestSigningHelpers): |
530 | +class TestLocalSigningUpload(RunPartsMixin, TestSigningHelpers): |
531 | |
532 | def getSignedPath(self, loader_type, arch): |
533 | return os.path.join(self.getDistsPath(), "signed", |
534 | @@ -604,7 +614,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
535 | upload = SigningUpload() |
536 | upload.generateUefiKeys = FakeMethod() |
537 | upload.setTargetDirectory( |
538 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
539 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
540 | upload.signUefi('t.efi') |
541 | self.assertEqual(1, fake_call.call_count) |
542 | # Assert command form. |
543 | @@ -624,7 +634,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
544 | upload = SigningUpload() |
545 | upload.generateUefiKeys = FakeMethod() |
546 | upload.setTargetDirectory( |
547 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
548 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
549 | upload.signUefi('t.efi') |
550 | self.assertEqual(0, fake_call.call_count) |
551 | self.assertEqual(0, upload.generateUefiKeys.call_count) |
552 | @@ -638,7 +648,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
553 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
554 | upload = SigningUpload() |
555 | upload.setTargetDirectory( |
556 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
557 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
558 | upload.generateUefiKeys() |
559 | self.assertEqual(1, fake_call.call_count) |
560 | # Assert the actual command matches. |
561 | @@ -662,7 +672,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
562 | upload = SigningUpload() |
563 | upload.generateFitKeys = FakeMethod() |
564 | upload.setTargetDirectory( |
565 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
566 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
567 | upload.signFit('t.fit') |
568 | # Confirm the copy was performed. |
569 | self.assertEqual(1, fake_copy.call_count) |
570 | @@ -687,7 +697,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
571 | upload = SigningUpload() |
572 | upload.generateFitKeys = FakeMethod() |
573 | upload.setTargetDirectory( |
574 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
575 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
576 | upload.signUefi('t.fit') |
577 | self.assertEqual(0, fake_call.call_count) |
578 | self.assertEqual(0, upload.generateFitKeys.call_count) |
579 | @@ -701,7 +711,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
580 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
581 | upload = SigningUpload() |
582 | upload.setTargetDirectory( |
583 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
584 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
585 | upload.generateFitKeys() |
586 | self.assertEqual(1, fake_call.call_count) |
587 | # Assert the actual command matches. |
588 | @@ -720,7 +730,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
589 | self.setUpPPA() |
590 | upload = SigningUpload() |
591 | upload.setTargetDirectory( |
592 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
593 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
594 | text = upload.generateOpensslConfig('Kmod', upload.openssl_config_kmod) |
595 | |
596 | id_re = re.compile(r'^# KMOD OpenSSL config\n') |
597 | @@ -743,7 +753,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
598 | upload = SigningUpload() |
599 | upload.generateKmodKeys = FakeMethod() |
600 | upload.setTargetDirectory( |
601 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
602 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
603 | upload.signKmod('t.ko') |
604 | self.assertEqual(1, fake_call.call_count) |
605 | # Assert command form. |
606 | @@ -764,7 +774,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
607 | upload = SigningUpload() |
608 | upload.generateKmodKeys = FakeMethod() |
609 | upload.setTargetDirectory( |
610 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
611 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
612 | upload.signKmod('t.ko') |
613 | self.assertEqual(0, fake_call.call_count) |
614 | self.assertEqual(0, upload.generateKmodKeys.call_count) |
615 | @@ -778,7 +788,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
616 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
617 | upload = SigningUpload() |
618 | upload.setTargetDirectory( |
619 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
620 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
621 | upload.generateKmodKeys() |
622 | self.assertEqual(2, fake_call.call_count) |
623 | # Assert the actual command matches. |
624 | @@ -806,7 +816,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
625 | self.setUpPPA() |
626 | upload = SigningUpload() |
627 | upload.setTargetDirectory( |
628 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
629 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
630 | text = upload.generateOpensslConfig('Opal', upload.openssl_config_opal) |
631 | |
632 | id_re = re.compile(r'^# OPAL OpenSSL config\n') |
633 | @@ -826,7 +836,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
634 | upload = SigningUpload() |
635 | upload.generateOpalKeys = FakeMethod() |
636 | upload.setTargetDirectory( |
637 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
638 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
639 | upload.signOpal('t.opal') |
640 | self.assertEqual(1, fake_call.call_count) |
641 | # Assert command form. |
642 | @@ -847,7 +857,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
643 | upload = SigningUpload() |
644 | upload.generateOpalKeys = FakeMethod() |
645 | upload.setTargetDirectory( |
646 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
647 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
648 | upload.signOpal('t.opal') |
649 | self.assertEqual(0, fake_call.call_count) |
650 | self.assertEqual(0, upload.generateOpalKeys.call_count) |
651 | @@ -861,7 +871,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
652 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
653 | upload = SigningUpload() |
654 | upload.setTargetDirectory( |
655 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
656 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
657 | upload.generateOpalKeys() |
658 | self.assertEqual(2, fake_call.call_count) |
659 | # Assert the actual command matches. |
660 | @@ -889,7 +899,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
661 | self.setUpPPA() |
662 | upload = SigningUpload() |
663 | upload.setTargetDirectory( |
664 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
665 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
666 | text = upload.generateOpensslConfig('SIPL', upload.openssl_config_sipl) |
667 | |
668 | id_re = re.compile(r'^# SIPL OpenSSL config\n') |
669 | @@ -909,7 +919,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
670 | upload = SigningUpload() |
671 | upload.generateSiplKeys = FakeMethod() |
672 | upload.setTargetDirectory( |
673 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
674 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
675 | upload.signSipl('t.sipl') |
676 | self.assertEqual(1, fake_call.call_count) |
677 | # Assert command form. |
678 | @@ -930,7 +940,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
679 | upload = SigningUpload() |
680 | upload.generateSiplKeys = FakeMethod() |
681 | upload.setTargetDirectory( |
682 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
683 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
684 | upload.signOpal('t.sipl') |
685 | self.assertEqual(0, fake_call.call_count) |
686 | self.assertEqual(0, upload.generateSiplKeys.call_count) |
687 | @@ -944,7 +954,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
688 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
689 | upload = SigningUpload() |
690 | upload.setTargetDirectory( |
691 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
692 | + self.archive, "test_1.0_amd64.tar.gz", self.suite) |
693 | upload.generateSiplKeys() |
694 | self.assertEqual(2, fake_call.call_count) |
695 | # Assert the actual command matches. |
696 | @@ -979,22 +989,20 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
697 | This should fall through to the first series, |
698 | as the second does not have keys. |
699 | """ |
700 | - self.suite = "nokeys-distroseries" |
701 | first_series = self.factory.makeDistroSeries( |
702 | self.distro, |
703 | name="existingkeys" |
704 | ) |
705 | - self.factory.makeDistroSeries( |
706 | - self.distro, |
707 | - name="nokeys" |
708 | - ) |
709 | + self.distroseries = self.factory.makeDistroSeries( |
710 | + self.distro, name="nokeys") |
711 | + self.suite = self.distroseries.name |
712 | # Each image in the tarball is signed. |
713 | self.setUpUefiKeys() |
714 | self.setUpUefiKeys(series=first_series) |
715 | self.openArchive("test", "1.0", "amd64") |
716 | self.tarfile.add_file("1.0/empty.efi", b"") |
717 | upload = self.process_emulate() |
718 | - expected_callers = [('UEFI signing', 1),] |
719 | + expected_callers = [('UEFI signing', 1)] |
720 | self.assertContentEqual(expected_callers, upload.callLog.caller_list()) |
721 | # Check the correct series name appears in the call arguments |
722 | self.assertIn( |
723 | @@ -1103,7 +1111,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
724 | upload = SigningUpload() |
725 | upload.callLog = FakeMethodCallLog(upload=upload) |
726 | upload.setTargetDirectory( |
727 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
728 | + self.archive, "test_1.0_amd64.tar.gz", "") |
729 | upload.signUefi(os.path.join(self.makeTemporaryDirectory(), 't.efi')) |
730 | self.assertEqual(0, upload.callLog.caller_count('UEFI keygen')) |
731 | self.assertFalse(os.path.exists(self.key)) |
732 | @@ -1120,7 +1128,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
733 | upload = SigningUpload() |
734 | upload.callLog = FakeMethodCallLog(upload=upload) |
735 | upload.setTargetDirectory( |
736 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
737 | + self.archive, "test_1.0_amd64.tar.gz", "") |
738 | upload.signUefi(os.path.join(self.makeTemporaryDirectory(), 't.efi')) |
739 | self.assertEqual(1, upload.callLog.caller_count('UEFI keygen')) |
740 | self.assertTrue(os.path.exists(self.key)) |
741 | @@ -1138,7 +1146,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
742 | upload = SigningUpload() |
743 | upload.callLog = FakeMethodCallLog(upload=upload) |
744 | upload.setTargetDirectory( |
745 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
746 | + self.archive, "test_1.0_amd64.tar.gz", "") |
747 | upload.signFit(os.path.join(self.makeTemporaryDirectory(), 'fit')) |
748 | self.assertEqual(0, upload.callLog.caller_count('FIT keygen')) |
749 | self.assertFalse(os.path.exists(self.fit_key)) |
750 | @@ -1157,7 +1165,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
751 | upload = SigningUpload() |
752 | upload.callLog = FakeMethodCallLog(upload=upload) |
753 | upload.setTargetDirectory( |
754 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
755 | + self.archive, "test_1.0_amd64.tar.gz", "") |
756 | upload.signFit(os.path.join(self.makeTemporaryDirectory(), 't.fit')) |
757 | self.assertEqual(1, upload.callLog.caller_count('FIT keygen')) |
758 | self.assertTrue(os.path.exists(self.fit_key)) |
759 | @@ -1175,7 +1183,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
760 | upload = SigningUpload() |
761 | upload.callLog = FakeMethodCallLog(upload=upload) |
762 | upload.setTargetDirectory( |
763 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
764 | + self.archive, "test_1.0_amd64.tar.gz", "") |
765 | upload.signKmod(os.path.join(self.makeTemporaryDirectory(), 't.ko')) |
766 | self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key')) |
767 | self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert')) |
768 | @@ -1193,7 +1201,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
769 | upload = SigningUpload() |
770 | upload.callLog = FakeMethodCallLog(upload=upload) |
771 | upload.setTargetDirectory( |
772 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
773 | + self.archive, "test_1.0_amd64.tar.gz", "") |
774 | upload.signKmod(os.path.join(self.makeTemporaryDirectory(), 't.ko')) |
775 | self.assertEqual(1, upload.callLog.caller_count('Kmod keygen key')) |
776 | self.assertEqual(1, upload.callLog.caller_count('Kmod keygen cert')) |
777 | @@ -1212,7 +1220,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
778 | upload = SigningUpload() |
779 | upload.callLog = FakeMethodCallLog(upload=upload) |
780 | upload.setTargetDirectory( |
781 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
782 | + self.archive, "test_1.0_amd64.tar.gz", "") |
783 | upload.signOpal(os.path.join(self.makeTemporaryDirectory(), 't.opal')) |
784 | self.assertEqual(0, upload.callLog.caller_count('Opal keygen key')) |
785 | self.assertEqual(0, upload.callLog.caller_count('Opal keygen cert')) |
786 | @@ -1230,7 +1238,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
787 | upload = SigningUpload() |
788 | upload.callLog = FakeMethodCallLog(upload=upload) |
789 | upload.setTargetDirectory( |
790 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
791 | + self.archive, "test_1.0_amd64.tar.gz", "") |
792 | upload.signOpal(os.path.join(self.makeTemporaryDirectory(), 't.opal')) |
793 | self.assertEqual(1, upload.callLog.caller_count('Opal keygen key')) |
794 | self.assertEqual(1, upload.callLog.caller_count('Opal keygen cert')) |
795 | @@ -1249,7 +1257,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
796 | upload = SigningUpload() |
797 | upload.callLog = FakeMethodCallLog(upload=upload) |
798 | upload.setTargetDirectory( |
799 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
800 | + self.archive, "test_1.0_amd64.tar.gz", "") |
801 | upload.signOpal(os.path.join(self.makeTemporaryDirectory(), 't.sipl')) |
802 | self.assertEqual(0, upload.callLog.caller_count('SIPL keygen key')) |
803 | self.assertEqual(0, upload.callLog.caller_count('SIPL keygen cert')) |
804 | @@ -1267,7 +1275,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
805 | upload = SigningUpload() |
806 | upload.callLog = FakeMethodCallLog(upload=upload) |
807 | upload.setTargetDirectory( |
808 | - self.archive, "test_1.0_amd64.tar.gz", "distroseries") |
809 | + self.archive, "test_1.0_amd64.tar.gz", "") |
810 | upload.signSipl(os.path.join(self.makeTemporaryDirectory(), 't.sipl')) |
811 | self.assertEqual(1, upload.callLog.caller_count('SIPL keygen key')) |
812 | self.assertEqual(1, upload.callLog.caller_count('SIPL keygen cert')) |
813 | @@ -1290,7 +1298,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
814 | self.tarfile.add_file("1.0/empty.sipl", b"") |
815 | self.process_emulate() |
816 | sha256file = os.path.join(self.getSignedPath("test", "amd64"), |
817 | - "1.0", "SHA256SUMS") |
818 | + "1.0", "SHA256SUMS") |
819 | self.assertTrue(os.path.exists(sha256file)) |
820 | |
821 | @defer.inlineCallbacks |
822 | @@ -1309,7 +1317,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
823 | self.tarfile.add_file("1.0/empty.sipl", b"") |
824 | self.process_emulate() |
825 | sha256file = os.path.join(self.getSignedPath("test", "amd64"), |
826 | - "1.0", "SHA256SUMS") |
827 | + "1.0", "SHA256SUMS") |
828 | self.assertTrue(os.path.exists(sha256file)) |
829 | self.assertThat( |
830 | sha256file + '.gpg', |
831 | @@ -1333,7 +1341,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
832 | self.tarfile.add_file("1.0/empty.sipl", b"") |
833 | self.process_emulate() |
834 | sha256file = os.path.join(self.getSignedPath("test", "amd64"), |
835 | - "1.0", "SHA256SUMS") |
836 | + "1.0", "SHA256SUMS") |
837 | self.assertTrue(os.path.exists(sha256file)) |
838 | self.assertThat( |
839 | sha256file + '.gpg', |
840 | @@ -1344,10 +1352,10 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
841 | "1.0", "signed.tar.gz") |
842 | with tarfile.open(tarfilename) as tarball: |
843 | self.assertThat(tarball.getnames(), MatchesAll(*[ |
844 | - Not(Contains(name)) for name in [ |
845 | - "1.0/SHA256SUMS", "1.0/SHA256SUMS.gpg", |
846 | - "1.0/signed.tar.gz", |
847 | - ]])) |
848 | + Not(Contains(name)) for name in [ |
849 | + "1.0/SHA256SUMS", "1.0/SHA256SUMS.gpg", |
850 | + "1.0/signed.tar.gz", |
851 | + ]])) |
852 | |
853 | def test_checksumming_tree_signed_with_external_run_parts(self): |
854 | # Checksum files can be signed using an external run-parts helper. |
855 | @@ -1368,7 +1376,7 @@ class TestSigning(RunPartsMixin, TestSigningHelpers): |
856 | self.tarfile.add_file("1.0/empty.sipl", "") |
857 | self.process_emulate() |
858 | sha256file = os.path.join(self.getSignedPath("test", "amd64"), |
859 | - "1.0", "SHA256SUMS") |
860 | + "1.0", "SHA256SUMS") |
861 | self.assertTrue(os.path.exists(sha256file)) |
862 | self.assertEqual(1, run_parts_fixture.new_value.call_count) |
863 | args, kwargs = run_parts_fixture.new_value.calls[-1] |
864 | @@ -1499,3 +1507,500 @@ class TestUefi(TestSigningHelpers): |
865 | self.getDistsPath(), "uefi"))) |
866 | self.assertTrue(os.path.exists(os.path.join( |
867 | self.getSignedPath("test", "amd64"), "1.0", "empty.efi"))) |
868 | + |
869 | + |
870 | +class TestSigningUploadWithSigningService(TestSigningHelpers): |
871 | + """Tests for SigningUpload using lp-signing service |
872 | + """ |
873 | + layer = ZopelessDatabaseLayer |
874 | + |
875 | + def setUp(self): |
876 | + super(TestSigningUploadWithSigningService, self).setUp() |
877 | + self.useFixture(FeatureFixture({PUBLISHER_USES_SIGNING_SERVICE: True})) |
878 | + |
879 | + self.signing_service_client = self.useFixture( |
880 | + SigningServiceClientFixture(self.factory)) |
881 | + self.signing_keys = { |
882 | + k: v.signing_key for k, v in self.setUpAllKeyTypes( |
883 | + self.archive).items()} |
884 | + |
885 | + def setUpAllKeyTypes(self, archive): |
886 | + """Helper to create |
887 | + |
888 | + :return: A dict like {key_type: signing_key} with all keys available. |
889 | + """ |
890 | + keys_per_type = {} |
891 | + for key_type in SigningKeyType.items: |
892 | + signing_key = self.factory.makeSigningKey(key_type=key_type) |
893 | + arch_key = self.factory.makeArchiveSigningKey( |
894 | + archive=archive, signing_key=signing_key) |
895 | + keys_per_type[key_type] = arch_key |
896 | + return keys_per_type |
897 | + |
898 | + def getArchiveSigningKey(self, key_type): |
899 | + signing_key = self.factory.makeSigningKey(key_type=key_type) |
900 | + arch_signing_key = self.factory.makeArchiveSigningKey( |
901 | + archive=self.archive, signing_key=signing_key) |
902 | + return arch_signing_key |
903 | + |
904 | + @staticmethod |
905 | + def getFileListContent(basedir, filenames): |
906 | + contents = [] |
907 | + for filename in filenames: |
908 | + with open(os.path.join(basedir, filename), 'rb') as fd: |
909 | + contents.append(fd.read()) |
910 | + return contents |
911 | + |
912 | + def getSignedPath(self, loader_type, arch): |
913 | + return os.path.join(self.getDistsPath(), "signed", |
914 | + "%s-%s" % (loader_type, arch)) |
915 | + |
916 | + def process_emulate(self): |
917 | + """Shortcut to close tarfile and run SigningUpload.process. |
918 | + """ |
919 | + self.tarfile.close() |
920 | + self.buffer.close() |
921 | + |
922 | + upload = SigningUpload() |
923 | + upload.process(self.archive, self.path, self.suite) |
924 | + return upload |
925 | + |
926 | + def test_set_target_directory_with_distroseries(self): |
927 | + archive = self.factory.makeArchive() |
928 | + series_name = archive.distribution.series[1].name |
929 | + |
930 | + upload = SigningUpload() |
931 | + upload.setTargetDirectory( |
932 | + archive, "test_1.0_amd64.tar.gz", series_name) |
933 | + |
934 | + pubconfig = getPubConfig(archive) |
935 | + self.assertThat(upload, MatchesStructure.byEquality( |
936 | + distro_series=archive.distribution.series[1], |
937 | + archive=archive, |
938 | + autokey=pubconfig.signingautokey)) |
939 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
940 | + self.assertEqual(0, self.signing_service_client.sign.call_count) |
941 | + |
942 | + def test_options_handling_single(self): |
943 | + """If the configured key/cert are missing, processing succeeds but |
944 | + nothing is signed. |
945 | + """ |
946 | + self.openArchive("test", "1.0", "amd64") |
947 | + self.tarfile.add_file("1.0/control/options", b"first\n") |
948 | + |
949 | + upload = self.process_emulate() |
950 | + |
951 | + self.assertContentEqual(['first'], upload.signing_options.keys()) |
952 | + |
953 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
954 | + self.assertEqual(0, self.signing_service_client.sign.call_count) |
955 | + |
956 | + def test_options_handling_multiple(self): |
957 | + """If the configured key/cert are missing, processing succeeds but |
958 | + nothing is signed. |
959 | + """ |
960 | + self.openArchive("test", "1.0", "amd64") |
961 | + self.tarfile.add_file("1.0/control/options", b"first\nsecond\n") |
962 | + |
963 | + upload = self.process_emulate() |
964 | + |
965 | + self.assertContentEqual(['first', 'second'], |
966 | + upload.signing_options.keys()) |
967 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
968 | + self.assertEqual(0, self.signing_service_client.sign.call_count) |
969 | + |
970 | + def test_options_tarball(self): |
971 | + """Specifying the "tarball" option should create an tarball in tmpdir. |
972 | + """ |
973 | + self.openArchive("test", "1.0", "amd64") |
974 | + self.tarfile.add_file("1.0/control/options", b"tarball") |
975 | + self.tarfile.add_file("1.0/empty.efi", b"a") |
976 | + self.tarfile.add_file("1.0/empty.ko", b"b") |
977 | + self.tarfile.add_file("1.0/empty.opal", b"c") |
978 | + self.tarfile.add_file("1.0/empty.sipl", b"d") |
979 | + self.tarfile.add_file("1.0/empty.fit", b"e") |
980 | + |
981 | + self.process_emulate() |
982 | + |
983 | + self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([ |
984 | + "1.0/SHA256SUMS", |
985 | + "1.0/signed.tar.gz", |
986 | + ])) |
987 | + tarfilename = os.path.join(self.getSignedPath("test", "amd64"), |
988 | + "1.0", "signed.tar.gz") |
989 | + with tarfile.open(tarfilename) as tarball: |
990 | + self.assertContentEqual([ |
991 | + '1.0', '1.0/control', '1.0/control/options', |
992 | + '1.0/empty.efi', '1.0/empty.efi.signed', |
993 | + '1.0/control/uefi.crt', |
994 | + '1.0/empty.ko', '1.0/empty.ko.sig', '1.0/control/kmod.x509', |
995 | + '1.0/empty.opal', '1.0/empty.opal.sig', |
996 | + '1.0/control/opal.x509', |
997 | + '1.0/empty.sipl', '1.0/empty.sipl.sig', |
998 | + '1.0/control/sipl.x509', |
999 | + '1.0/empty.fit', '1.0/empty.fit.signed', |
1000 | + '1.0/control/fit.crt', |
1001 | + ], tarball.getnames()) |
1002 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1003 | + keys = self.signing_keys |
1004 | + self.assertItemsEqual([ |
1005 | + call( |
1006 | + SigningKeyType.UEFI, keys[SigningKeyType.UEFI].fingerprint, |
1007 | + 'empty.efi', b'a', SigningMode.ATTACHED), |
1008 | + call( |
1009 | + SigningKeyType.KMOD, keys[SigningKeyType.KMOD].fingerprint, |
1010 | + 'empty.ko', b'b', SigningMode.DETACHED), |
1011 | + call( |
1012 | + SigningKeyType.OPAL, keys[SigningKeyType.OPAL].fingerprint, |
1013 | + 'empty.opal', b'c', SigningMode.DETACHED), |
1014 | + call( |
1015 | + SigningKeyType.SIPL, keys[SigningKeyType.SIPL].fingerprint, |
1016 | + 'empty.sipl', b'd', SigningMode.DETACHED), |
1017 | + call( |
1018 | + SigningKeyType.FIT, keys[SigningKeyType.FIT].fingerprint, |
1019 | + 'empty.fit', b'e', SigningMode.ATTACHED)], |
1020 | + self.signing_service_client.sign.call_args_list) |
1021 | + |
1022 | + def test_options_signed_only(self): |
1023 | + """Specifying the "signed-only" option should trigger removal of |
1024 | + the source files leaving signatures only. |
1025 | + """ |
1026 | + self.openArchive("test", "1.0", "amd64") |
1027 | + self.tarfile.add_file("1.0/control/options", b"signed-only") |
1028 | + self.tarfile.add_file("1.0/empty.efi", b"a") |
1029 | + self.tarfile.add_file("1.0/empty.ko", b"b") |
1030 | + self.tarfile.add_file("1.0/empty.opal", b"c") |
1031 | + self.tarfile.add_file("1.0/empty.sipl", b"d") |
1032 | + self.tarfile.add_file("1.0/empty.fit", b"e") |
1033 | + |
1034 | + self.process_emulate() |
1035 | + |
1036 | + self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([ |
1037 | + "1.0/SHA256SUMS", "1.0/control/options", |
1038 | + "1.0/empty.efi.signed", "1.0/control/uefi.crt", |
1039 | + "1.0/empty.ko.sig", "1.0/control/kmod.x509", |
1040 | + "1.0/empty.opal.sig", "1.0/control/opal.x509", |
1041 | + "1.0/empty.sipl.sig", "1.0/control/sipl.x509", |
1042 | + "1.0/empty.fit.signed", "1.0/control/fit.crt", |
1043 | + ])) |
1044 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1045 | + keys = self.signing_keys |
1046 | + self.assertItemsEqual([ |
1047 | + call( |
1048 | + SigningKeyType.UEFI, keys[SigningKeyType.UEFI].fingerprint, |
1049 | + 'empty.efi', b'a', SigningMode.ATTACHED), |
1050 | + call( |
1051 | + SigningKeyType.KMOD, keys[SigningKeyType.KMOD].fingerprint, |
1052 | + 'empty.ko', b'b', SigningMode.DETACHED), |
1053 | + call( |
1054 | + SigningKeyType.OPAL, keys[SigningKeyType.OPAL].fingerprint, |
1055 | + 'empty.opal', b'c', SigningMode.DETACHED), |
1056 | + call( |
1057 | + SigningKeyType.SIPL, keys[SigningKeyType.SIPL].fingerprint, |
1058 | + 'empty.sipl', b'd', SigningMode.DETACHED), |
1059 | + call( |
1060 | + SigningKeyType.FIT, keys[SigningKeyType.FIT].fingerprint, |
1061 | + 'empty.fit', b'e', SigningMode.ATTACHED)], |
1062 | + self.signing_service_client.sign.call_args_list) |
1063 | + |
1064 | + def test_options_tarball_signed_only(self): |
1065 | + """Specifying the "tarball" option should create an tarball in |
1066 | + the tmpdir. Adding signed-only should trigger removal of the |
1067 | + original files. |
1068 | + """ |
1069 | + self.openArchive("test", "1.0", "amd64") |
1070 | + self.tarfile.add_file("1.0/control/options", b"tarball\nsigned-only") |
1071 | + self.tarfile.add_file("1.0/empty.efi", b"a") |
1072 | + self.tarfile.add_file("1.0/empty.ko", b"b") |
1073 | + self.tarfile.add_file("1.0/empty.opal", b"c") |
1074 | + self.tarfile.add_file("1.0/empty.sipl", b"d") |
1075 | + self.tarfile.add_file("1.0/empty.fit", b"e") |
1076 | + self.process_emulate() |
1077 | + self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([ |
1078 | + "1.0/SHA256SUMS", |
1079 | + "1.0/signed.tar.gz", |
1080 | + ])) |
1081 | + tarfilename = os.path.join(self.getSignedPath("test", "amd64"), |
1082 | + "1.0", "signed.tar.gz") |
1083 | + with tarfile.open(tarfilename) as tarball: |
1084 | + self.assertContentEqual([ |
1085 | + '1.0', '1.0/control', '1.0/control/options', |
1086 | + '1.0/empty.efi.signed', '1.0/control/uefi.crt', |
1087 | + '1.0/empty.ko.sig', '1.0/control/kmod.x509', |
1088 | + '1.0/empty.opal.sig', '1.0/control/opal.x509', |
1089 | + '1.0/empty.sipl.sig', '1.0/control/sipl.x509', |
1090 | + '1.0/empty.fit.signed', '1.0/control/fit.crt', |
1091 | + ], tarball.getnames()) |
1092 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1093 | + keys = self.signing_keys |
1094 | + self.assertItemsEqual([ |
1095 | + call( |
1096 | + SigningKeyType.UEFI, keys[SigningKeyType.UEFI].fingerprint, |
1097 | + 'empty.efi', b'a', SigningMode.ATTACHED), |
1098 | + call( |
1099 | + SigningKeyType.KMOD, keys[SigningKeyType.KMOD].fingerprint, |
1100 | + 'empty.ko', b'b', SigningMode.DETACHED), |
1101 | + call( |
1102 | + SigningKeyType.OPAL, keys[SigningKeyType.OPAL].fingerprint, |
1103 | + 'empty.opal', b'c', SigningMode.DETACHED), |
1104 | + call( |
1105 | + SigningKeyType.SIPL, keys[SigningKeyType.SIPL].fingerprint, |
1106 | + 'empty.sipl', b'd', SigningMode.DETACHED), |
1107 | + call( |
1108 | + SigningKeyType.FIT, keys[SigningKeyType.FIT].fingerprint, |
1109 | + 'empty.fit', b'e', SigningMode.ATTACHED)], |
1110 | + self.signing_service_client.sign.call_args_list) |
1111 | + |
1112 | + def test_archive_copy(self): |
1113 | + """If there is no key/cert configuration, processing succeeds but |
1114 | + nothing is signed. |
1115 | + """ |
1116 | + self.archive = self.factory.makeArchive( |
1117 | + distribution=self.distro, purpose=ArchivePurpose.COPY) |
1118 | + |
1119 | + pubconf = getPubConfig(self.archive) |
1120 | + if not os.path.exists(pubconf.temproot): |
1121 | + os.makedirs(pubconf.temproot) |
1122 | + self.openArchive("test", "1.0", "amd64") |
1123 | + self.tarfile.add_file("1.0/empty.efi", b"a") |
1124 | + self.tarfile.add_file("1.0/empty.ko", b"b") |
1125 | + self.tarfile.add_file("1.0/empty.opal", b"c") |
1126 | + self.tarfile.add_file("1.0/empty.sipl", b"d") |
1127 | + self.tarfile.add_file("1.0/empty.fit", b"e") |
1128 | + self.tarfile.close() |
1129 | + self.buffer.close() |
1130 | + |
1131 | + upload = SigningUpload() |
1132 | + upload.process(self.archive, self.path, self.suite) |
1133 | + |
1134 | + signed_path = self.getSignedPath("test", "amd64") |
1135 | + self.assertThat(signed_path, SignedMatches( |
1136 | + ["1.0/SHA256SUMS", "1.0/empty.efi", "1.0/empty.ko", |
1137 | + "1.0/empty.opal", "1.0/empty.sipl", "1.0/empty.fit", ])) |
1138 | + |
1139 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1140 | + self.assertEqual(0, self.signing_service_client.sign.call_count) |
1141 | + |
1142 | + def test_sign_without_autokey_and_no_key_pre_set(self): |
1143 | + """This case should raise exception, since we don't have fallback |
1144 | + keys on the filesystem to cover for the missing signing service |
1145 | + keys. |
1146 | + """ |
1147 | + self.distro = self.factory.makeDistribution() |
1148 | + self.distroseries = self.factory.makeDistroSeries( |
1149 | + distribution=self.distro) |
1150 | + self.suite = self.distroseries.name |
1151 | + self.archive = self.factory.makeArchive( |
1152 | + distribution=self.distro, purpose=ArchivePurpose.PRIMARY) |
1153 | + |
1154 | + filenames = [ |
1155 | + "1.0/empty.efi", "1.0/empty.ko", "1.0/empty.opal", |
1156 | + "1.0/empty.sipl", "1.0/empty.fit"] |
1157 | + |
1158 | + # Write data on the archive |
1159 | + self.openArchive("test", "1.0", "amd64") |
1160 | + for filename in filenames: |
1161 | + self.tarfile.add_file(filename, b"somedata for %s" % filename) |
1162 | + |
1163 | + self.assertRaises(IOError, self.process_emulate) |
1164 | + |
1165 | + def test_sign_without_autokey_and_some_keys_pre_set(self): |
1166 | + """For no autokey archives, signing process should sign only for the |
1167 | + available keys, and skip signing the other files. |
1168 | + """ |
1169 | + # Pre-generate KMOD and OPAL keys |
1170 | + self.getArchiveSigningKey(SigningKeyType.KMOD) |
1171 | + self.getArchiveSigningKey(SigningKeyType.OPAL) |
1172 | + |
1173 | + filenames = ["1.0/empty.ko", "1.0/empty.opal"] |
1174 | + |
1175 | + self.openArchive("test", "1.0", "amd64") |
1176 | + for filename in filenames: |
1177 | + self.tarfile.add_file(filename, b"some data for %s" % filename) |
1178 | + |
1179 | + self.process_emulate() |
1180 | + |
1181 | + signed_path = self.getSignedPath("test", "amd64") |
1182 | + self.assertThat(signed_path, SignedMatches(filenames + [ |
1183 | + "1.0/SHA256SUMS", "1.0/empty.ko.sig", "1.0/empty.opal.sig", |
1184 | + "1.0/control/kmod.x509", "1.0/control/opal.x509"])) |
1185 | + |
1186 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1187 | + keys = self.signing_keys |
1188 | + self.assertItemsEqual([ |
1189 | + call( |
1190 | + SigningKeyType.KMOD, keys[SigningKeyType.KMOD].fingerprint, |
1191 | + 'empty.ko', b'some data for 1.0/empty.ko', |
1192 | + SigningMode.DETACHED), |
1193 | + call( |
1194 | + SigningKeyType.OPAL, keys[SigningKeyType.OPAL].fingerprint, |
1195 | + 'empty.opal', b'some data for 1.0/empty.opal', |
1196 | + SigningMode.DETACHED)], |
1197 | + self.signing_service_client.sign.call_args_list) |
1198 | + |
1199 | + def test_sign_with_autokey_ppa(self): |
1200 | + # PPAs should auto-generate keys. Let's use one for this test. |
1201 | + self.setUpPPA() |
1202 | + |
1203 | + filenames = [ |
1204 | + "1.0/empty.efi", "1.0/empty.ko", "1.0/empty.opal", |
1205 | + "1.0/empty.sipl", "1.0/empty.fit"] |
1206 | + |
1207 | + self.openArchive("test", "1.0", "amd64") |
1208 | + for filename in filenames: |
1209 | + self.tarfile.add_file(filename, b"data - %s" % filename) |
1210 | + |
1211 | + self.tarfile.close() |
1212 | + self.buffer.close() |
1213 | + |
1214 | + upload = SigningUpload() |
1215 | + upload.process(self.archive, self.path, self.suite) |
1216 | + |
1217 | + self.assertTrue(upload.autokey) |
1218 | + |
1219 | + expected_signed_filenames = [ |
1220 | + "1.0/empty.efi.signed", "1.0/empty.ko.sig", |
1221 | + "1.0/empty.opal.sig", "1.0/empty.sipl.sig", |
1222 | + "1.0/empty.fit.signed"] |
1223 | + |
1224 | + expected_public_keys_filenames = [ |
1225 | + "1.0/control/uefi.crt", "1.0/control/kmod.x509", |
1226 | + "1.0/control/opal.x509", "1.0/control/sipl.x509", |
1227 | + "1.0/control/fit.crt"] |
1228 | + |
1229 | + signed_path = self.getSignedPath("test", "amd64") |
1230 | + self.assertThat(signed_path, SignedMatches( |
1231 | + ["1.0/SHA256SUMS"] + filenames + expected_public_keys_filenames + |
1232 | + expected_signed_filenames)) |
1233 | + |
1234 | + self.assertEqual(5, self.signing_service_client.generate.call_count) |
1235 | + self.assertEqual(5, self.signing_service_client.sign.call_count) |
1236 | + |
1237 | + fingerprints = { |
1238 | + key_type: data['fingerprint'] for key_type, data in |
1239 | + self.signing_service_client.generate_returns} |
1240 | + self.assertItemsEqual([ |
1241 | + call( |
1242 | + SigningKeyType.UEFI, fingerprints[SigningKeyType.UEFI], |
1243 | + 'empty.efi', b'data - 1.0/empty.efi', SigningMode.ATTACHED), |
1244 | + call( |
1245 | + SigningKeyType.KMOD, fingerprints[SigningKeyType.KMOD], |
1246 | + 'empty.ko', b'data - 1.0/empty.ko', SigningMode.DETACHED), |
1247 | + call( |
1248 | + SigningKeyType.OPAL, fingerprints[SigningKeyType.OPAL], |
1249 | + 'empty.opal', b'data - 1.0/empty.opal', SigningMode.DETACHED), |
1250 | + call( |
1251 | + SigningKeyType.SIPL, fingerprints[SigningKeyType.SIPL], |
1252 | + 'empty.sipl', b'data - 1.0/empty.sipl', SigningMode.DETACHED), |
1253 | + call( |
1254 | + SigningKeyType.FIT, fingerprints[SigningKeyType.FIT], |
1255 | + 'empty.fit', b'data - 1.0/empty.fit', SigningMode.ATTACHED)], |
1256 | + self.signing_service_client.sign.call_args_list) |
1257 | + |
1258 | + # Checks that all files got signed |
1259 | + contents = self.getFileListContent( |
1260 | + signed_path, expected_signed_filenames) |
1261 | + key_types = ( |
1262 | + SigningKeyType.UEFI, SigningKeyType.KMOD, SigningKeyType.OPAL, |
1263 | + SigningKeyType.SIPL, SigningKeyType.FIT) |
1264 | + expected_signed_contents = [ |
1265 | + b"signed with key_type=%s" % k.name for k in key_types] |
1266 | + self.assertItemsEqual(expected_signed_contents, contents) |
1267 | + |
1268 | + # Checks that all public keys ended up in the 1.0/control/xxx files |
1269 | + public_keys = { |
1270 | + key_type: data['public-key'] for key_type, data in |
1271 | + self.signing_service_client.generate_returns} |
1272 | + contents = self.getFileListContent( |
1273 | + signed_path, expected_public_keys_filenames) |
1274 | + expected_public_keys = [ |
1275 | + public_keys[k] for k in key_types] |
1276 | + self.assertEqual(expected_public_keys, contents) |
1277 | + |
1278 | + def test_fallback_handler(self): |
1279 | + upload = SigningUpload() |
1280 | + |
1281 | + # Creating a new archive since our setUp method fills the self.archive |
1282 | + # with signing keys, and we don't want that here. |
1283 | + self.distro = self.factory.makeDistribution() |
1284 | + self.distroseries = self.factory.makeDistroSeries( |
1285 | + distribution=self.distro) |
1286 | + self.suite = self.distroseries.name |
1287 | + self.archive = self.factory.makeArchive( |
1288 | + distribution=self.distro, |
1289 | + purpose=ArchivePurpose.PRIMARY) |
1290 | + pubconf = getPubConfig(self.archive) |
1291 | + if not os.path.exists(pubconf.temproot): |
1292 | + os.makedirs(pubconf.temproot) |
1293 | + self.addCleanup(lambda: shutil.rmtree(pubconf.temproot, True)) |
1294 | + old_umask = os.umask(0o022) |
1295 | + self.addCleanup(os.umask, old_umask) |
1296 | + self.addCleanup(lambda: shutil.rmtree(pubconf.distroroot, True)) |
1297 | + |
1298 | + # Make KMOD signing fail with an exception. |
1299 | + def mock_sign(key_type, *args, **kwargs): |
1300 | + if key_type == SigningKeyType.KMOD: |
1301 | + raise ValueError("!!") |
1302 | + return self.signing_service_client._sign(key_type, *args, **kwargs) |
1303 | + |
1304 | + self.signing_service_client.sign.side_effect = mock_sign |
1305 | + |
1306 | + # Pre-set KMOD fails on ".sign" method (should fallback to local |
1307 | + # signing method). |
1308 | + self.getArchiveSigningKey(SigningKeyType.KMOD) |
1309 | + upload.signKmod = FakeMethod(result=0) |
1310 | + |
1311 | + # We don't have a signing service key for UEFI. Should fallback too. |
1312 | + upload.signUefi = FakeMethod(result=0) |
1313 | + |
1314 | + # OPAL key works just fine. |
1315 | + self.getArchiveSigningKey(SigningKeyType.OPAL) |
1316 | + upload.signOpal = FakeMethod(result=0) |
1317 | + |
1318 | + filenames = ["1.0/empty.efi", "1.0/empty.ko", "1.0/empty.opal"] |
1319 | + |
1320 | + self.openArchive("test", "1.0", "amd64") |
1321 | + for filename in filenames: |
1322 | + self.tarfile.add_file(filename, b"data - %s" % filename) |
1323 | + |
1324 | + self.tarfile.close() |
1325 | + self.buffer.close() |
1326 | + |
1327 | + # Small hack to keep the tmpdir used during upload.process |
1328 | + # Without this hack, upload.tmpdir is set back to None at the end of |
1329 | + # process() method execution, during cleanup phase. |
1330 | + original_cleanup = upload.cleanup |
1331 | + |
1332 | + def intercept_cleanup(): |
1333 | + upload.tmpdir_used = upload.tmpdir |
1334 | + original_cleanup() |
1335 | + |
1336 | + upload.cleanup = intercept_cleanup |
1337 | + |
1338 | + # Pretend that all key files exists, so the fallback calls are not |
1339 | + # blocked. |
1340 | + upload.keyFilesExist = lambda _: True |
1341 | + |
1342 | + upload.process(self.archive, self.path, self.suite) |
1343 | + |
1344 | + # Make sure it only used the existing keys and fallbacks. No new key |
1345 | + # should be generated. |
1346 | + self.assertFalse(upload.autokey) |
1347 | + |
1348 | + self.assertEqual(0, self.signing_service_client.generate.call_count) |
1349 | + self.assertEqual(2, self.signing_service_client.sign.call_count) |
1350 | + |
1351 | + # Check kmod signing |
1352 | + self.assertEqual(1, upload.signKmod.call_count) |
1353 | + self.assertEqual( |
1354 | + [(os.path.join(upload.tmpdir_used, "1.0/empty.ko"), )], |
1355 | + upload.signKmod.extract_args()) |
1356 | + |
1357 | + # Check OPAL signing |
1358 | + self.assertEqual(0, upload.signOpal.call_count) |
1359 | + |
1360 | + # Check UEFI signing |
1361 | + self.assertEqual(1, upload.signUefi.call_count) |
1362 | + self.assertEqual( |
1363 | + [(os.path.join(upload.tmpdir_used, "1.0/empty.efi"),)], |
1364 | + upload.signUefi.extract_args()) |
1365 | diff --git a/lib/lp/services/compat.py b/lib/lp/services/compat.py |
1366 | index 942e690..8f3c813 100644 |
1367 | --- a/lib/lp/services/compat.py |
1368 | +++ b/lib/lp/services/compat.py |
1369 | @@ -11,9 +11,16 @@ from __future__ import absolute_import, print_function, unicode_literals |
1370 | __metaclass__ = type |
1371 | __all__ = [ |
1372 | 'SafeConfigParser', |
1373 | + 'mock', |
1374 | ] |
1375 | |
1376 | try: |
1377 | from configparser import ConfigParser as SafeConfigParser |
1378 | except ImportError: |
1379 | from ConfigParser import SafeConfigParser |
1380 | + |
1381 | + |
1382 | +try: |
1383 | + import mock |
1384 | +except ImportError: |
1385 | + from unittest import mock |
1386 | diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf |
1387 | index c43a908..18b452b 100644 |
1388 | --- a/lib/lp/services/config/schema-lazr.conf |
1389 | +++ b/lib/lp/services/config/schema-lazr.conf |
1390 | @@ -358,7 +358,7 @@ update_preview_diff_ready_timeout: 15 |
1391 | # An HTTP service that will have status 200 OK when the service is available |
1392 | # for more connections, and 503 Service Unavailable when it is in the process |
1393 | # of shutting down and so should not receive any more connections. |
1394 | -web_status_port = tcp:8022 |
1395 | +web_status_port: tcp:8022 |
1396 | |
1397 | # The URL of the internal Bazaar hosting API endpoint. |
1398 | internal_bzr_api_endpoint: none |
1399 | @@ -1147,7 +1147,7 @@ dbname: session_prod |
1400 | |
1401 | |
1402 | [librarianlogparser] |
1403 | -logs_root = /srv/launchpadlibrarian.net-logs |
1404 | +logs_root: /srv/launchpadlibrarian.net-logs |
1405 | |
1406 | |
1407 | [librarian] |
1408 | @@ -1203,7 +1203,7 @@ download_url: http://librarian.launchpad.net/ |
1409 | # datatype: urlbase |
1410 | restricted_download_url: http://restricted-librarian.launchpad.net/ |
1411 | |
1412 | -use_https = True |
1413 | +use_https: True |
1414 | |
1415 | # The URL of the XML-RPC endpoint that handles verifying macaroons. This |
1416 | # should implement IAuthServer. |
1417 | @@ -1568,11 +1568,17 @@ generate_templates: True |
1418 | [rosetta_pofile_stats] |
1419 | # In daily runs of pofile statistics update, check for |
1420 | # POFiles that have been updated in the last how many days. |
1421 | -days_considered_recent = 7 |
1422 | +days_considered_recent: 7 |
1423 | |
1424 | # Number of seconds each LoopTuner iteration should take. |
1425 | -looptuner_iteration_duration = 4 |
1426 | - |
1427 | +looptuner_iteration_duration: 4 |
1428 | + |
1429 | +# lp-signing service connection info. See lp-signing's documentation on how to |
1430 | +# get valid keys. |
1431 | +[signing] |
1432 | +signing_endpoint: none |
1433 | +client_private_key: none |
1434 | +client_public_key: none |
1435 | |
1436 | # For the personal standing updater cron script. |
1437 | [standingupdater] |
1438 | diff --git a/lib/lp/services/configure.zcml b/lib/lp/services/configure.zcml |
1439 | index b85bfa5..f23faea 100644 |
1440 | --- a/lib/lp/services/configure.zcml |
1441 | +++ b/lib/lp/services/configure.zcml |
1442 | @@ -1,4 +1,4 @@ |
1443 | -<!-- Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
1444 | +<!-- Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
1445 | GNU Affero General Public License version 3 (see the file LICENSE). |
1446 | --> |
1447 | |
1448 | @@ -26,6 +26,7 @@ |
1449 | <include package=".profile" /> |
1450 | <include package=".scripts" /> |
1451 | <include package=".session" /> |
1452 | + <include package=".signing" /> |
1453 | <include package=".sitesearch" /> |
1454 | <include package=".statistics" /> |
1455 | <include package=".temporaryblobstorage" /> |
1456 | diff --git a/lib/lp/services/features/flags.py b/lib/lp/services/features/flags.py |
1457 | index 9fdb4a0..4694cac 100644 |
1458 | --- a/lib/lp/services/features/flags.py |
1459 | +++ b/lib/lp/services/features/flags.py |
1460 | @@ -1,4 +1,4 @@ |
1461 | -# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
1462 | +# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
1463 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1464 | |
1465 | __all__ = [ |
1466 | @@ -227,6 +227,12 @@ flag_info = sorted([ |
1467 | 'bing', |
1468 | 'Site search engine', |
1469 | ''), |
1470 | + ('archivepublisher.signing_service.enabled', |
1471 | + 'boolean', |
1472 | + 'If true, sign packages using signing service instead of local files.', |
1473 | + '', |
1474 | + '', |
1475 | + ''), |
1476 | ]) |
1477 | |
1478 | # The set of all flag names that are documented. |
1479 | diff --git a/lib/lp/services/signing/__init__.py b/lib/lp/services/signing/__init__.py |
1480 | new file mode 100644 |
1481 | index 0000000..e69de29 |
1482 | --- /dev/null |
1483 | +++ b/lib/lp/services/signing/__init__.py |
1484 | diff --git a/lib/lp/services/signing/configure.zcml b/lib/lp/services/signing/configure.zcml |
1485 | new file mode 100644 |
1486 | index 0000000..4b72d7d |
1487 | --- /dev/null |
1488 | +++ b/lib/lp/services/signing/configure.zcml |
1489 | @@ -0,0 +1,27 @@ |
1490 | +<configure |
1491 | + xmlns="http://namespaces.zope.org/zope"> |
1492 | + |
1493 | + <class class="lp.services.signing.model.signingkey.ArchiveSigningKey"> |
1494 | + <allow |
1495 | + interface="lp.services.signing.interfaces.signingkey.IArchiveSigningKey"/> |
1496 | + </class> |
1497 | + |
1498 | + <class class="lp.services.signing.model.signingkey.SigningKey"> |
1499 | + <allow |
1500 | + interface="lp.services.signing.interfaces.signingkey.ISigningKey"/> |
1501 | + </class> |
1502 | + |
1503 | + <securedutility |
1504 | + class="lp.services.signing.model.signingkey.ArchiveSigningKeySet" |
1505 | + provides="lp.services.signing.interfaces.signingkey.IArchiveSigningKeySet"> |
1506 | + <allow |
1507 | + interface="lp.services.signing.interfaces.signingkey.IArchiveSigningKeySet"/> |
1508 | + </securedutility> |
1509 | + |
1510 | + <securedutility |
1511 | + class="lp.services.signing.proxy.SigningServiceClient" |
1512 | + provides="lp.services.signing.interfaces.signingserviceclient.ISigningServiceClient"> |
1513 | + <allow |
1514 | + interface="lp.services.signing.interfaces.signingserviceclient.ISigningServiceClient" /> |
1515 | + </securedutility> |
1516 | +</configure> |
1517 | diff --git a/lib/lp/services/signing/enums.py b/lib/lp/services/signing/enums.py |
1518 | new file mode 100644 |
1519 | index 0000000..fdffe5a |
1520 | --- /dev/null |
1521 | +++ b/lib/lp/services/signing/enums.py |
1522 | @@ -0,0 +1,65 @@ |
1523 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1524 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1525 | + |
1526 | +"""Enums for signing keys management |
1527 | +""" |
1528 | + |
1529 | +__metaclass__ = type |
1530 | + |
1531 | +__all__ = [ |
1532 | + 'SigningKeyType', |
1533 | + 'SigningMode', |
1534 | + ] |
1535 | + |
1536 | +from lazr.enum import ( |
1537 | + DBEnumeratedType, |
1538 | + DBItem, |
1539 | + EnumeratedType, |
1540 | + Item, |
1541 | + ) |
1542 | + |
1543 | + |
1544 | +class SigningKeyType(DBEnumeratedType): |
1545 | + """Available key types on lp-signing service. |
1546 | + |
1547 | + These items should be kept in sync with |
1548 | + lp-signing:lp-signing/lp_signing/enums.py (specially the numbers) to |
1549 | + avoid confusion when reading values from different databases. |
1550 | + """ |
1551 | + UEFI = DBItem(1, """ |
1552 | + UEFI |
1553 | + |
1554 | + A signing key for UEFI Secure Boot images. |
1555 | + """) |
1556 | + |
1557 | + KMOD = DBItem(2, """ |
1558 | + Kmod |
1559 | + |
1560 | + A signing key for kernel modules. |
1561 | + """) |
1562 | + |
1563 | + OPAL = DBItem(3, """ |
1564 | + OPAL |
1565 | + |
1566 | + A signing key for OPAL kernel images. |
1567 | + """) |
1568 | + |
1569 | + SIPL = DBItem(4, """ |
1570 | + SIPL |
1571 | + |
1572 | + A signing key for Secure Initial Program Load kernel images. |
1573 | + """) |
1574 | + |
1575 | + FIT = DBItem(5, """ |
1576 | + FIT |
1577 | + |
1578 | + A signing key for U-Boot Flat Image Tree images. |
1579 | + """) |
1580 | + |
1581 | + |
1582 | +class SigningMode(EnumeratedType): |
1583 | + """Archive file signing mode.""" |
1584 | + |
1585 | + ATTACHED = Item("Attached signature") |
1586 | + DETACHED = Item("Detached signature") |
1587 | + CLEAR = Item("Cleartext signature") |
1588 | diff --git a/lib/lp/services/signing/interfaces/__init__.py b/lib/lp/services/signing/interfaces/__init__.py |
1589 | new file mode 100644 |
1590 | index 0000000..e69de29 |
1591 | --- /dev/null |
1592 | +++ b/lib/lp/services/signing/interfaces/__init__.py |
1593 | diff --git a/lib/lp/services/signing/interfaces/signingkey.py b/lib/lp/services/signing/interfaces/signingkey.py |
1594 | new file mode 100644 |
1595 | index 0000000..594a053 |
1596 | --- /dev/null |
1597 | +++ b/lib/lp/services/signing/interfaces/signingkey.py |
1598 | @@ -0,0 +1,126 @@ |
1599 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
1600 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1601 | + |
1602 | +"""Interfaces for signing keys stored at the signing service.""" |
1603 | + |
1604 | +__metaclass__ = type |
1605 | + |
1606 | +__all__ = [ |
1607 | + 'IArchiveSigningKey', |
1608 | + 'IArchiveSigningKeySet', |
1609 | + 'ISigningKey', |
1610 | + 'ISigningKeySet', |
1611 | +] |
1612 | + |
1613 | +from lazr.restful.fields import Reference |
1614 | +from zope.interface.interface import Interface |
1615 | +from zope.schema import ( |
1616 | + Bytes, |
1617 | + Choice, |
1618 | + Datetime, |
1619 | + Int, |
1620 | + Text, |
1621 | + ) |
1622 | + |
1623 | +from lp import _ |
1624 | +from lp.registry.interfaces.distroseries import IDistroSeries |
1625 | +from lp.services.signing.enums import SigningKeyType |
1626 | +from lp.soyuz.interfaces.archive import IArchive |
1627 | + |
1628 | + |
1629 | +class ISigningKey(Interface): |
1630 | + """A key registered to sign uploaded files""" |
1631 | + |
1632 | + id = Int(title=_('ID'), required=True, readonly=True) |
1633 | + |
1634 | + key_type = Choice( |
1635 | + title=_("The signing key type (UEFI, KMOD, etc)."), |
1636 | + required=True, readonly=True, vocabulary=SigningKeyType) |
1637 | + |
1638 | + fingerprint = Text( |
1639 | + title=_("Fingerprint of the key"), required=True, readonly=True) |
1640 | + |
1641 | + public_key = Bytes( |
1642 | + title=_("Public key binary content"), required=False, |
1643 | + readonly=True) |
1644 | + |
1645 | + date_created = Datetime( |
1646 | + title=_('When this key was created'), required=True, readonly=True) |
1647 | + |
1648 | + def sign(message, message_name): |
1649 | + """Sign the given message using this key |
1650 | + |
1651 | + :param message: The message to be signed. |
1652 | + :param message_name: A name for the message being signed. |
1653 | + """ |
1654 | + |
1655 | + |
1656 | +class ISigningKeySet(Interface): |
1657 | + """Interface to deal with the collection of signing keys |
1658 | + """ |
1659 | + |
1660 | + def generate(key_type, description=None): |
1661 | + """Generates a new signing key on lp-signing and stores it in LP's |
1662 | + database. |
1663 | + |
1664 | + :param key_type: One of the SigningKeyType enum's value |
1665 | + :param description: (optional) The description associated with this |
1666 | + key |
1667 | + :returns: The SigningKey object associated with the newly created |
1668 | + key at lp-signing""" |
1669 | + |
1670 | + |
1671 | +class IArchiveSigningKey(Interface): |
1672 | + """Which signing key should be used by a specific archive""" |
1673 | + |
1674 | + id = Int(title=_('ID'), required=True, readonly=True) |
1675 | + |
1676 | + archive = Reference( |
1677 | + IArchive, title=_("Archive"), required=True, readonly=True, |
1678 | + description=_("The archive that owns this key.")) |
1679 | + |
1680 | + earliest_distro_series = Reference( |
1681 | + IDistroSeries, title=_("Distro series"), required=False, readonly=True, |
1682 | + description=_("The minimum series that uses this key, if any.")) |
1683 | + |
1684 | + key_type = Choice( |
1685 | + title=_("The signing key type (UEFI, KMOD, etc)."), |
1686 | + required=True, readonly=True, vocabulary=SigningKeyType) |
1687 | + |
1688 | + signing_key = Reference( |
1689 | + ISigningKey, title=_("Signing key"), required=True, readonly=True, |
1690 | + description=_("Which signing key should be used by this archive")) |
1691 | + |
1692 | + |
1693 | +class IArchiveSigningKeySet(Interface): |
1694 | + """Management class to deal with ArchiveSigningKey objects |
1695 | + """ |
1696 | + |
1697 | + def create(archive, earliest_distro_series, signing_key): |
1698 | + """Creates a new ArchiveSigningKey for archive/distro_series. |
1699 | + |
1700 | + :return: A tuple like (db_object:ArchiveSigningKey, created:boolean) |
1701 | + with the ArchiveSigningKey and True if it was created ( |
1702 | + False if it was updated). |
1703 | + """ |
1704 | + |
1705 | + def getSigningKey(key_type, archive, distro_series): |
1706 | + """Get the most suitable key for a given archive / distro series |
1707 | + pair. |
1708 | + |
1709 | + :return: The most suitable key |
1710 | + """ |
1711 | + |
1712 | + def generate(key_type, archive, earliest_distro_series=None, |
1713 | + description=None): |
1714 | + """Generate a new key on signing service, and save it to db. |
1715 | + |
1716 | + :param key_type: One of the SigningKeyType enum's value |
1717 | + :param archive: The package Archive that should be associated with |
1718 | + this key |
1719 | + :param earliest_distro_series: (optional) The minimum distro series |
1720 | + that should use the generated key. |
1721 | + :param description: (optional) The description associated with this |
1722 | + key |
1723 | + :returns: The generated ArchiveSigningKey |
1724 | + """ |
1725 | diff --git a/lib/lp/services/signing/interfaces/signingserviceclient.py b/lib/lp/services/signing/interfaces/signingserviceclient.py |
1726 | new file mode 100644 |
1727 | index 0000000..0d2a242 |
1728 | --- /dev/null |
1729 | +++ b/lib/lp/services/signing/interfaces/signingserviceclient.py |
1730 | @@ -0,0 +1,47 @@ |
1731 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
1732 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1733 | + |
1734 | +"""Interfaces for signing keys stored at the signing service.""" |
1735 | + |
1736 | +__metaclass__ = type |
1737 | + |
1738 | +__all__ = [ |
1739 | + 'ISigningServiceClient', |
1740 | + ] |
1741 | + |
1742 | +from zope.interface import ( |
1743 | + Attribute, |
1744 | + Interface, |
1745 | + ) |
1746 | + |
1747 | +from lp import _ |
1748 | + |
1749 | + |
1750 | +class ISigningServiceClient(Interface): |
1751 | + service_public_key = Attribute(_("The public key of signing service.")) |
1752 | + private_key = Attribute(_("This client's private key.")) |
1753 | + |
1754 | + def getNonce(): |
1755 | + """Get nonce, to be used when sending messages. |
1756 | + """ |
1757 | + |
1758 | + def generate(key_type, description): |
1759 | + """Generate a key to be used when signing. |
1760 | + |
1761 | + :param key_type: One of available key types at SigningKeyType |
1762 | + :param description: String description of the generated key |
1763 | + :return: A dict with 'fingerprint' (str) and 'public-key' (bytes) |
1764 | + """ |
1765 | + |
1766 | + def sign(key_type, fingerprint, message_name, message, mode): |
1767 | + """Sign the given message using the specified key_type and a |
1768 | + pre-generated fingerprint (see `generate` method). |
1769 | + |
1770 | + :param key_type: One of the key types from SigningKeyType enum |
1771 | + :param fingerprint: The fingerprint of the signing key, generated by |
1772 | + the `generate` method |
1773 | + :param message_name: A description of the message being signed |
1774 | + :param message: The message to be signed |
1775 | + :param mode: SigningMode.ATTACHED or SigningMode.DETACHED |
1776 | + :return: A dict with 'public-key' and 'signed-message' |
1777 | + """ |
1778 | diff --git a/lib/lp/services/signing/model/__init__.py b/lib/lp/services/signing/model/__init__.py |
1779 | new file mode 100644 |
1780 | index 0000000..e69de29 |
1781 | --- /dev/null |
1782 | +++ b/lib/lp/services/signing/model/__init__.py |
1783 | diff --git a/lib/lp/services/signing/model/signingkey.py b/lib/lp/services/signing/model/signingkey.py |
1784 | new file mode 100644 |
1785 | index 0000000..e77c200 |
1786 | --- /dev/null |
1787 | +++ b/lib/lp/services/signing/model/signingkey.py |
1788 | @@ -0,0 +1,196 @@ |
1789 | +# Copyright 2020 Canonical Ltd. This software is licensed under the |
1790 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1791 | + |
1792 | +"""Database classes to manage signing keys stored at the signing service.""" |
1793 | + |
1794 | + |
1795 | +__metaclass__ = type |
1796 | + |
1797 | +__all__ = [ |
1798 | + 'ArchiveSigningKey', |
1799 | + 'ArchiveSigningKeySet', |
1800 | + 'SigningKey', |
1801 | + ] |
1802 | + |
1803 | +from collections import defaultdict |
1804 | + |
1805 | +import pytz |
1806 | +from storm.locals import ( |
1807 | + Bytes, |
1808 | + DateTime, |
1809 | + Int, |
1810 | + Reference, |
1811 | + Unicode, |
1812 | + ) |
1813 | +from zope.component import getUtility |
1814 | +from zope.interface import ( |
1815 | + implementer, |
1816 | + provider, |
1817 | + ) |
1818 | + |
1819 | +from lp.services.database.constants import ( |
1820 | + DEFAULT, |
1821 | + UTC_NOW, |
1822 | + ) |
1823 | +from lp.services.database.enumcol import DBEnum |
1824 | +from lp.services.database.interfaces import ( |
1825 | + IMasterStore, |
1826 | + IStore, |
1827 | + ) |
1828 | +from lp.services.database.stormbase import StormBase |
1829 | +from lp.services.signing.enums import ( |
1830 | + SigningKeyType, |
1831 | + SigningMode, |
1832 | + ) |
1833 | +from lp.services.signing.interfaces.signingkey import ( |
1834 | + IArchiveSigningKey, |
1835 | + IArchiveSigningKeySet, |
1836 | + ISigningKey, |
1837 | + ISigningKeySet, |
1838 | + ) |
1839 | +from lp.services.signing.interfaces.signingserviceclient import ( |
1840 | + ISigningServiceClient, |
1841 | + ) |
1842 | + |
1843 | + |
1844 | +@implementer(ISigningKey) |
1845 | +@provider(ISigningKeySet) |
1846 | +class SigningKey(StormBase): |
1847 | + """A key stored at lp-signing, used to sign uploaded files and packages""" |
1848 | + |
1849 | + __storm_table__ = 'SigningKey' |
1850 | + |
1851 | + id = Int(primary=True) |
1852 | + |
1853 | + key_type = DBEnum(enum=SigningKeyType, allow_none=False) |
1854 | + |
1855 | + description = Unicode(allow_none=True) |
1856 | + |
1857 | + fingerprint = Unicode(allow_none=False) |
1858 | + |
1859 | + public_key = Bytes(allow_none=False) |
1860 | + |
1861 | + date_created = DateTime( |
1862 | + allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC) |
1863 | + |
1864 | + def __init__(self, key_type, fingerprint, public_key, |
1865 | + description=None, date_created=DEFAULT): |
1866 | + """Builds the signing key |
1867 | + |
1868 | + :param key_type: One of the SigningKeyType enum items |
1869 | + :param fingerprint: The key's fingerprint |
1870 | + :param public_key: The key's public key (raw; not base64-encoded) |
1871 | + """ |
1872 | + super(SigningKey, self).__init__() |
1873 | + self.key_type = key_type |
1874 | + self.fingerprint = fingerprint |
1875 | + self.public_key = public_key |
1876 | + self.description = description |
1877 | + self.date_created = date_created |
1878 | + |
1879 | + @classmethod |
1880 | + def generate(cls, key_type, description=None): |
1881 | + signing_service = getUtility(ISigningServiceClient) |
1882 | + generated_key = signing_service.generate(key_type, description) |
1883 | + signing_key = SigningKey( |
1884 | + key_type=key_type, fingerprint=generated_key['fingerprint'], |
1885 | + public_key=generated_key['public-key'], |
1886 | + description=description) |
1887 | + store = IMasterStore(SigningKey) |
1888 | + store.add(signing_key) |
1889 | + return signing_key |
1890 | + |
1891 | + def sign(self, message, message_name): |
1892 | + if self.key_type in (SigningKeyType.UEFI, SigningKeyType.FIT): |
1893 | + mode = SigningMode.ATTACHED |
1894 | + else: |
1895 | + mode = SigningMode.DETACHED |
1896 | + signing_service = getUtility(ISigningServiceClient) |
1897 | + signed = signing_service.sign( |
1898 | + self.key_type, self.fingerprint, message_name, message, mode) |
1899 | + return signed['signed-message'] |
1900 | + |
1901 | + |
1902 | +@implementer(IArchiveSigningKey) |
1903 | +class ArchiveSigningKey(StormBase): |
1904 | + """Which signing key should be used by a given archive / series. |
1905 | + """ |
1906 | + |
1907 | + __storm_table__ = 'ArchiveSigningKey' |
1908 | + |
1909 | + id = Int(primary=True) |
1910 | + |
1911 | + archive_id = Int(name="archive", allow_none=False) |
1912 | + archive = Reference(archive_id, 'Archive.id') |
1913 | + |
1914 | + earliest_distro_series_id = Int( |
1915 | + name="earliest_distro_series", allow_none=True) |
1916 | + earliest_distro_series = Reference( |
1917 | + earliest_distro_series_id, 'DistroSeries.id') |
1918 | + |
1919 | + key_type = DBEnum(enum=SigningKeyType, allow_none=False) |
1920 | + |
1921 | + signing_key_id = Int(name="signing_key", allow_none=False) |
1922 | + signing_key = Reference(signing_key_id, SigningKey.id) |
1923 | + |
1924 | + def __init__(self, archive=None, earliest_distro_series=None, |
1925 | + signing_key=None): |
1926 | + super(ArchiveSigningKey, self).__init__() |
1927 | + self.archive = archive |
1928 | + self.signing_key = signing_key |
1929 | + self.key_type = signing_key.key_type |
1930 | + self.earliest_distro_series = earliest_distro_series |
1931 | + |
1932 | + |
1933 | +@implementer(IArchiveSigningKeySet) |
1934 | +class ArchiveSigningKeySet: |
1935 | + |
1936 | + @classmethod |
1937 | + def create(cls, archive, earliest_distro_series, signing_key): |
1938 | + store = IMasterStore(SigningKey) |
1939 | + obj = ArchiveSigningKey(archive, earliest_distro_series, signing_key) |
1940 | + store.add(obj) |
1941 | + return obj |
1942 | + |
1943 | + @classmethod |
1944 | + def getSigningKey(cls, key_type, archive, distro_series): |
1945 | + store = IStore(ArchiveSigningKey) |
1946 | + # Gets all the keys of the given key_type available for the archive |
1947 | + rs = store.find(ArchiveSigningKey, |
1948 | + SigningKey.id == ArchiveSigningKey.signing_key_id, |
1949 | + SigningKey.key_type == key_type, |
1950 | + ArchiveSigningKey.key_type == key_type, |
1951 | + ArchiveSigningKey.archive == archive) |
1952 | + |
1953 | + # prefetch related signing keys to avoid extra queries. |
1954 | + signing_keys = store.find(SigningKey, [ |
1955 | + SigningKey.id.is_in([i.signing_key_id for i in rs])]) |
1956 | + signing_keys_by_id = {i.id: i for i in signing_keys} |
1957 | + |
1958 | + # Group keys per type, and per distro series |
1959 | + keys_per_series = defaultdict(dict) |
1960 | + for i in rs: |
1961 | + signing_key = signing_keys_by_id[i.signing_key_id] |
1962 | + keys_per_series[i.earliest_distro_series] = signing_key |
1963 | + |
1964 | + # Let's search the most suitable per key type. |
1965 | + found_series = False |
1966 | + # Note that archive.distribution.series is, by default, sorted by |
1967 | + # "version", reversed. |
1968 | + for series in archive.distribution.series: |
1969 | + if series == distro_series: |
1970 | + found_series = True |
1971 | + if found_series and series in keys_per_series: |
1972 | + return keys_per_series[series] |
1973 | + # If no specific key for distro_series was found, returns |
1974 | + # the keys for the archive itself (or None if no key is |
1975 | + # available for the archive either). |
1976 | + return keys_per_series.get(None) |
1977 | + |
1978 | + @classmethod |
1979 | + def generate(cls, key_type, archive, earliest_distro_series=None, |
1980 | + description=None): |
1981 | + signing_key = SigningKey.generate(key_type, description) |
1982 | + archive_signing = ArchiveSigningKeySet.create( |
1983 | + archive, earliest_distro_series, signing_key) |
1984 | + return archive_signing |
1985 | diff --git a/lib/lp/services/signing/proxy.py b/lib/lp/services/signing/proxy.py |
1986 | new file mode 100644 |
1987 | index 0000000..d8542e3 |
1988 | --- /dev/null |
1989 | +++ b/lib/lp/services/signing/proxy.py |
1990 | @@ -0,0 +1,182 @@ |
1991 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
1992 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1993 | + |
1994 | +"""Proxy calls to lp-signing service""" |
1995 | + |
1996 | +from __future__ import absolute_import, print_function, unicode_literals |
1997 | + |
1998 | +__metaclass__ = type |
1999 | + |
2000 | +import base64 |
2001 | +import json |
2002 | + |
2003 | +from lazr.restful.utils import get_current_browser_request |
2004 | +from nacl.encoding import Base64Encoder |
2005 | +from nacl.public import ( |
2006 | + Box, |
2007 | + PrivateKey, |
2008 | + PublicKey, |
2009 | + ) |
2010 | +from nacl.utils import random |
2011 | +from six.moves.urllib.parse import urljoin |
2012 | +from zope.interface import implementer |
2013 | + |
2014 | +from lp.services.config import config |
2015 | +from lp.services.propertycache import ( |
2016 | + cachedproperty, |
2017 | + get_property_cache, |
2018 | + ) |
2019 | +from lp.services.signing.enums import ( |
2020 | + SigningKeyType, |
2021 | + SigningMode, |
2022 | + ) |
2023 | +from lp.services.signing.interfaces.signingserviceclient import ( |
2024 | + ISigningServiceClient, |
2025 | + ) |
2026 | +from lp.services.timeline.requesttimeline import get_request_timeline |
2027 | +from lp.services.timeout import urlfetch |
2028 | + |
2029 | + |
2030 | +@implementer(ISigningServiceClient) |
2031 | +class SigningServiceClient: |
2032 | + """Representation of lp-signing service REST interface |
2033 | + |
2034 | + To benefit from caching, use this class as a singleton through |
2035 | + getUtility(ISigningServiceClient). |
2036 | + """ |
2037 | + |
2038 | + def _cleanCaches(self): |
2039 | + """Cleanup cached properties""" |
2040 | + del get_property_cache(self).service_public_key |
2041 | + |
2042 | + def getUrl(self, path): |
2043 | + """Shortcut to concatenate lp-signing address with the desired |
2044 | + endpoint path. |
2045 | + |
2046 | + :param path: The REST endpoint to be joined. |
2047 | + """ |
2048 | + base_url = config.signing.signing_endpoint |
2049 | + return urljoin(base_url, path) |
2050 | + |
2051 | + def _makeResponseNonce(self): |
2052 | + return random(Box.NONCE_SIZE) |
2053 | + |
2054 | + def _decryptResponseJson(self, response, response_nonce): |
2055 | + box = Box(self.private_key, self.service_public_key) |
2056 | + return json.loads(box.decrypt( |
2057 | + response.content, response_nonce, encoder=Base64Encoder)) |
2058 | + |
2059 | + def _requestJson(self, path, method="GET", **kwargs): |
2060 | + """Helper method to do an HTTP request and get back a json from the |
2061 | + signing service, raising exception if status code != 2xx. |
2062 | + |
2063 | + :param path: The endpoint path |
2064 | + :param method: The HTTP method to be used (GET, POST, etc) |
2065 | + :param needs_resp_nonce: Indicates if the endpoint requires us to |
2066 | + include a X-Response-Nonce, and returns back an encrypted |
2067 | + response JSON. |
2068 | + """ |
2069 | + timeline = get_request_timeline(get_current_browser_request()) |
2070 | + action = timeline.start( |
2071 | + "services-signing-proxy-%s" % method, "%s %s" % |
2072 | + (path, json.dumps(kwargs))) |
2073 | + |
2074 | + headers = kwargs.get("headers", {}) |
2075 | + response_nonce = None |
2076 | + if "X-Response-Nonce" in headers: |
2077 | + response_nonce = base64.b64decode(headers["X-Response-Nonce"]) |
2078 | + |
2079 | + try: |
2080 | + url = self.getUrl(path) |
2081 | + response = urlfetch(url, method=method.lower(), **kwargs) |
2082 | + response.raise_for_status() |
2083 | + if response_nonce is None: |
2084 | + return response.json() |
2085 | + else: |
2086 | + return self._decryptResponseJson(response, response_nonce) |
2087 | + finally: |
2088 | + action.finish() |
2089 | + |
2090 | + @cachedproperty |
2091 | + def service_public_key(self): |
2092 | + """Returns the lp-signing service's public key. |
2093 | + """ |
2094 | + data = self._requestJson("/service-key") |
2095 | + return PublicKey(data["service-key"], encoder=Base64Encoder) |
2096 | + |
2097 | + @property |
2098 | + def private_key(self): |
2099 | + return PrivateKey( |
2100 | + config.signing.client_private_key, encoder=Base64Encoder) |
2101 | + |
2102 | + def getNonce(self): |
2103 | + data = self._requestJson("/nonce", "POST") |
2104 | + return base64.b64decode(data["nonce"].encode("UTF-8")) |
2105 | + |
2106 | + def _getAuthHeaders(self, nonce, response_nonce): |
2107 | + """Get headers to call authenticated endpoints. |
2108 | + |
2109 | + :param nonce: The nonce bytes to be used (not the base64 encoded one!) |
2110 | + :param response_nonce: The X-Response-Nonce bytes to be used to |
2111 | + decrypt the boxed response. |
2112 | + :return: Header dict, ready to be used by requests |
2113 | + """ |
2114 | + return { |
2115 | + "Content-Type": "application/x-boxed-json", |
2116 | + "X-Client-Public-Key": config.signing.client_public_key, |
2117 | + "X-Nonce": base64.b64encode(nonce), |
2118 | + "X-Response-Nonce": base64.b64encode(response_nonce), |
2119 | + } |
2120 | + |
2121 | + def _encryptPayload(self, nonce, message): |
2122 | + """Returns the encrypted version of message, base64 encoded and |
2123 | + ready to be sent on a HTTP request to lp-signing service. |
2124 | + |
2125 | + :param nonce: The original (non-base64 encoded) nonce |
2126 | + :param message: The str message to be encrypted |
2127 | + """ |
2128 | + box = Box(self.private_key, self.service_public_key) |
2129 | + encrypted_message = box.encrypt(message, nonce, encoder=Base64Encoder) |
2130 | + return encrypted_message.ciphertext |
2131 | + |
2132 | + def generate(self, key_type, description): |
2133 | + if key_type not in SigningKeyType.items: |
2134 | + raise ValueError("%s is not a valid key type" % key_type) |
2135 | + |
2136 | + nonce = self.getNonce() |
2137 | + response_nonce = self._makeResponseNonce() |
2138 | + data = json.dumps({ |
2139 | + "key-type": key_type.name, |
2140 | + "description": description, |
2141 | + }).encode("UTF-8") |
2142 | + ret = self._requestJson( |
2143 | + "/generate", "POST", |
2144 | + headers=self._getAuthHeaders(nonce, response_nonce), |
2145 | + data=self._encryptPayload(nonce, data)) |
2146 | + return { |
2147 | + "fingerprint": ret["fingerprint"], |
2148 | + "public-key": base64.b64decode(ret["public-key"])} |
2149 | + |
2150 | + def sign(self, key_type, fingerprint, message_name, message, mode): |
2151 | + if mode not in {SigningMode.ATTACHED, SigningMode.DETACHED}: |
2152 | + raise ValueError("%s is not a valid mode" % mode) |
2153 | + if key_type not in SigningKeyType.items: |
2154 | + raise ValueError("%s is not a valid key type" % key_type) |
2155 | + |
2156 | + nonce = self.getNonce() |
2157 | + response_nonce = self._makeResponseNonce() |
2158 | + data = json.dumps({ |
2159 | + "key-type": key_type.name, |
2160 | + "fingerprint": fingerprint, |
2161 | + "message-name": message_name, |
2162 | + "message": base64.b64encode(message).decode("UTF-8"), |
2163 | + "mode": mode.name, |
2164 | + }).encode("UTF-8") |
2165 | + data = self._requestJson( |
2166 | + "/sign", "POST", |
2167 | + headers=self._getAuthHeaders(nonce, response_nonce), |
2168 | + data=self._encryptPayload(nonce, data)) |
2169 | + |
2170 | + return { |
2171 | + 'public-key': base64.b64decode(data['public-key']), |
2172 | + 'signed-message': base64.b64decode(data['signed-message'])} |
2173 | diff --git a/lib/lp/services/signing/tests/__init__.py b/lib/lp/services/signing/tests/__init__.py |
2174 | new file mode 100644 |
2175 | index 0000000..e69de29 |
2176 | --- /dev/null |
2177 | +++ b/lib/lp/services/signing/tests/__init__.py |
2178 | diff --git a/lib/lp/services/signing/tests/helpers.py b/lib/lp/services/signing/tests/helpers.py |
2179 | new file mode 100644 |
2180 | index 0000000..e01730f |
2181 | --- /dev/null |
2182 | +++ b/lib/lp/services/signing/tests/helpers.py |
2183 | @@ -0,0 +1,67 @@ |
2184 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
2185 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
2186 | + |
2187 | +"""Helper functions for code testing live here.""" |
2188 | + |
2189 | +from __future__ import absolute_import, print_function, unicode_literals |
2190 | + |
2191 | +__metaclass__ = type |
2192 | +__all__ = [ |
2193 | + 'SigningServiceClientFixture', |
2194 | + ] |
2195 | + |
2196 | +import fixtures |
2197 | +from nacl.public import PrivateKey |
2198 | +from six import text_type |
2199 | + |
2200 | +from lp.services.compat import mock |
2201 | +from lp.services.signing.interfaces.signingserviceclient import ( |
2202 | + ISigningServiceClient, |
2203 | + ) |
2204 | +from lp.testing.fixture import ZopeUtilityFixture |
2205 | + |
2206 | + |
2207 | +class SigningServiceClientFixture(fixtures.Fixture): |
2208 | + """Mock for SigningServiceClient class. |
2209 | + |
2210 | + This method fakes the API calls on generate and sign methods, |
2211 | + and provides a nice way of getting the fake returned values on |
2212 | + self.generate_returns and self.sign_returns attributes. |
2213 | + |
2214 | + Both generate_returns and sign_returns format is the following: |
2215 | + [(key_type, api_return_dict), (key_type, api_return_dict), ...]""" |
2216 | + def __init__(self, factory): |
2217 | + self.factory = factory |
2218 | + |
2219 | + self.generate = mock.Mock() |
2220 | + self.generate.side_effect = self._generate |
2221 | + |
2222 | + self.sign = mock.Mock() |
2223 | + self.sign.side_effect = self._sign |
2224 | + |
2225 | + self.generate_returns = [] |
2226 | + self.sign_returns = [] |
2227 | + |
2228 | + def _generate(self, key_type, description): |
2229 | + key = bytes(PrivateKey.generate().public_key) |
2230 | + data = { |
2231 | + "fingerprint": text_type(self.factory.getUniqueHexString(40)), |
2232 | + "public-key": key} |
2233 | + self.generate_returns.append((key_type, data)) |
2234 | + return data |
2235 | + |
2236 | + def _sign(self, key_type, fingerprint, message_name, message, mode): |
2237 | + key = bytes(PrivateKey.generate().public_key) |
2238 | + signed_msg = "signed with key_type={}".format(key_type.name) |
2239 | + data = { |
2240 | + 'public-key': key, |
2241 | + 'signed-message': signed_msg} |
2242 | + self.sign_returns.append((key_type, data)) |
2243 | + return data |
2244 | + |
2245 | + def _setUp(self): |
2246 | + self.useFixture(ZopeUtilityFixture(self, ISigningServiceClient)) |
2247 | + |
2248 | + def _cleanup(self): |
2249 | + self.generate_returns = [] |
2250 | + self.sign_returns = [] |
2251 | diff --git a/lib/lp/services/signing/tests/test_proxy.py b/lib/lp/services/signing/tests/test_proxy.py |
2252 | new file mode 100644 |
2253 | index 0000000..a972ea3 |
2254 | --- /dev/null |
2255 | +++ b/lib/lp/services/signing/tests/test_proxy.py |
2256 | @@ -0,0 +1,318 @@ |
2257 | +# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
2258 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
2259 | + |
2260 | +__metaclass__ = type |
2261 | + |
2262 | +import base64 |
2263 | +import json |
2264 | + |
2265 | +from fixtures import MockPatch |
2266 | +from fixtures.testcase import TestWithFixtures |
2267 | +from nacl.encoding import Base64Encoder |
2268 | +from nacl.public import ( |
2269 | + Box, |
2270 | + PrivateKey, |
2271 | + PublicKey, |
2272 | + ) |
2273 | +from nacl.utils import random |
2274 | +import responses |
2275 | +from testtools.matchers import ( |
2276 | + ContainsDict, |
2277 | + Equals, |
2278 | + ) |
2279 | +from zope.component import getUtility |
2280 | +from zope.security.proxy import removeSecurityProxy |
2281 | + |
2282 | +from lp.services.config import config |
2283 | +from lp.services.signing.enums import ( |
2284 | + SigningKeyType, |
2285 | + SigningMode, |
2286 | + ) |
2287 | +from lp.services.signing.interfaces.signingserviceclient import ( |
2288 | + ISigningServiceClient, |
2289 | + ) |
2290 | +from lp.services.signing.proxy import SigningServiceClient |
2291 | +from lp.testing import TestCaseWithFactory |
2292 | +from lp.testing.layers import ZopelessLayer |
2293 | + |
2294 | + |
2295 | +class SigningServiceResponseFactory: |
2296 | + """Factory for fake responses from lp-signing service. |
2297 | + |
2298 | + This class is a helper to pretend that lp-signing service is running by |
2299 | + mocking `requests` module, and returning fake responses from |
2300 | + response.get(url) and response.post(url). See `patch` method. |
2301 | + """ |
2302 | + def __init__(self): |
2303 | + self.service_private_key = PrivateKey.generate() |
2304 | + self.service_public_key = self.service_private_key.public_key |
2305 | + self.b64_service_public_key = self.service_public_key.encode( |
2306 | + encoder=Base64Encoder).decode("UTF-8") |
2307 | + |
2308 | + self.client_private_key = PrivateKey( |
2309 | + config.signing.client_private_key, encoder=Base64Encoder) |
2310 | + self.client_public_key = self.client_private_key.public_key |
2311 | + |
2312 | + self.nonce = random(Box.NONCE_SIZE) |
2313 | + self.b64_nonce = base64.b64encode(self.nonce).decode("UTF-8") |
2314 | + |
2315 | + self.generated_public_key = PrivateKey.generate().public_key |
2316 | + self.b64_generated_public_key = base64.b64encode( |
2317 | + bytes(self.generated_public_key)) |
2318 | + self.generated_fingerprint = ( |
2319 | + u'338D218488DFD597D8FCB9C328C3E9D9ADA16CEE') |
2320 | + self.b64_signed_msg = base64.b64encode(b"the-signed-msg") |
2321 | + |
2322 | + self.signed_msg_template = "%s::signed!" |
2323 | + |
2324 | + @classmethod |
2325 | + def getUrl(cls, path): |
2326 | + """Shortcut to get full path of an endpoint at lp-signing. |
2327 | + """ |
2328 | + return SigningServiceClient().getUrl(path) |
2329 | + |
2330 | + def _encryptPayload(self, data, nonce): |
2331 | + """Translated the given data dict as a boxed json, encrypted as |
2332 | + lp-signing would do.""" |
2333 | + box = Box(self.service_private_key, self.client_public_key) |
2334 | + return box.encrypt( |
2335 | + json.dumps(data), nonce, encoder=Base64Encoder).ciphertext |
2336 | + |
2337 | + def getAPISignedContent(self, call_index=0): |
2338 | + """Returns the signed message returned by the API. |
2339 | + |
2340 | + This is a shortcut to avoid inspecting and decrypting API calls, |
2341 | + since we know that the content of /sign calls are hardcoded by this |
2342 | + fixture. |
2343 | + """ |
2344 | + return self.signed_msg_template % (call_index + 1) |
2345 | + |
2346 | + def addResponses(self, test_case): |
2347 | + """Patches all requests with default test values. |
2348 | + |
2349 | + This method uses `responses` module to mock `requests`. You should use |
2350 | + @responses.activate decorator in your test method before |
2351 | + calling this method. |
2352 | + |
2353 | + See https://github.com/getsentry/responses for details on how to |
2354 | + inspect the HTTP calls made. |
2355 | + |
2356 | + Other helpful attributes are: |
2357 | + - self.b64_service_public_key |
2358 | + - self.b64_nonce |
2359 | + - self.generated_public_key |
2360 | + - self.generated_fingerprint |
2361 | + which holds the respective values used in the default fake responses. |
2362 | + |
2363 | + The /sign endpoint will return, as signed message, "$n::signed!", |
2364 | + where $n is the call number (base64-encoded, as lp-signing would |
2365 | + return). This could be useful on black-box tests, where several |
2366 | + calls to /sign would be done and the response should be checked. |
2367 | + """ |
2368 | + # Patch SigningServiceClient._makeResponseNonce to return always the |
2369 | + # same nonce, to simplify the tests. |
2370 | + response_nonce = random(Box.NONCE_SIZE) |
2371 | + test_case.useFixture(MockPatch( |
2372 | + 'lp.services.signing.proxy.SigningServiceClient.' |
2373 | + '_makeResponseNonce', |
2374 | + return_value=response_nonce)) |
2375 | + |
2376 | + responses.add( |
2377 | + responses.GET, self.getUrl("/service-key"), |
2378 | + json={"service-key": self.b64_service_public_key.decode('utf8')}, |
2379 | + status=200) |
2380 | + |
2381 | + responses.add( |
2382 | + responses.POST, self.getUrl("/nonce"), |
2383 | + json={"nonce": self.b64_nonce.decode('utf8')}, status=201) |
2384 | + |
2385 | + responses.add( |
2386 | + responses.POST, self.getUrl("/generate"), |
2387 | + body=self._encryptPayload({ |
2388 | + 'fingerprint': self.generated_fingerprint, |
2389 | + 'public-key': self.b64_generated_public_key.decode('utf8') |
2390 | + }, nonce=response_nonce), |
2391 | + status=201) |
2392 | + call_counts = {'/sign': 0} |
2393 | + |
2394 | + def sign_callback(request): |
2395 | + call_counts['/sign'] += 1 |
2396 | + signed = base64.b64encode( |
2397 | + self.signed_msg_template % call_counts['/sign']) |
2398 | + data = {'signed-message': signed.decode('utf8'), |
2399 | + 'public-key': self.b64_generated_public_key.decode('utf8')} |
2400 | + return 201, {}, self._encryptPayload(data, response_nonce) |
2401 | + |
2402 | + responses.add_callback( |
2403 | + responses.POST, self.getUrl("/sign"), |
2404 | + callback=sign_callback) |
2405 | + |
2406 | + |
2407 | +class SigningServiceProxyTest(TestCaseWithFactory, TestWithFixtures): |
2408 | + """Tests signing service without actually making calls to lp-signing. |
2409 | + |
2410 | + Every REST call is mocked using self.response_factory, and most of this |
2411 | + class's work is actually calling those endpoints. So, many things are |
2412 | + mocked here, returning fake responses created at |
2413 | + SigningServiceResponseFactory. |
2414 | + """ |
2415 | + layer = ZopelessLayer |
2416 | + |
2417 | + def setUp(self, *args, **kwargs): |
2418 | + super(TestCaseWithFactory, self).setUp(*args, **kwargs) |
2419 | + self.response_factory = SigningServiceResponseFactory() |
2420 | + |
2421 | + client = removeSecurityProxy(getUtility(ISigningServiceClient)) |
2422 | + self.addCleanup(client._cleanCaches) |
2423 | + |
2424 | + @responses.activate |
2425 | + def test_get_service_public_key(self): |
2426 | + self.response_factory.addResponses(self) |
2427 | + |
2428 | + signing = getUtility(ISigningServiceClient) |
2429 | + key = removeSecurityProxy(signing.service_public_key) |
2430 | + |
2431 | + # Asserts that the public key is correct. |
2432 | + self.assertIsInstance(key, PublicKey) |
2433 | + self.assertEqual( |
2434 | + key.encode(Base64Encoder), |
2435 | + self.response_factory.b64_service_public_key) |
2436 | + |
2437 | + # Checks that the HTTP call was made |
2438 | + self.assertEqual(1, len(responses.calls)) |
2439 | + call = responses.calls[0] |
2440 | + self.assertEqual("GET", call.request.method) |
2441 | + self.assertEqual( |
2442 | + self.response_factory.getUrl("/service-key"), call.request.url) |
2443 | + |
2444 | + @responses.activate |
2445 | + def test_get_nonce(self): |
2446 | + self.response_factory.addResponses(self) |
2447 | + |
2448 | + signing = getUtility(ISigningServiceClient) |
2449 | + nonce = signing.getNonce() |
2450 | + |
2451 | + self.assertEqual( |
2452 | + base64.b64encode(nonce), self.response_factory.b64_nonce) |
2453 | + |
2454 | + # Checks that the HTTP call was made |
2455 | + self.assertEqual(1, len(responses.calls)) |
2456 | + call = responses.calls[0] |
2457 | + self.assertEqual("POST", call.request.method) |
2458 | + self.assertEqual( |
2459 | + self.response_factory.getUrl("/nonce"), call.request.url) |
2460 | + |
2461 | + @responses.activate |
2462 | + def test_generate_unknown_key_type_raises_exception(self): |
2463 | + self.response_factory.addResponses(self) |
2464 | + |
2465 | + signing = getUtility(ISigningServiceClient) |
2466 | + self.assertRaises( |
2467 | + ValueError, signing.generate, "banana", "Wrong key type") |
2468 | + self.assertEqual(0, len(responses.calls)) |
2469 | + |
2470 | + @responses.activate |
2471 | + def test_generate_key(self): |
2472 | + """Makes sure that the SigningService.generate method calls the |
2473 | + correct endpoints |
2474 | + """ |
2475 | + self.response_factory.addResponses(self) |
2476 | + # Generate the key, and checks if we got back the correct dict. |
2477 | + signing = getUtility(ISigningServiceClient) |
2478 | + generated = signing.generate(SigningKeyType.UEFI, "my lp test key") |
2479 | + |
2480 | + self.assertEqual(generated, { |
2481 | + 'public-key': bytes(self.response_factory.generated_public_key), |
2482 | + 'fingerprint': self.response_factory.generated_fingerprint}) |
2483 | + |
2484 | + self.assertEqual(3, len(responses.calls)) |
2485 | + |
2486 | + # expected order of HTTP calls |
2487 | + http_nonce, http_service_key, http_generate = responses.calls |
2488 | + |
2489 | + self.assertEqual("POST", http_nonce.request.method) |
2490 | + self.assertEqual( |
2491 | + self.response_factory.getUrl("/nonce"), http_nonce.request.url) |
2492 | + |
2493 | + self.assertEqual("GET", http_service_key.request.method) |
2494 | + self.assertEqual( |
2495 | + self.response_factory.getUrl("/service-key"), |
2496 | + http_service_key.request.url) |
2497 | + |
2498 | + self.assertEqual("POST", http_generate.request.method) |
2499 | + self.assertEqual( |
2500 | + self.response_factory.getUrl("/generate"), |
2501 | + http_generate.request.url) |
2502 | + self.assertThat(http_generate.request.headers, ContainsDict({ |
2503 | + "Content-Type": Equals("application/x-boxed-json"), |
2504 | + "X-Client-Public-Key": Equals(config.signing.client_public_key), |
2505 | + "X-Nonce": Equals(self.response_factory.b64_nonce)})) |
2506 | + self.assertIsNotNone(http_generate.request.body) |
2507 | + |
2508 | + @responses.activate |
2509 | + def test_sign_invalid_mode(self): |
2510 | + signing = getUtility(ISigningServiceClient) |
2511 | + self.assertRaises( |
2512 | + ValueError, signing.sign, |
2513 | + SigningKeyType.UEFI, 'fingerprint', 'message_name', 'message', |
2514 | + 'NO-MODE') |
2515 | + self.assertEqual(0, len(responses.calls)) |
2516 | + |
2517 | + @responses.activate |
2518 | + def test_sign_invalid_key_type(self): |
2519 | + signing = getUtility(ISigningServiceClient) |
2520 | + self.assertRaises( |
2521 | + ValueError, signing.sign, |
2522 | + 'shrug', 'fingerprint', 'message_name', 'message', |
2523 | + SigningMode.ATTACHED) |
2524 | + self.assertEqual(0, len(responses.calls)) |
2525 | + |
2526 | + @responses.activate |
2527 | + def test_sign(self): |
2528 | + """Runs through SignService.sign() flow""" |
2529 | + # Replace GET /service-key response by our mock. |
2530 | + resp_factory = self.response_factory |
2531 | + resp_factory.addResponses(self) |
2532 | + |
2533 | + fingerprint = self.factory.getUniqueHexString(40).upper() |
2534 | + key_type = SigningKeyType.KMOD |
2535 | + mode = SigningMode.DETACHED |
2536 | + message_name = 'my test msg' |
2537 | + message = 'this is the message content' |
2538 | + |
2539 | + signing = getUtility(ISigningServiceClient) |
2540 | + data = signing.sign( |
2541 | + key_type, fingerprint, message_name, message, mode) |
2542 | + |
2543 | + self.assertEqual(3, len(responses.calls)) |
2544 | + # expected order of HTTP calls |
2545 | + http_nonce, http_service_key, http_sign = responses.calls |
2546 | + |
2547 | + self.assertEqual("POST", http_nonce.request.method) |
2548 | + self.assertEqual( |
2549 | + self.response_factory.getUrl("/nonce"), http_nonce.request.url) |
2550 | + |
2551 | + self.assertEqual("GET", http_service_key.request.method) |
2552 | + self.assertEqual( |
2553 | + self.response_factory.getUrl("/service-key"), |
2554 | + http_service_key.request.url) |
2555 | + |
2556 | + self.assertEqual("POST", http_sign.request.method) |
2557 | + self.assertEqual( |
2558 | + self.response_factory.getUrl("/sign"), |
2559 | + http_sign.request.url) |
2560 | + self.assertThat(http_sign.request.headers, ContainsDict({ |
2561 | + "Content-Type": Equals("application/x-boxed-json"), |
2562 | + "X-Client-Public-Key": Equals(config.signing.client_public_key), |
2563 | + "X-Nonce": Equals(self.response_factory.b64_nonce)})) |
2564 | + self.assertIsNotNone(http_sign.request.body) |
2565 | + |
2566 | + # It should have returned the correct JSON content, with signed |
2567 | + # message from the API and the public-key. |
2568 | + self.assertEqual(2, len(data)) |
2569 | + self.assertEqual( |
2570 | + self.response_factory.getAPISignedContent(), |
2571 | + data['signed-message']) |
2572 | + self.assertEqual( |
2573 | + bytes(self.response_factory.generated_public_key), |
2574 | + data['public-key']) |
2575 | diff --git a/lib/lp/services/signing/tests/test_signingkey.py b/lib/lp/services/signing/tests/test_signingkey.py |
2576 | new file mode 100644 |
2577 | index 0000000..2e0a5df |
2578 | --- /dev/null |
2579 | +++ b/lib/lp/services/signing/tests/test_signingkey.py |
2580 | @@ -0,0 +1,244 @@ |
2581 | +# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
2582 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
2583 | + |
2584 | +__metaclass__ = type |
2585 | + |
2586 | +import base64 |
2587 | + |
2588 | +from fixtures.testcase import TestWithFixtures |
2589 | +import responses |
2590 | +from storm.store import Store |
2591 | +from testtools.matchers import MatchesStructure |
2592 | +from zope.component import getUtility |
2593 | +from zope.security.proxy import removeSecurityProxy |
2594 | + |
2595 | +from lp.services.database.interfaces import IMasterStore |
2596 | +from lp.services.signing.enums import SigningKeyType |
2597 | +from lp.services.signing.interfaces.signingkey import IArchiveSigningKeySet |
2598 | +from lp.services.signing.interfaces.signingserviceclient import ( |
2599 | + ISigningServiceClient, |
2600 | + ) |
2601 | +from lp.services.signing.model.signingkey import ( |
2602 | + ArchiveSigningKey, |
2603 | + SigningKey, |
2604 | + ) |
2605 | +from lp.services.signing.tests.test_proxy import SigningServiceResponseFactory |
2606 | +from lp.testing import TestCaseWithFactory |
2607 | +from lp.testing.layers import ZopelessDatabaseLayer |
2608 | + |
2609 | + |
2610 | +class TestSigningKey(TestCaseWithFactory, TestWithFixtures): |
2611 | + |
2612 | + layer = ZopelessDatabaseLayer |
2613 | + |
2614 | + def setUp(self, *args, **kwargs): |
2615 | + super(TestSigningKey, self).setUp(*args, **kwargs) |
2616 | + self.signing_service = SigningServiceResponseFactory() |
2617 | + |
2618 | + client = removeSecurityProxy(getUtility(ISigningServiceClient)) |
2619 | + self.addCleanup(client._cleanCaches) |
2620 | + |
2621 | + @responses.activate |
2622 | + def test_generate_signing_key_saves_correctly(self): |
2623 | + self.signing_service.addResponses(self) |
2624 | + |
2625 | + key = SigningKey.generate(SigningKeyType.UEFI, u"this is my key") |
2626 | + self.assertIsInstance(key, SigningKey) |
2627 | + |
2628 | + store = IMasterStore(SigningKey) |
2629 | + store.invalidate() |
2630 | + |
2631 | + rs = store.find(SigningKey) |
2632 | + self.assertEqual(1, rs.count()) |
2633 | + db_key = rs.one() |
2634 | + |
2635 | + self.assertEqual(SigningKeyType.UEFI, db_key.key_type) |
2636 | + self.assertEqual( |
2637 | + self.signing_service.generated_fingerprint, db_key.fingerprint) |
2638 | + self.assertEqual( |
2639 | + self.signing_service.b64_generated_public_key, |
2640 | + base64.b64encode(db_key.public_key)) |
2641 | + self.assertEqual("this is my key", db_key.description) |
2642 | + |
2643 | + @responses.activate |
2644 | + def test_sign_some_data(self): |
2645 | + self.signing_service.addResponses(self) |
2646 | + |
2647 | + s = SigningKey( |
2648 | + SigningKeyType.UEFI, u"a fingerprint", |
2649 | + bytes(self.signing_service.generated_public_key), |
2650 | + description=u"This is my key!") |
2651 | + signed = s.sign("secure message", "message_name") |
2652 | + |
2653 | + # Checks if the returned value is actually the returning value from |
2654 | + # HTTP POST /sign call to lp-signing service |
2655 | + self.assertEqual(3, len(responses.calls)) |
2656 | + self.assertEqual(self.signing_service.getAPISignedContent(), signed) |
2657 | + |
2658 | + |
2659 | +class TestArchiveSigningKey(TestCaseWithFactory): |
2660 | + layer = ZopelessDatabaseLayer |
2661 | + |
2662 | + def setUp(self, *args, **kwargs): |
2663 | + super(TestArchiveSigningKey, self).setUp(*args, **kwargs) |
2664 | + self.signing_service = SigningServiceResponseFactory() |
2665 | + |
2666 | + client = removeSecurityProxy(getUtility(ISigningServiceClient)) |
2667 | + self.addCleanup(client._cleanCaches) |
2668 | + |
2669 | + @responses.activate |
2670 | + def test_generate_saves_correctly(self): |
2671 | + self.signing_service.addResponses(self) |
2672 | + |
2673 | + archive = self.factory.makeArchive() |
2674 | + distro_series = archive.distribution.series[0] |
2675 | + |
2676 | + arch_key = getUtility(IArchiveSigningKeySet).generate( |
2677 | + SigningKeyType.UEFI, archive, earliest_distro_series=distro_series, |
2678 | + description=u"some description") |
2679 | + |
2680 | + store = Store.of(arch_key) |
2681 | + store.invalidate() |
2682 | + |
2683 | + rs = store.find(ArchiveSigningKey) |
2684 | + self.assertEqual(1, rs.count()) |
2685 | + |
2686 | + db_arch_key = rs.one() |
2687 | + self.assertThat(db_arch_key, MatchesStructure.byEquality( |
2688 | + key_type=SigningKeyType.UEFI, archive=archive, |
2689 | + earliest_distro_series=distro_series)) |
2690 | + |
2691 | + self.assertThat(db_arch_key.signing_key, MatchesStructure.byEquality( |
2692 | + key_type=SigningKeyType.UEFI, description=u"some description", |
2693 | + fingerprint=self.signing_service.generated_fingerprint, |
2694 | + public_key=bytes(self.signing_service.generated_public_key))) |
2695 | + |
2696 | + def test_create(self): |
2697 | + archive = self.factory.makeArchive() |
2698 | + distro_series = archive.distribution.series[0] |
2699 | + signing_key = self.factory.makeSigningKey() |
2700 | + |
2701 | + arch_signing_key_set = getUtility(IArchiveSigningKeySet) |
2702 | + |
2703 | + arch_key = arch_signing_key_set.create( |
2704 | + archive, distro_series, signing_key) |
2705 | + |
2706 | + store = Store.of(arch_key) |
2707 | + store.invalidate() |
2708 | + rs = store.find(ArchiveSigningKey) |
2709 | + |
2710 | + self.assertEqual(1, rs.count()) |
2711 | + db_arch_key = rs.one() |
2712 | + self.assertThat(db_arch_key, MatchesStructure.byEquality( |
2713 | + key_type=signing_key.key_type, archive=archive, |
2714 | + earliest_distro_series=distro_series, |
2715 | + signing_key=signing_key)) |
2716 | + |
2717 | + # Saving another type should create a new entry |
2718 | + signing_key_from_another_type = self.factory.makeSigningKey( |
2719 | + key_type=SigningKeyType.KMOD) |
2720 | + arch_signing_key_set.create( |
2721 | + archive, distro_series, signing_key_from_another_type) |
2722 | + |
2723 | + self.assertEqual(2, store.find(ArchiveSigningKey).count()) |
2724 | + |
2725 | + def test_get_signing_keys_without_distro_series_configured(self): |
2726 | + UEFI = SigningKeyType.UEFI |
2727 | + KMOD = SigningKeyType.KMOD |
2728 | + |
2729 | + archive = self.factory.makeArchive() |
2730 | + distro_series = archive.distribution.series[0] |
2731 | + uefi_key = self.factory.makeSigningKey( |
2732 | + key_type=SigningKeyType.UEFI) |
2733 | + kmod_key = self.factory.makeSigningKey( |
2734 | + key_type=SigningKeyType.KMOD) |
2735 | + |
2736 | + # Fill the database with keys from other archives to make sure we |
2737 | + # are filtering it out |
2738 | + other_archive = self.factory.makeArchive() |
2739 | + arch_signing_key_set = getUtility(IArchiveSigningKeySet) |
2740 | + arch_signing_key_set.create( |
2741 | + other_archive, None, self.factory.makeSigningKey()) |
2742 | + |
2743 | + # Create a key for the archive (no specific series) |
2744 | + arch_uefi_key = arch_signing_key_set.create( |
2745 | + archive, None, uefi_key) |
2746 | + arch_kmod_key = arch_signing_key_set.create( |
2747 | + archive, None, kmod_key) |
2748 | + |
2749 | + # Should find the keys if we ask for the archive key |
2750 | + self.assertEqual( |
2751 | + arch_uefi_key.signing_key, |
2752 | + arch_signing_key_set.getSigningKey(UEFI, archive, None)) |
2753 | + self.assertEqual( |
2754 | + arch_kmod_key.signing_key, |
2755 | + arch_signing_key_set.getSigningKey(KMOD, archive, None)) |
2756 | + |
2757 | + # Should find the key if we ask for archive + distro_series key |
2758 | + self.assertEqual( |
2759 | + arch_uefi_key.signing_key, |
2760 | + arch_signing_key_set.getSigningKey(UEFI, archive, distro_series)) |
2761 | + self.assertEqual( |
2762 | + arch_kmod_key.signing_key, |
2763 | + arch_signing_key_set.getSigningKey(KMOD, archive, distro_series)) |
2764 | + |
2765 | + def test_get_signing_keys_with_distro_series_configured(self): |
2766 | + UEFI = SigningKeyType.UEFI |
2767 | + KMOD = SigningKeyType.KMOD |
2768 | + |
2769 | + archive = self.factory.makeArchive() |
2770 | + series = archive.distribution.series |
2771 | + uefi_key = self.factory.makeSigningKey(key_type=UEFI) |
2772 | + kmod_key = self.factory.makeSigningKey(key_type=KMOD) |
2773 | + |
2774 | + # Fill the database with keys from other archives to make sure we |
2775 | + # are filtering it out |
2776 | + other_archive = self.factory.makeArchive() |
2777 | + arch_signing_key_set = getUtility(IArchiveSigningKeySet) |
2778 | + arch_signing_key_set.create( |
2779 | + other_archive, None, self.factory.makeSigningKey()) |
2780 | + |
2781 | + # Create a key for the archive (no specific series) |
2782 | + arch_uefi_key = arch_signing_key_set.create( |
2783 | + archive, None, uefi_key) |
2784 | + |
2785 | + # for kmod, should give back this one if provided a |
2786 | + # newer distro series |
2787 | + arch_kmod_key = arch_signing_key_set.create( |
2788 | + archive, series[1], kmod_key) |
2789 | + old_arch_kmod_key = arch_signing_key_set.create( |
2790 | + archive, series[2], kmod_key) |
2791 | + |
2792 | + # If no distroseries is specified, it should give back no KMOD key, |
2793 | + # since we don't have a default |
2794 | + self.assertEqual( |
2795 | + arch_uefi_key.signing_key, |
2796 | + arch_signing_key_set.getSigningKey(UEFI, archive, None)) |
2797 | + self.assertEqual( |
2798 | + None, |
2799 | + arch_signing_key_set.getSigningKey(KMOD, archive, None)) |
2800 | + |
2801 | + # For the most recent series, use the KMOD key we've set for the |
2802 | + # previous one |
2803 | + self.assertEqual( |
2804 | + arch_uefi_key.signing_key, |
2805 | + arch_signing_key_set.getSigningKey(UEFI, archive, series[0])) |
2806 | + self.assertEqual( |
2807 | + arch_kmod_key.signing_key, |
2808 | + arch_signing_key_set.getSigningKey(KMOD, archive, series[0])) |
2809 | + |
2810 | + # For the previous series, we have a KMOD key configured |
2811 | + self.assertEqual( |
2812 | + arch_uefi_key.signing_key, |
2813 | + arch_signing_key_set.getSigningKey(UEFI, archive, series[1])) |
2814 | + self.assertEqual( |
2815 | + arch_kmod_key.signing_key, |
2816 | + arch_signing_key_set.getSigningKey(KMOD, archive, series[1])) |
2817 | + |
2818 | + # For the old series, we have an old KMOD key configured |
2819 | + self.assertEqual( |
2820 | + arch_uefi_key.signing_key, |
2821 | + arch_signing_key_set.getSigningKey(UEFI, archive, series[2])) |
2822 | + self.assertEqual( |
2823 | + old_arch_kmod_key.signing_key, |
2824 | + arch_signing_key_set.getSigningKey(KMOD, archive, series[2])) |
2825 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
2826 | index 8e0f1c1..a3b7736 100644 |
2827 | --- a/lib/lp/testing/factory.py |
2828 | +++ b/lib/lp/testing/factory.py |
2829 | @@ -283,6 +283,9 @@ from lp.services.propertycache import ( |
2830 | clear_property_cache, |
2831 | get_property_cache, |
2832 | ) |
2833 | +from lp.services.signing.enums import SigningKeyType |
2834 | +from lp.services.signing.interfaces.signingkey import IArchiveSigningKeySet |
2835 | +from lp.services.signing.model.signingkey import SigningKey |
2836 | from lp.services.temporaryblobstorage.interfaces import ( |
2837 | ITemporaryStorageManager, |
2838 | ) |
2839 | @@ -4190,6 +4193,32 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
2840 | removeSecurityProxy(bpr).datecreated = date_created |
2841 | return bpr |
2842 | |
2843 | + def makeSigningKey(self, key_type=None, fingerprint=None, |
2844 | + public_key=None, description=None): |
2845 | + """Makes a SigningKey (integration with lp-signing) |
2846 | + """ |
2847 | + if key_type is None: |
2848 | + key_type = SigningKeyType.UEFI |
2849 | + if fingerprint is None: |
2850 | + fingerprint = self.getUniqueUnicode('fingerprint') |
2851 | + if public_key is None: |
2852 | + public_key = self.getUniqueHexString(64) |
2853 | + store = IMasterStore(SigningKey) |
2854 | + signing_key = SigningKey( |
2855 | + key_type=key_type, fingerprint=fingerprint, public_key=public_key, |
2856 | + description=description) |
2857 | + store.add(signing_key) |
2858 | + return signing_key |
2859 | + |
2860 | + def makeArchiveSigningKey(self, archive=None, distro_series=None, |
2861 | + signing_key=None): |
2862 | + if archive is None: |
2863 | + archive = self.makeArchive() |
2864 | + if signing_key is None: |
2865 | + signing_key = self.makeSigningKey() |
2866 | + return getUtility(IArchiveSigningKeySet).create( |
2867 | + archive, distro_series, signing_key) |
2868 | + |
2869 | def makeSection(self, name=None): |
2870 | """Make a `Section`.""" |
2871 | if name is None: |
The database patch has been deployed to production now, so it should be OK to try landing this again.