Merge lp:~apw/launchpad/signing-add-kernel-module-signing into lp:launchpad

Proposed by Andy Whitcroft
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
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.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
Revision history for this message
Andy Whitcroft (apw) wrote :

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.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))