Merge lp:~apw/launchpad/signing-add-kernel-module-signing into lp:launchpad
- signing-add-kernel-module-signing
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18054 | ||||
Proposed branch: | lp:~apw/launchpad/signing-add-kernel-module-signing | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
907 lines (+554/-118) 2 files modified
lib/lp/archivepublisher/signing.py (+182/-60) lib/lp/archivepublisher/tests/test_signing.py (+372/-58) |
||||
To merge this branch: | bzr merge lp:~apw/launchpad/signing-add-kernel-module-signing | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+294948@code.launchpad.net |
Commit message
Add Kernel Module (KMod) signing support to the raw-signing custom uploader and add support for signing options (tarball output and signature only modes).
Description of the change
Add Kernel Module (KMod) signing support to the raw-signing custom uploader and add support for signing options (tarball output and signature only modes).
This update adds support for signing kernel modules (.ko) files. A new kernel signing key is used (generated for PPAs) to create detached signatures for those modules. Appaendable signatures are generated and placed in module.ko.sig.
This update also adds support for raw-signing signing options. These are specified in the raw-signing tarball via the raw-signing.options files. Each line of this file specifies an option. There are two options supported:
1) tarball -- this indicates that rather than exposing all of the signing artifacts in dists directly they should be converted into a signing.tar.gz in the version directory, and
2) sigonly -- this indicates that we only want the signing artifacts not the original files.
Colin Watson (cjwatson) : | # |
Andy Whitcroft (apw) wrote : | # |
Colin Watson (cjwatson) : | # |
Andy Whitcroft (apw) wrote : | # |
That fail thing, I meant to confirm it was valid. Anyhow fixed up those remaining issues and pushed them up. Thanks for the reviews.
Preview Diff
1 | === modified file 'lib/lp/archivepublisher/signing.py' |
2 | --- lib/lp/archivepublisher/signing.py 2016-05-11 15:31:20 +0000 |
3 | +++ lib/lp/archivepublisher/signing.py 2016-05-19 16:56:11 +0000 |
4 | @@ -9,6 +9,8 @@ |
5 | secure to hold signing keys, so we sign them as a custom upload instead. |
6 | """ |
7 | |
8 | +from __future__ import print_function |
9 | + |
10 | __metaclass__ = type |
11 | |
12 | __all__ = [ |
13 | @@ -17,12 +19,26 @@ |
14 | ] |
15 | |
16 | import os |
17 | +import shutil |
18 | import subprocess |
19 | +import tarfile |
20 | +import tempfile |
21 | +import textwrap |
22 | |
23 | -from lp.archivepublisher.customupload import CustomUpload |
24 | +from lp.archivepublisher.customupload import ( |
25 | + CustomUpload, |
26 | + CustomUploadError, |
27 | + ) |
28 | from lp.services.osutils import remove_if_exists |
29 | |
30 | |
31 | +class SigningUploadPackError(CustomUploadError): |
32 | + def __init__(self, tarfile_path, exc): |
33 | + message = "Problem building tarball '%s': %s" % ( |
34 | + tarfile_path, exc) |
35 | + CustomUploadError.__init__(self, message) |
36 | + |
37 | + |
38 | class SigningUpload(CustomUpload): |
39 | """Signing custom upload. |
40 | |
41 | @@ -67,12 +83,16 @@ |
42 | if self.logger is not None: |
43 | self.logger.warning( |
44 | "No signing root configured for this archive") |
45 | - self.key = None |
46 | - self.cert = None |
47 | + self.uefi_key = None |
48 | + self.uefi_cert = None |
49 | + self.kmod_pem = None |
50 | + self.kmod_x509 = None |
51 | self.autokey = False |
52 | else: |
53 | - self.key = os.path.join(pubconf.signingroot, "uefi.key") |
54 | - self.cert = os.path.join(pubconf.signingroot, "uefi.crt") |
55 | + self.uefi_key = os.path.join(pubconf.signingroot, "uefi.key") |
56 | + self.uefi_cert = os.path.join(pubconf.signingroot, "uefi.crt") |
57 | + self.kmod_pem = os.path.join(pubconf.signingroot, "kmod.pem") |
58 | + self.kmod_x509 = os.path.join(pubconf.signingroot, "kmod.x509") |
59 | self.autokey = pubconf.signingautokey |
60 | |
61 | self.setComponents(tarfile_path) |
62 | @@ -99,6 +119,19 @@ |
63 | dists_signed, "%s-%s" % (self.package, self.arch)) |
64 | self.archiveroot = pubconf.archiveroot |
65 | |
66 | + def setSigningOptions(self): |
67 | + """Find and extract raw-signing.options from the tarball.""" |
68 | + self.signing_options = {} |
69 | + |
70 | + options_file = os.path.join(self.tmpdir, self.version, |
71 | + "raw-signing.options") |
72 | + if not os.path.exists(options_file): |
73 | + return |
74 | + |
75 | + with open(options_file) as options_fd: |
76 | + for option in options_fd: |
77 | + self.signing_options[option.strip()] = True |
78 | + |
79 | @classmethod |
80 | def getSeriesKey(cls, tarfile_path): |
81 | try: |
82 | @@ -107,81 +140,164 @@ |
83 | except ValueError: |
84 | return None |
85 | |
86 | - def findEfiFilenames(self): |
87 | - """Find all the *.efi files in an extracted tarball.""" |
88 | + def getArchiveOwnerAndName(self): |
89 | + # XXX apw 2016-05-18: pull out the PPA owner and name to seed key CN |
90 | + archive_name = os.path.dirname(self.archiveroot) |
91 | + owner_name = os.path.basename(os.path.dirname(archive_name)) |
92 | + archive_name = os.path.basename(archive_name) |
93 | + |
94 | + return owner_name + ' ' + archive_name |
95 | + |
96 | + def callLog(self, description, cmdl): |
97 | + status = subprocess.call(cmdl) |
98 | + if status != 0: |
99 | + # Just log this rather than failing, since custom upload errors |
100 | + # tend to make the publisher rather upset. |
101 | + if self.logger is not None: |
102 | + self.logger.warning("%s Failed (cmd='%s')" % |
103 | + (description, " ".join(cmdl))) |
104 | + return status |
105 | + |
106 | + def findSigningHandlers(self): |
107 | + """Find all the signable files in an extracted tarball.""" |
108 | for dirpath, dirnames, filenames in os.walk(self.tmpdir): |
109 | for filename in filenames: |
110 | if filename.endswith(".efi"): |
111 | - yield os.path.join(dirpath, filename) |
112 | + yield (os.path.join(dirpath, filename), self.signUefi) |
113 | + elif filename.endswith(".ko"): |
114 | + yield (os.path.join(dirpath, filename), self.signKmod) |
115 | + |
116 | + def getKeys(self, which, generate, *keynames): |
117 | + """Validate and return the uefi key and cert for encryption.""" |
118 | + |
119 | + if self.autokey: |
120 | + for keyfile in keynames: |
121 | + if keyfile and not os.path.exists(keyfile): |
122 | + generate() |
123 | + break |
124 | + |
125 | + valid = True |
126 | + for keyfile in keynames: |
127 | + if keyfile and not os.access(keyfile, os.R_OK): |
128 | + if self.logger is not None: |
129 | + self.logger.warning( |
130 | + "%s key %s not readable" % (which, keyfile)) |
131 | + valid = False |
132 | + |
133 | + if not valid: |
134 | + return [None for k in keynames] |
135 | + return keynames |
136 | |
137 | def generateUefiKeys(self): |
138 | """Generate new UEFI Keys for this archive.""" |
139 | - directory = os.path.dirname(self.key) |
140 | + directory = os.path.dirname(self.uefi_key) |
141 | if not os.path.exists(directory): |
142 | os.makedirs(directory) |
143 | |
144 | - # XXX: pull out the PPA owner and name to seed key CN |
145 | - archive_name = os.path.dirname(self.archiveroot) |
146 | - owner_name = os.path.basename(os.path.dirname(archive_name)) |
147 | - archive_name = os.path.basename(archive_name) |
148 | - common_name = '/CN=PPA ' + owner_name + ' ' + archive_name + '/' |
149 | + common_name = '/CN=PPA ' + self.getArchiveOwnerAndName() + '/' |
150 | |
151 | old_mask = os.umask(0o077) |
152 | try: |
153 | new_key_cmd = [ |
154 | 'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048', |
155 | - '-subj', common_name, '-keyout', self.key, '-out', self.cert, |
156 | - '-days', '3650', '-nodes', '-sha256', |
157 | + '-subj', common_name, '-keyout', self.uefi_key, |
158 | + '-out', self.uefi_cert, '-days', '3650', '-nodes', '-sha256', |
159 | ] |
160 | - if subprocess.call(new_key_cmd) != 0: |
161 | - # Just log this rather than failing, since custom upload errors |
162 | - # tend to make the publisher rather upset. |
163 | - if self.logger is not None: |
164 | - self.logger.warning( |
165 | - "Failed to generate UEFI signing keys for %s" % |
166 | - common_name) |
167 | + self.callLog("UEFI keygen", new_key_cmd) |
168 | finally: |
169 | os.umask(old_mask) |
170 | |
171 | - if os.path.exists(self.cert): |
172 | - os.chmod(self.cert, 0o644) |
173 | - |
174 | - def getUefiKeys(self): |
175 | - """Validate and return the uefi key and cert for encryption.""" |
176 | - |
177 | - if self.key and self.cert: |
178 | - # If neither of the key files exists then attempt to |
179 | - # generate them. |
180 | - if (self.autokey and not os.path.exists(self.key) |
181 | - and not os.path.exists(self.cert)): |
182 | - self.generateUefiKeys() |
183 | - |
184 | - # If we have keys, but cannot read them they are dead to us. |
185 | - if not os.access(self.key, os.R_OK): |
186 | - if self.logger is not None: |
187 | - self.logger.warning( |
188 | - "UEFI private key %s not readable" % self.key) |
189 | - self.key = None |
190 | - if not os.access(self.cert, os.R_OK): |
191 | - if self.logger is not None: |
192 | - self.logger.warning( |
193 | - "UEFI certificate %s not readable" % self.cert) |
194 | - self.cert = None |
195 | - |
196 | - return (self.key, self.cert) |
197 | + if os.path.exists(self.uefi_cert): |
198 | + os.chmod(self.uefi_cert, 0o644) |
199 | |
200 | def signUefi(self, image): |
201 | """Attempt to sign an image.""" |
202 | - (key, cert) = self.getUefiKeys() |
203 | + remove_if_exists("%s.signed" % image) |
204 | + (key, cert) = self.getKeys('UEFI', self.generateUefiKeys, |
205 | + self.uefi_key, self.uefi_cert) |
206 | if not key or not cert: |
207 | return |
208 | cmdl = ["sbsign", "--key", key, "--cert", cert, image] |
209 | - if subprocess.call(cmdl) != 0: |
210 | - # Just log this rather than failing, since custom upload errors |
211 | - # tend to make the publisher rather upset. |
212 | - if self.logger is not None: |
213 | - self.logger.warning("UEFI Signing Failed '%s'" % |
214 | - " ".join(cmdl)) |
215 | + return self.callLog("UEFI signing", cmdl) |
216 | + |
217 | + def generateKmodKeys(self): |
218 | + """Generate new Kernel Signing Keys for this archive.""" |
219 | + directory = os.path.dirname(self.kmod_pem) |
220 | + if not os.path.exists(directory): |
221 | + os.makedirs(directory) |
222 | + |
223 | + old_mask = os.umask(0o077) |
224 | + try: |
225 | + with tempfile.NamedTemporaryFile(suffix='.keygen') as tf: |
226 | + common_name = self.getArchiveOwnerAndName() |
227 | + |
228 | + genkey_text = textwrap.dedent("""\ |
229 | + [ req ] |
230 | + default_bits = 4096 |
231 | + distinguished_name = req_distinguished_name |
232 | + prompt = no |
233 | + string_mask = utf8only |
234 | + x509_extensions = myexts |
235 | + |
236 | + [ req_distinguished_name ] |
237 | + CN = /CN=PPA """ + common_name + """ kmod/ |
238 | + |
239 | + [ myexts ] |
240 | + basicConstraints=critical,CA:FALSE |
241 | + keyUsage=digitalSignature |
242 | + subjectKeyIdentifier=hash |
243 | + authorityKeyIdentifier=keyid |
244 | + """) |
245 | + |
246 | + print(genkey_text, file=tf) |
247 | + |
248 | + # Close out the underlying file so we know it is complete. |
249 | + tf.file.close() |
250 | + |
251 | + new_key_cmd = [ |
252 | + 'openssl', 'req', '-new', '-nodes', '-utf8', '-sha512', |
253 | + '-days', '3650', '-batch', '-x509', '-config', tf.name, |
254 | + '-outform', 'PEM', '-out', self.kmod_pem, |
255 | + '-keyout', self.kmod_pem |
256 | + ] |
257 | + if self.callLog("Kmod keygen key", new_key_cmd) == 0: |
258 | + new_x509_cmd = [ |
259 | + 'openssl', 'x509', '-in', self.kmod_pem, |
260 | + '-outform', 'DER', '-out', self.kmod_x509 |
261 | + ] |
262 | + if self.callLog("Kmod keygen cert", new_x509_cmd) != 0: |
263 | + os.unlink(self.kmod_pem) |
264 | + finally: |
265 | + os.umask(old_mask) |
266 | + |
267 | + def signKmod(self, image): |
268 | + """Attempt to sign a kernel module.""" |
269 | + remove_if_exists("%s.sig" % image) |
270 | + (pem, cert) = self.getKeys('Kernel Module', self.generateKmodKeys, |
271 | + self.kmod_pem, self.kmod_x509) |
272 | + if not pem or not cert: |
273 | + return |
274 | + cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"] |
275 | + return self.callLog("Kmod signing", cmdl) |
276 | + |
277 | + def convertToTarball(self): |
278 | + """Convert unpacked output to signing tarball.""" |
279 | + tarfilename = os.path.join(self.tmpdir, "signed.tar.gz") |
280 | + versiondir = os.path.join(self.tmpdir, self.version) |
281 | + |
282 | + try: |
283 | + with tarfile.open(tarfilename, "w:gz") as tarball: |
284 | + tarball.add(versiondir, arcname=self.version) |
285 | + except tarfile.TarError as exc: |
286 | + raise SigningUploadPackError(tarfilename, exc) |
287 | + |
288 | + # Clean out the original tree and move the signing tarball in. |
289 | + try: |
290 | + shutil.rmtree(versiondir) |
291 | + os.mkdir(versiondir) |
292 | + os.rename(tarfilename, os.path.join(versiondir, "signed.tar.gz")) |
293 | + except OSError as exc: |
294 | + raise SigningUploadPackError(tarfilename, exc) |
295 | |
296 | def extract(self): |
297 | """Copy the custom upload to a temporary directory, and sign it. |
298 | @@ -189,10 +305,16 @@ |
299 | No actual extraction is required. |
300 | """ |
301 | super(SigningUpload, self).extract() |
302 | - efi_filenames = list(self.findEfiFilenames()) |
303 | - for efi_filename in efi_filenames: |
304 | - remove_if_exists("%s.signed" % efi_filename) |
305 | - self.signUefi(efi_filename) |
306 | + self.setSigningOptions() |
307 | + filehandlers = list(self.findSigningHandlers()) |
308 | + for (filename, handler) in filehandlers: |
309 | + if (handler(filename) == 0 and |
310 | + 'signed-only' in self.signing_options): |
311 | + os.unlink(filename) |
312 | + |
313 | + # If tarball output is requested, tar up the results. |
314 | + if 'tarball' in self.signing_options: |
315 | + self.convertToTarball() |
316 | |
317 | def shouldInstall(self, filename): |
318 | return filename.startswith("%s/" % self.version) |
319 | |
320 | === modified file 'lib/lp/archivepublisher/tests/test_signing.py' |
321 | --- lib/lp/archivepublisher/tests/test_signing.py 2016-05-11 12:59:17 +0000 |
322 | +++ lib/lp/archivepublisher/tests/test_signing.py 2016-05-19 16:56:11 +0000 |
323 | @@ -6,6 +6,7 @@ |
324 | __metaclass__ = type |
325 | |
326 | import os |
327 | +import tarfile |
328 | |
329 | from fixtures import MonkeyPatch |
330 | |
331 | @@ -20,20 +21,56 @@ |
332 | from lp.testing.fakemethod import FakeMethod |
333 | |
334 | |
335 | -class FakeMethodGenUefiKeys(FakeMethod): |
336 | - """Fake execution of generation of Uefi keys pairs.""" |
337 | +class FakeMethodCallLog(FakeMethod): |
338 | + """Fake execution general commands.""" |
339 | def __init__(self, upload=None, *args, **kwargs): |
340 | - super(FakeMethodGenUefiKeys, self).__init__(*args, **kwargs) |
341 | + super(FakeMethodCallLog, self).__init__(*args, **kwargs) |
342 | self.upload = upload |
343 | + self.callers = { |
344 | + "UEFI signing": 0, |
345 | + "Kmod signing": 0, |
346 | + "UEFI keygen": 0, |
347 | + "Kmod keygen key": 0, |
348 | + "Kmod keygen cert": 0, |
349 | + } |
350 | |
351 | def __call__(self, *args, **kwargs): |
352 | - super(FakeMethodGenUefiKeys, self).__call__(*args, **kwargs) |
353 | - |
354 | - write_file(self.upload.key, "") |
355 | - write_file(self.upload.cert, "") |
356 | - |
357 | - |
358 | -class FakeConfig: |
359 | + super(FakeMethodCallLog, self).__call__(*args, **kwargs) |
360 | + |
361 | + description = args[0] |
362 | + cmdl = args[1] |
363 | + self.callers[description] += 1 |
364 | + if description == "UEFI signing": |
365 | + filename = cmdl[-1] |
366 | + if filename.endswith(".efi"): |
367 | + write_file(filename + ".signed", "") |
368 | + |
369 | + elif description == "Kmod signing": |
370 | + filename = cmdl[-1] |
371 | + if filename.endswith(".ko.sig"): |
372 | + write_file(filename, "") |
373 | + |
374 | + elif description == "Kmod keygen cert": |
375 | + write_file(self.upload.kmod_x509, "") |
376 | + |
377 | + elif description == "Kmod keygen key": |
378 | + write_file(self.upload.kmod_pem, "") |
379 | + |
380 | + elif description == "UEFI keygen": |
381 | + write_file(self.upload.uefi_key, "") |
382 | + write_file(self.upload.uefi_cert, "") |
383 | + |
384 | + else: |
385 | + raise AssertionError("unknown command executed cmd=(%s)" % |
386 | + " ".join(cmdl)) |
387 | + |
388 | + return 0 |
389 | + |
390 | + def caller_count(self, caller): |
391 | + return self.callers.get(caller, 0) |
392 | + |
393 | + |
394 | +class FakeConfigPrimary: |
395 | """A fake publisher configuration for the main archive.""" |
396 | def __init__(self, distroroot, signingroot): |
397 | self.distroroot = distroroot |
398 | @@ -42,6 +79,15 @@ |
399 | self.signingautokey = False |
400 | |
401 | |
402 | +class FakeConfigCopy: |
403 | + """A fake publisher configuration for a copy archive.""" |
404 | + def __init__(self, distroroot): |
405 | + self.distroroot = distroroot |
406 | + self.signingroot = None |
407 | + self.archiveroot = os.path.join(self.distroroot, 'ubuntu') |
408 | + self.signingautokey = False |
409 | + |
410 | + |
411 | class FakeConfigPPA: |
412 | """A fake publisher configuration for a PPA.""" |
413 | def __init__(self, distroroot, signingroot, owner, ppa): |
414 | @@ -57,7 +103,7 @@ |
415 | super(TestSigning, self).setUp() |
416 | self.temp_dir = self.makeTemporaryDirectory() |
417 | self.signing_dir = self.makeTemporaryDirectory() |
418 | - self.pubconf = FakeConfig(self.temp_dir, self.signing_dir) |
419 | + self.pubconf = FakeConfigPrimary(self.temp_dir, self.signing_dir) |
420 | self.suite = "distroseries" |
421 | # CustomUpload.installFiles requires a umask of 0o022. |
422 | old_umask = os.umask(0o022) |
423 | @@ -68,28 +114,48 @@ |
424 | 'ubuntu-archive', 'testing') |
425 | self.testcase_cn = '/CN=PPA ubuntu-archive testing/' |
426 | |
427 | - def setUpKeyAndCert(self, create=True): |
428 | + def setUpUefiKeys(self, create=True): |
429 | self.key = os.path.join(self.signing_dir, "uefi.key") |
430 | self.cert = os.path.join(self.signing_dir, "uefi.crt") |
431 | if create: |
432 | write_file(self.key, "") |
433 | write_file(self.cert, "") |
434 | |
435 | + def setUpKmodKeys(self, create=True): |
436 | + self.kmod_pem = os.path.join(self.signing_dir, "kmod.pem") |
437 | + self.kmod_x509 = os.path.join(self.signing_dir, "kmod.x509") |
438 | + if create: |
439 | + write_file(self.kmod_pem, "") |
440 | + write_file(self.kmod_x509, "") |
441 | + |
442 | def openArchive(self, loader_type, version, arch): |
443 | self.path = os.path.join( |
444 | self.temp_dir, "%s_%s_%s.tar.gz" % (loader_type, version, arch)) |
445 | self.buffer = open(self.path, "wb") |
446 | self.archive = LaunchpadWriteTarFile(self.buffer) |
447 | |
448 | + def process_emulate(self): |
449 | + self.archive.close() |
450 | + self.buffer.close() |
451 | + upload = SigningUpload() |
452 | + # Under no circumstances is it safe to execute actual commands. |
453 | + self.fake_call = FakeMethod(result=0) |
454 | + upload.callLog = FakeMethodCallLog(upload=upload) |
455 | + self.useFixture(MonkeyPatch("subprocess.call", self.fake_call)) |
456 | + upload.process(self.pubconf, self.path, self.suite) |
457 | + |
458 | + return upload |
459 | + |
460 | def process(self): |
461 | self.archive.close() |
462 | self.buffer.close() |
463 | - fake_call = FakeMethod() |
464 | upload = SigningUpload() |
465 | upload.signUefi = FakeMethod() |
466 | + upload.signKmod = FakeMethod() |
467 | + # Under no circumstances is it safe to execute actual commands. |
468 | + fake_call = FakeMethod(result=0) |
469 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
470 | upload.process(self.pubconf, self.path, self.suite) |
471 | - # Under no circumstances is it safe to execute actual commands. |
472 | self.assertEqual(0, fake_call.call_count) |
473 | |
474 | return upload |
475 | @@ -106,37 +172,165 @@ |
476 | return os.path.join(self.getDistsPath(), "uefi", |
477 | "%s-%s" % (loader_type, arch)) |
478 | |
479 | - def test_unconfigured(self): |
480 | + def test_archive_copy(self): |
481 | # If there is no key/cert configuration, processing succeeds but |
482 | - # nothing is signed. Signing is attempted. |
483 | - self.pubconf = FakeConfig(self.temp_dir, None) |
484 | - self.openArchive("test", "1.0", "amd64") |
485 | - self.archive.add_file("1.0/empty.efi", "") |
486 | - upload = self.process() |
487 | - self.assertEqual(1, upload.signUefi.call_count) |
488 | - |
489 | - def test_missing_key_and_cert(self): |
490 | - # If the configured key/cert are missing, processing succeeds but |
491 | - # nothing is signed. Signing is attempted. |
492 | - self.openArchive("test", "1.0", "amd64") |
493 | - self.archive.add_file("1.0/empty.efi", "") |
494 | - upload = self.process() |
495 | - self.assertEqual(1, upload.signUefi.call_count) |
496 | - |
497 | - def test_no_efi_files(self): |
498 | + # nothing is signed. |
499 | + self.pubconf = FakeConfigCopy(self.temp_dir) |
500 | + self.openArchive("test", "1.0", "amd64") |
501 | + self.archive.add_file("1.0/empty.efi", "") |
502 | + self.archive.add_file("1.0/empty.ko", "") |
503 | + upload = self.process_emulate() |
504 | + self.assertEqual(0, upload.callLog.caller_count('UEFI keygen')) |
505 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key')) |
506 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert')) |
507 | + self.assertEqual(0, upload.callLog.caller_count('UEFI signing')) |
508 | + self.assertEqual(0, upload.callLog.caller_count('Kmod signing')) |
509 | + |
510 | + def test_archive_primary_no_keys(self): |
511 | + # If the configured key/cert are missing, processing succeeds but |
512 | + # nothing is signed. |
513 | + self.openArchive("test", "1.0", "amd64") |
514 | + self.archive.add_file("1.0/empty.efi", "") |
515 | + self.archive.add_file("1.0/empty.ko", "") |
516 | + upload = self.process_emulate() |
517 | + self.assertEqual(0, upload.callLog.caller_count('UEFI keygen')) |
518 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key')) |
519 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert')) |
520 | + self.assertEqual(0, upload.callLog.caller_count('UEFI signing')) |
521 | + self.assertEqual(0, upload.callLog.caller_count('Kmod signing')) |
522 | + |
523 | + def test_archive_primary_keys(self): |
524 | + # If the configured key/cert are missing, processing succeeds but |
525 | + # nothing is signed. |
526 | + self.setUpUefiKeys() |
527 | + self.setUpKmodKeys() |
528 | + self.openArchive("test", "1.0", "amd64") |
529 | + self.archive.add_file("1.0/empty.efi", "") |
530 | + self.archive.add_file("1.0/empty.ko", "") |
531 | + upload = self.process_emulate() |
532 | + self.assertEqual(0, upload.callLog.caller_count('UEFI keygen')) |
533 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key')) |
534 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert')) |
535 | + self.assertEqual(1, upload.callLog.caller_count('UEFI signing')) |
536 | + self.assertEqual(1, upload.callLog.caller_count('Kmod signing')) |
537 | + |
538 | + def test_PPA_creates_keys(self): |
539 | + # If the configured key/cert are missing, processing succeeds but |
540 | + # nothing is signed. |
541 | + self.setUpPPA() |
542 | + self.openArchive("test", "1.0", "amd64") |
543 | + self.archive.add_file("1.0/empty.efi", "") |
544 | + self.archive.add_file("1.0/empty.ko", "") |
545 | + upload = self.process_emulate() |
546 | + self.assertEqual(1, upload.callLog.caller_count('UEFI keygen')) |
547 | + self.assertEqual(1, upload.callLog.caller_count('Kmod keygen key')) |
548 | + self.assertEqual(1, upload.callLog.caller_count('Kmod keygen cert')) |
549 | + self.assertEqual(1, upload.callLog.caller_count('UEFI signing')) |
550 | + self.assertEqual(1, upload.callLog.caller_count('Kmod signing')) |
551 | + |
552 | + def test_options_handling_none(self): |
553 | + # If the configured key/cert are missing, processing succeeds but |
554 | + # nothing is signed. |
555 | + self.openArchive("test", "1.0", "amd64") |
556 | + self.archive.add_file("1.0/raw-signing.options", "") |
557 | + upload = self.process_emulate() |
558 | + self.assertContentEqual([], upload.signing_options.keys()) |
559 | + |
560 | + def test_options_handling_single(self): |
561 | + # If the configured key/cert are missing, processing succeeds but |
562 | + # nothing is signed. |
563 | + self.openArchive("test", "1.0", "amd64") |
564 | + self.archive.add_file("1.0/raw-signing.options", "first\n") |
565 | + upload = self.process_emulate() |
566 | + self.assertContentEqual(['first'], upload.signing_options.keys()) |
567 | + |
568 | + def test_options_handling_multiple(self): |
569 | + # If the configured key/cert are missing, processing succeeds but |
570 | + # nothing is signed. |
571 | + self.openArchive("test", "1.0", "amd64") |
572 | + self.archive.add_file("1.0/raw-signing.options", "first\nsecond\n") |
573 | + upload = self.process_emulate() |
574 | + self.assertContentEqual(['first', 'second'], |
575 | + upload.signing_options.keys()) |
576 | + |
577 | + def test_options_tarball(self): |
578 | + # Specifying the "tarball" option should create an tarball in |
579 | + # the tmpdir. |
580 | + self.setUpUefiKeys() |
581 | + self.setUpKmodKeys() |
582 | + self.openArchive("test", "1.0", "amd64") |
583 | + self.archive.add_file("1.0/raw-signing.options", "tarball") |
584 | + self.archive.add_file("1.0/empty.efi", "") |
585 | + self.archive.add_file("1.0/empty.ko", "") |
586 | + self.process_emulate() |
587 | + self.assertFalse(os.path.exists(os.path.join( |
588 | + self.getSignedPath("test", "amd64"), "1.0", "empty.efi"))) |
589 | + self.assertFalse(os.path.exists(os.path.join( |
590 | + self.getSignedPath("test", "amd64"), "1.0", "empty.ko"))) |
591 | + tarfilename = os.path.join(self.getSignedPath("test", "amd64"), |
592 | + "1.0", "signed.tar.gz") |
593 | + self.assertTrue(os.path.exists(tarfilename)) |
594 | + with tarfile.open(tarfilename) as tarball: |
595 | + self.assertContentEqual([ |
596 | + '1.0', '1.0/empty.efi', '1.0/empty.efi.signed', '1.0/empty.ko', |
597 | + '1.0/empty.ko.sig', '1.0/raw-signing.options', |
598 | + ], tarball.getnames()) |
599 | + |
600 | + def test_options_signed_only(self): |
601 | + # Specifying the "signed-only" option should trigger removal of |
602 | + # the source files leaving signatures only. |
603 | + self.setUpUefiKeys() |
604 | + self.setUpKmodKeys() |
605 | + self.openArchive("test", "1.0", "amd64") |
606 | + self.archive.add_file("1.0/raw-signing.options", "signed-only") |
607 | + self.archive.add_file("1.0/empty.efi", "") |
608 | + self.archive.add_file("1.0/empty.ko", "") |
609 | + self.process_emulate() |
610 | + self.assertFalse(os.path.exists(os.path.join( |
611 | + self.getSignedPath("test", "amd64"), "1.0", "empty.efi"))) |
612 | + self.assertTrue(os.path.exists(os.path.join( |
613 | + self.getSignedPath("test", "amd64"), "1.0", "empty.efi.signed"))) |
614 | + self.assertFalse(os.path.exists(os.path.join( |
615 | + self.getSignedPath("test", "amd64"), "1.0", "empty.ko"))) |
616 | + self.assertTrue(os.path.exists(os.path.join( |
617 | + self.getSignedPath("test", "amd64"), "1.0", "empty.ko.sig"))) |
618 | + |
619 | + def test_options_tarball_signed_only(self): |
620 | + # Specifying the "tarball" option should create an tarball in |
621 | + # the tmpdir. Adding signed-only should trigger removal of the |
622 | + # original files. |
623 | + self.setUpUefiKeys() |
624 | + self.setUpKmodKeys() |
625 | + self.openArchive("test", "1.0", "amd64") |
626 | + self.archive.add_file("1.0/raw-signing.options", |
627 | + "tarball\nsigned-only") |
628 | + self.archive.add_file("1.0/empty.efi", "") |
629 | + self.archive.add_file("1.0/empty.ko", "") |
630 | + self.process_emulate() |
631 | + tarfilename = os.path.join(self.getSignedPath("test", "amd64"), |
632 | + "1.0", "signed.tar.gz") |
633 | + self.assertTrue(os.path.exists(tarfilename)) |
634 | + with tarfile.open(tarfilename) as tarball: |
635 | + self.assertContentEqual([ |
636 | + '1.0', '1.0/empty.efi.signed', '1.0/empty.ko.sig', |
637 | + '1.0/raw-signing.options', |
638 | + ], tarball.getnames()) |
639 | + |
640 | + def test_no_signed_files(self): |
641 | # Tarballs containing no *.efi files are extracted without complaint. |
642 | # Nothing is signed. |
643 | - self.setUpKeyAndCert() |
644 | + self.setUpUefiKeys() |
645 | self.openArchive("empty", "1.0", "amd64") |
646 | self.archive.add_file("1.0/hello", "world") |
647 | upload = self.process() |
648 | self.assertTrue(os.path.exists(os.path.join( |
649 | self.getSignedPath("empty", "amd64"), "1.0", "hello"))) |
650 | self.assertEqual(0, upload.signUefi.call_count) |
651 | + self.assertEqual(0, upload.signKmod.call_count) |
652 | |
653 | def test_already_exists(self): |
654 | # If the target directory already exists, processing fails. |
655 | - self.setUpKeyAndCert() |
656 | + self.setUpUefiKeys() |
657 | self.openArchive("test", "1.0", "amd64") |
658 | self.archive.add_file("1.0/empty.efi", "") |
659 | os.makedirs(os.path.join(self.getSignedPath("test", "amd64"), "1.0")) |
660 | @@ -144,7 +338,7 @@ |
661 | |
662 | def test_bad_umask(self): |
663 | # The umask must be 0o022 to avoid incorrect permissions. |
664 | - self.setUpKeyAndCert() |
665 | + self.setUpUefiKeys() |
666 | self.openArchive("test", "1.0", "amd64") |
667 | self.archive.add_file("1.0/dir/file.efi", "foo") |
668 | os.umask(0o002) # cleanup already handled by setUp |
669 | @@ -153,8 +347,8 @@ |
670 | def test_correct_uefi_signing_command_executed(self): |
671 | # Check that calling signUefi() will generate the expected command |
672 | # when appropriate keys are present. |
673 | - self.setUpKeyAndCert() |
674 | - fake_call = FakeMethod() |
675 | + self.setUpUefiKeys() |
676 | + fake_call = FakeMethod(result=0) |
677 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
678 | upload = SigningUpload() |
679 | upload.generateUefiKeys = FakeMethod() |
680 | @@ -173,8 +367,8 @@ |
681 | def test_correct_uefi_signing_command_executed_no_keys(self): |
682 | # Check that calling signUefi() will generate no commands when |
683 | # no keys are present. |
684 | - self.setUpKeyAndCert(create=False) |
685 | - fake_call = FakeMethod() |
686 | + self.setUpUefiKeys(create=False) |
687 | + fake_call = FakeMethod(result=0) |
688 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
689 | upload = SigningUpload() |
690 | upload.generateUefiKeys = FakeMethod() |
691 | @@ -188,8 +382,8 @@ |
692 | # Check that calling generateUefiKeys() will generate the |
693 | # expected command. |
694 | self.setUpPPA() |
695 | - self.setUpKeyAndCert(create=False) |
696 | - fake_call = FakeMethod() |
697 | + self.setUpUefiKeys(create=False) |
698 | + fake_call = FakeMethod(result=0) |
699 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
700 | upload = SigningUpload() |
701 | upload.setTargetDirectory( |
702 | @@ -205,17 +399,102 @@ |
703 | ] |
704 | self.assertEqual(expected_cmd, args) |
705 | |
706 | - def test_signs_image(self): |
707 | - # Each image in the tarball is signed. |
708 | - self.setUpKeyAndCert() |
709 | - self.openArchive("test", "1.0", "amd64") |
710 | - self.archive.add_file("1.0/empty.efi", "") |
711 | - upload = self.process() |
712 | - self.assertEqual(1, upload.signUefi.call_count) |
713 | + def test_correct_kmod_signing_command_executed(self): |
714 | + # Check that calling signKmod() will generate the expected command |
715 | + # when appropriate keys are present. |
716 | + self.setUpKmodKeys() |
717 | + fake_call = FakeMethod(result=0) |
718 | + self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
719 | + upload = SigningUpload() |
720 | + upload.generateKmodKeys = FakeMethod() |
721 | + upload.setTargetDirectory( |
722 | + self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
723 | + upload.signKmod('t.ko') |
724 | + self.assertEqual(1, fake_call.call_count) |
725 | + # Assert command form. |
726 | + args = fake_call.calls[0][0][0] |
727 | + expected_cmd = [ |
728 | + 'kmodsign', '-D', 'sha512', self.kmod_pem, self.kmod_x509, |
729 | + 't.ko', 't.ko.sig' |
730 | + ] |
731 | + self.assertEqual(expected_cmd, args) |
732 | + self.assertEqual(0, upload.generateKmodKeys.call_count) |
733 | + |
734 | + def test_correct_kmod_signing_command_executed_no_keys(self): |
735 | + # Check that calling signKmod() will generate no commands when |
736 | + # no keys are present. |
737 | + self.setUpKmodKeys(create=False) |
738 | + fake_call = FakeMethod(result=0) |
739 | + self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
740 | + upload = SigningUpload() |
741 | + upload.generateKmodKeys = FakeMethod() |
742 | + upload.setTargetDirectory( |
743 | + self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
744 | + upload.signUefi('t.ko') |
745 | + self.assertEqual(0, fake_call.call_count) |
746 | + self.assertEqual(0, upload.generateKmodKeys.call_count) |
747 | + |
748 | + def test_correct_kmod_keygen_command_executed(self): |
749 | + # Check that calling generateUefiKeys() will generate the |
750 | + # expected command. |
751 | + self.setUpPPA() |
752 | + self.setUpKmodKeys(create=False) |
753 | + fake_call = FakeMethod(result=0) |
754 | + self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
755 | + upload = SigningUpload() |
756 | + upload.setTargetDirectory( |
757 | + self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
758 | + upload.generateKmodKeys() |
759 | + self.assertEqual(2, fake_call.call_count) |
760 | + # Assert the actual command matches. |
761 | + args = fake_call.calls[0][0][0] |
762 | + # Sanitise the keygen tmp file. |
763 | + if args[11].endswith('.keygen'): |
764 | + args[11] = 'XXX.keygen' |
765 | + expected_cmd = [ |
766 | + 'openssl', 'req', '-new', '-nodes', '-utf8', '-sha512', |
767 | + '-days', '3650', '-batch', '-x509', |
768 | + '-config', 'XXX.keygen', '-outform', 'PEM', |
769 | + '-out', self.kmod_pem, '-keyout', self.kmod_pem |
770 | + ] |
771 | + self.assertEqual(expected_cmd, args) |
772 | + args = fake_call.calls[1][0][0] |
773 | + expected_cmd = [ |
774 | + 'openssl', 'x509', '-in', self.kmod_pem, '-outform', 'DER', |
775 | + '-out', self.kmod_x509 |
776 | + ] |
777 | + self.assertEqual(expected_cmd, args) |
778 | + |
779 | + def test_signs_uefi_image(self): |
780 | + # Each image in the tarball is signed. |
781 | + self.setUpUefiKeys() |
782 | + self.openArchive("test", "1.0", "amd64") |
783 | + self.archive.add_file("1.0/empty.efi", "") |
784 | + upload = self.process() |
785 | + self.assertEqual(1, upload.signUefi.call_count) |
786 | + |
787 | + def test_signs_kmod_image(self): |
788 | + # Each image in the tarball is signed. |
789 | + self.setUpKmodKeys() |
790 | + self.openArchive("test", "1.0", "amd64") |
791 | + self.archive.add_file("1.0/empty.ko", "") |
792 | + upload = self.process() |
793 | + self.assertEqual(1, upload.signKmod.call_count) |
794 | + |
795 | + def test_signs_combo_image(self): |
796 | + # Each image in the tarball is signed. |
797 | + self.setUpKmodKeys() |
798 | + self.openArchive("test", "1.0", "amd64") |
799 | + self.archive.add_file("1.0/empty.efi", "") |
800 | + self.archive.add_file("1.0/empty.ko", "") |
801 | + self.archive.add_file("1.0/empty2.ko", "") |
802 | + upload = self.process() |
803 | + self.assertEqual(1, upload.signUefi.call_count) |
804 | + self.assertEqual(2, upload.signKmod.call_count) |
805 | |
806 | def test_installed(self): |
807 | # Files in the tarball are installed correctly. |
808 | - self.setUpKeyAndCert() |
809 | + self.setUpUefiKeys() |
810 | self.openArchive("test", "1.0", "amd64") |
811 | self.archive.add_file("1.0/empty.efi", "") |
812 | self.process() |
813 | @@ -231,7 +510,7 @@ |
814 | def test_installed_existing_uefi(self): |
815 | # Files in the tarball are installed correctly. |
816 | os.makedirs(os.path.join(self.getDistsPath(), "uefi")) |
817 | - self.setUpKeyAndCert() |
818 | + self.setUpUefiKeys() |
819 | self.openArchive("test", "1.0", "amd64") |
820 | self.archive.add_file("1.0/empty.efi", "") |
821 | self.process() |
822 | @@ -247,7 +526,7 @@ |
823 | def test_installed_existing_signing(self): |
824 | # Files in the tarball are installed correctly. |
825 | os.makedirs(os.path.join(self.getDistsPath(), "signing")) |
826 | - self.setUpKeyAndCert() |
827 | + self.setUpUefiKeys() |
828 | self.openArchive("test", "1.0", "amd64") |
829 | self.archive.add_file("1.0/empty.efi", "") |
830 | self.process() |
831 | @@ -262,33 +541,68 @@ |
832 | |
833 | def test_create_uefi_keys_autokey_off(self): |
834 | # Keys are not created. |
835 | - self.setUpKeyAndCert(create=False) |
836 | + self.setUpUefiKeys(create=False) |
837 | self.assertFalse(os.path.exists(self.key)) |
838 | self.assertFalse(os.path.exists(self.cert)) |
839 | - fake_call = FakeMethod() |
840 | + fake_call = FakeMethod(result=0) |
841 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
842 | upload = SigningUpload() |
843 | - upload.generateUefiKeys = FakeMethodGenUefiKeys(upload=upload) |
844 | + upload.callLog = FakeMethodCallLog(upload=upload) |
845 | upload.setTargetDirectory( |
846 | self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
847 | upload.signUefi('t.efi') |
848 | - self.assertEqual(0, upload.generateUefiKeys.call_count) |
849 | + self.assertEqual(0, upload.callLog.caller_count('UEFI keygen')) |
850 | self.assertFalse(os.path.exists(self.key)) |
851 | self.assertFalse(os.path.exists(self.cert)) |
852 | |
853 | def test_create_uefi_keys_autokey_on(self): |
854 | # Keys are created on demand. |
855 | self.setUpPPA() |
856 | - self.setUpKeyAndCert(create=False) |
857 | + self.setUpUefiKeys(create=False) |
858 | self.assertFalse(os.path.exists(self.key)) |
859 | self.assertFalse(os.path.exists(self.cert)) |
860 | - fake_call = FakeMethod() |
861 | + fake_call = FakeMethod(result=0) |
862 | self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
863 | upload = SigningUpload() |
864 | - upload.generateUefiKeys = FakeMethodGenUefiKeys(upload=upload) |
865 | + upload.callLog = FakeMethodCallLog(upload=upload) |
866 | upload.setTargetDirectory( |
867 | self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
868 | upload.signUefi('t.efi') |
869 | - self.assertEqual(1, upload.generateUefiKeys.call_count) |
870 | + self.assertEqual(1, upload.callLog.caller_count('UEFI keygen')) |
871 | self.assertTrue(os.path.exists(self.key)) |
872 | self.assertTrue(os.path.exists(self.cert)) |
873 | + |
874 | + def test_create_kmod_keys_autokey_off(self): |
875 | + # Keys are not created. |
876 | + self.setUpKmodKeys(create=False) |
877 | + self.assertFalse(os.path.exists(self.kmod_pem)) |
878 | + self.assertFalse(os.path.exists(self.kmod_x509)) |
879 | + fake_call = FakeMethod(result=0) |
880 | + self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
881 | + upload = SigningUpload() |
882 | + upload.callLog = FakeMethodCallLog(upload=upload) |
883 | + upload.setTargetDirectory( |
884 | + self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
885 | + upload.signKmod('t.ko') |
886 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key')) |
887 | + self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert')) |
888 | + self.assertFalse(os.path.exists(self.kmod_pem)) |
889 | + self.assertFalse(os.path.exists(self.kmod_x509)) |
890 | + |
891 | + def test_create_kmod_keys_autokey_on(self): |
892 | + # Keys are created on demand. |
893 | + self.setUpPPA() |
894 | + self.setUpKmodKeys(create=False) |
895 | + self.assertFalse(os.path.exists(self.kmod_pem)) |
896 | + self.assertFalse(os.path.exists(self.kmod_x509)) |
897 | + fake_call = FakeMethod(result=0) |
898 | + self.useFixture(MonkeyPatch("subprocess.call", fake_call)) |
899 | + upload = SigningUpload() |
900 | + upload.callLog = FakeMethodCallLog(upload=upload) |
901 | + upload.setTargetDirectory( |
902 | + self.pubconf, "test_1.0_amd64.tar.gz", "distroseries") |
903 | + upload.signKmod('t.ko') |
904 | + self.assertEqual(1, upload.callLog.caller_count('Kmod keygen key')) |
905 | + self.assertEqual(1, upload.callLog.caller_count('Kmod keygen cert')) |
906 | + self.assertTrue(os.path.exists(self.kmod_pem)) |
907 | + self.assertTrue(os.path.exists(self.kmod_x509)) |
Ok I have pushed up and update to this branch which I hope will fix these concerns. This has all the formatting bits applied. I have also revamped the testing of command execution to be much simpler. Finally it adds some additional tests which seemed to be absent for Kmod signing.
In reviewing the command handling for testing I realised that the signing.py code had the call and emit error form repeated all over the place so I have pulled that out to its own helper, by doing this I now have a much more tractible test point for the unit-tests to hang off of. This has made the testing much simpler. I have also moved all tests that need keys made to the new form allowing removal of the duplicated FakeMethods for key generation.