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
=== modified file 'lib/lp/archivepublisher/signing.py'
--- lib/lp/archivepublisher/signing.py 2016-05-11 15:31:20 +0000
+++ lib/lp/archivepublisher/signing.py 2016-05-19 16:56:11 +0000
@@ -9,6 +9,8 @@
9secure to hold signing keys, so we sign them as a custom upload instead.9secure to hold signing keys, so we sign them as a custom upload instead.
10"""10"""
1111
12from __future__ import print_function
13
12__metaclass__ = type14__metaclass__ = type
1315
14__all__ = [16__all__ = [
@@ -17,12 +19,26 @@
17 ]19 ]
1820
19import os21import os
22import shutil
20import subprocess23import subprocess
24import tarfile
25import tempfile
26import textwrap
2127
22from lp.archivepublisher.customupload import CustomUpload28from lp.archivepublisher.customupload import (
29 CustomUpload,
30 CustomUploadError,
31 )
23from lp.services.osutils import remove_if_exists32from lp.services.osutils import remove_if_exists
2433
2534
35class SigningUploadPackError(CustomUploadError):
36 def __init__(self, tarfile_path, exc):
37 message = "Problem building tarball '%s': %s" % (
38 tarfile_path, exc)
39 CustomUploadError.__init__(self, message)
40
41
26class SigningUpload(CustomUpload):42class SigningUpload(CustomUpload):
27 """Signing custom upload.43 """Signing custom upload.
2844
@@ -67,12 +83,16 @@
67 if self.logger is not None:83 if self.logger is not None:
68 self.logger.warning(84 self.logger.warning(
69 "No signing root configured for this archive")85 "No signing root configured for this archive")
70 self.key = None86 self.uefi_key = None
71 self.cert = None87 self.uefi_cert = None
88 self.kmod_pem = None
89 self.kmod_x509 = None
72 self.autokey = False90 self.autokey = False
73 else:91 else:
74 self.key = os.path.join(pubconf.signingroot, "uefi.key")92 self.uefi_key = os.path.join(pubconf.signingroot, "uefi.key")
75 self.cert = os.path.join(pubconf.signingroot, "uefi.crt")93 self.uefi_cert = os.path.join(pubconf.signingroot, "uefi.crt")
94 self.kmod_pem = os.path.join(pubconf.signingroot, "kmod.pem")
95 self.kmod_x509 = os.path.join(pubconf.signingroot, "kmod.x509")
76 self.autokey = pubconf.signingautokey96 self.autokey = pubconf.signingautokey
7797
78 self.setComponents(tarfile_path)98 self.setComponents(tarfile_path)
@@ -99,6 +119,19 @@
99 dists_signed, "%s-%s" % (self.package, self.arch))119 dists_signed, "%s-%s" % (self.package, self.arch))
100 self.archiveroot = pubconf.archiveroot120 self.archiveroot = pubconf.archiveroot
101121
122 def setSigningOptions(self):
123 """Find and extract raw-signing.options from the tarball."""
124 self.signing_options = {}
125
126 options_file = os.path.join(self.tmpdir, self.version,
127 "raw-signing.options")
128 if not os.path.exists(options_file):
129 return
130
131 with open(options_file) as options_fd:
132 for option in options_fd:
133 self.signing_options[option.strip()] = True
134
102 @classmethod135 @classmethod
103 def getSeriesKey(cls, tarfile_path):136 def getSeriesKey(cls, tarfile_path):
104 try:137 try:
@@ -107,81 +140,164 @@
107 except ValueError:140 except ValueError:
108 return None141 return None
109142
110 def findEfiFilenames(self):143 def getArchiveOwnerAndName(self):
111 """Find all the *.efi files in an extracted tarball."""144 # XXX apw 2016-05-18: 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
149 return owner_name + ' ' + archive_name
150
151 def callLog(self, description, cmdl):
152 status = subprocess.call(cmdl)
153 if status != 0:
154 # Just log this rather than failing, since custom upload errors
155 # tend to make the publisher rather upset.
156 if self.logger is not None:
157 self.logger.warning("%s Failed (cmd='%s')" %
158 (description, " ".join(cmdl)))
159 return status
160
161 def findSigningHandlers(self):
162 """Find all the signable files in an extracted tarball."""
112 for dirpath, dirnames, filenames in os.walk(self.tmpdir):163 for dirpath, dirnames, filenames in os.walk(self.tmpdir):
113 for filename in filenames:164 for filename in filenames:
114 if filename.endswith(".efi"):165 if filename.endswith(".efi"):
115 yield os.path.join(dirpath, filename)166 yield (os.path.join(dirpath, filename), self.signUefi)
167 elif filename.endswith(".ko"):
168 yield (os.path.join(dirpath, filename), self.signKmod)
169
170 def getKeys(self, which, generate, *keynames):
171 """Validate and return the uefi key and cert for encryption."""
172
173 if self.autokey:
174 for keyfile in keynames:
175 if keyfile and not os.path.exists(keyfile):
176 generate()
177 break
178
179 valid = True
180 for keyfile in keynames:
181 if keyfile and not os.access(keyfile, os.R_OK):
182 if self.logger is not None:
183 self.logger.warning(
184 "%s key %s not readable" % (which, keyfile))
185 valid = False
186
187 if not valid:
188 return [None for k in keynames]
189 return keynames
116190
117 def generateUefiKeys(self):191 def generateUefiKeys(self):
118 """Generate new UEFI Keys for this archive."""192 """Generate new UEFI Keys for this archive."""
119 directory = os.path.dirname(self.key)193 directory = os.path.dirname(self.uefi_key)
120 if not os.path.exists(directory):194 if not os.path.exists(directory):
121 os.makedirs(directory)195 os.makedirs(directory)
122196
123 # XXX: pull out the PPA owner and name to seed key CN197 common_name = '/CN=PPA ' + self.getArchiveOwnerAndName() + '/'
124 archive_name = os.path.dirname(self.archiveroot)
125 owner_name = os.path.basename(os.path.dirname(archive_name))
126 archive_name = os.path.basename(archive_name)
127 common_name = '/CN=PPA ' + owner_name + ' ' + archive_name + '/'
128198
129 old_mask = os.umask(0o077)199 old_mask = os.umask(0o077)
130 try:200 try:
131 new_key_cmd = [201 new_key_cmd = [
132 'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048',202 'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048',
133 '-subj', common_name, '-keyout', self.key, '-out', self.cert,203 '-subj', common_name, '-keyout', self.uefi_key,
134 '-days', '3650', '-nodes', '-sha256',204 '-out', self.uefi_cert, '-days', '3650', '-nodes', '-sha256',
135 ]205 ]
136 if subprocess.call(new_key_cmd) != 0:206 self.callLog("UEFI keygen", new_key_cmd)
137 # Just log this rather than failing, since custom upload errors
138 # tend to make the publisher rather upset.
139 if self.logger is not None:
140 self.logger.warning(
141 "Failed to generate UEFI signing keys for %s" %
142 common_name)
143 finally:207 finally:
144 os.umask(old_mask)208 os.umask(old_mask)
145209
146 if os.path.exists(self.cert):210 if os.path.exists(self.uefi_cert):
147 os.chmod(self.cert, 0o644)211 os.chmod(self.uefi_cert, 0o644)
148
149 def getUefiKeys(self):
150 """Validate and return the uefi key and cert for encryption."""
151
152 if self.key and self.cert:
153 # If neither of the key files exists then attempt to
154 # generate them.
155 if (self.autokey and not os.path.exists(self.key)
156 and not os.path.exists(self.cert)):
157 self.generateUefiKeys()
158
159 # If we have keys, but cannot read them they are dead to us.
160 if not os.access(self.key, os.R_OK):
161 if self.logger is not None:
162 self.logger.warning(
163 "UEFI private key %s not readable" % self.key)
164 self.key = None
165 if not os.access(self.cert, os.R_OK):
166 if self.logger is not None:
167 self.logger.warning(
168 "UEFI certificate %s not readable" % self.cert)
169 self.cert = None
170
171 return (self.key, self.cert)
172212
173 def signUefi(self, image):213 def signUefi(self, image):
174 """Attempt to sign an image."""214 """Attempt to sign an image."""
175 (key, cert) = self.getUefiKeys()215 remove_if_exists("%s.signed" % image)
216 (key, cert) = self.getKeys('UEFI', self.generateUefiKeys,
217 self.uefi_key, self.uefi_cert)
176 if not key or not cert:218 if not key or not cert:
177 return219 return
178 cmdl = ["sbsign", "--key", key, "--cert", cert, image]220 cmdl = ["sbsign", "--key", key, "--cert", cert, image]
179 if subprocess.call(cmdl) != 0:221 return self.callLog("UEFI signing", cmdl)
180 # Just log this rather than failing, since custom upload errors222
181 # tend to make the publisher rather upset.223 def generateKmodKeys(self):
182 if self.logger is not None:224 """Generate new Kernel Signing Keys for this archive."""
183 self.logger.warning("UEFI Signing Failed '%s'" %225 directory = os.path.dirname(self.kmod_pem)
184 " ".join(cmdl))226 if not os.path.exists(directory):
227 os.makedirs(directory)
228
229 old_mask = os.umask(0o077)
230 try:
231 with tempfile.NamedTemporaryFile(suffix='.keygen') as tf:
232 common_name = self.getArchiveOwnerAndName()
233
234 genkey_text = textwrap.dedent("""\
235 [ req ]
236 default_bits = 4096
237 distinguished_name = req_distinguished_name
238 prompt = no
239 string_mask = utf8only
240 x509_extensions = myexts
241
242 [ req_distinguished_name ]
243 CN = /CN=PPA """ + common_name + """ kmod/
244
245 [ myexts ]
246 basicConstraints=critical,CA:FALSE
247 keyUsage=digitalSignature
248 subjectKeyIdentifier=hash
249 authorityKeyIdentifier=keyid
250 """)
251
252 print(genkey_text, file=tf)
253
254 # Close out the underlying file so we know it is complete.
255 tf.file.close()
256
257 new_key_cmd = [
258 'openssl', 'req', '-new', '-nodes', '-utf8', '-sha512',
259 '-days', '3650', '-batch', '-x509', '-config', tf.name,
260 '-outform', 'PEM', '-out', self.kmod_pem,
261 '-keyout', self.kmod_pem
262 ]
263 if self.callLog("Kmod keygen key", new_key_cmd) == 0:
264 new_x509_cmd = [
265 'openssl', 'x509', '-in', self.kmod_pem,
266 '-outform', 'DER', '-out', self.kmod_x509
267 ]
268 if self.callLog("Kmod keygen cert", new_x509_cmd) != 0:
269 os.unlink(self.kmod_pem)
270 finally:
271 os.umask(old_mask)
272
273 def signKmod(self, image):
274 """Attempt to sign a kernel module."""
275 remove_if_exists("%s.sig" % image)
276 (pem, cert) = self.getKeys('Kernel Module', self.generateKmodKeys,
277 self.kmod_pem, self.kmod_x509)
278 if not pem or not cert:
279 return
280 cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"]
281 return self.callLog("Kmod signing", cmdl)
282
283 def convertToTarball(self):
284 """Convert unpacked output to signing tarball."""
285 tarfilename = os.path.join(self.tmpdir, "signed.tar.gz")
286 versiondir = os.path.join(self.tmpdir, self.version)
287
288 try:
289 with tarfile.open(tarfilename, "w:gz") as tarball:
290 tarball.add(versiondir, arcname=self.version)
291 except tarfile.TarError as exc:
292 raise SigningUploadPackError(tarfilename, exc)
293
294 # Clean out the original tree and move the signing tarball in.
295 try:
296 shutil.rmtree(versiondir)
297 os.mkdir(versiondir)
298 os.rename(tarfilename, os.path.join(versiondir, "signed.tar.gz"))
299 except OSError as exc:
300 raise SigningUploadPackError(tarfilename, exc)
185301
186 def extract(self):302 def extract(self):
187 """Copy the custom upload to a temporary directory, and sign it.303 """Copy the custom upload to a temporary directory, and sign it.
@@ -189,10 +305,16 @@
189 No actual extraction is required.305 No actual extraction is required.
190 """306 """
191 super(SigningUpload, self).extract()307 super(SigningUpload, self).extract()
192 efi_filenames = list(self.findEfiFilenames())308 self.setSigningOptions()
193 for efi_filename in efi_filenames:309 filehandlers = list(self.findSigningHandlers())
194 remove_if_exists("%s.signed" % efi_filename)310 for (filename, handler) in filehandlers:
195 self.signUefi(efi_filename)311 if (handler(filename) == 0 and
312 'signed-only' in self.signing_options):
313 os.unlink(filename)
314
315 # If tarball output is requested, tar up the results.
316 if 'tarball' in self.signing_options:
317 self.convertToTarball()
196318
197 def shouldInstall(self, filename):319 def shouldInstall(self, filename):
198 return filename.startswith("%s/" % self.version)320 return filename.startswith("%s/" % self.version)
199321
=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
--- lib/lp/archivepublisher/tests/test_signing.py 2016-05-11 12:59:17 +0000
+++ lib/lp/archivepublisher/tests/test_signing.py 2016-05-19 16:56:11 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8import os8import os
9import tarfile
910
10from fixtures import MonkeyPatch11from fixtures import MonkeyPatch
1112
@@ -20,20 +21,56 @@
20from lp.testing.fakemethod import FakeMethod21from lp.testing.fakemethod import FakeMethod
2122
2223
23class FakeMethodGenUefiKeys(FakeMethod):24class FakeMethodCallLog(FakeMethod):
24 """Fake execution of generation of Uefi keys pairs."""25 """Fake execution general commands."""
25 def __init__(self, upload=None, *args, **kwargs):26 def __init__(self, upload=None, *args, **kwargs):
26 super(FakeMethodGenUefiKeys, self).__init__(*args, **kwargs)27 super(FakeMethodCallLog, self).__init__(*args, **kwargs)
27 self.upload = upload28 self.upload = upload
29 self.callers = {
30 "UEFI signing": 0,
31 "Kmod signing": 0,
32 "UEFI keygen": 0,
33 "Kmod keygen key": 0,
34 "Kmod keygen cert": 0,
35 }
2836
29 def __call__(self, *args, **kwargs):37 def __call__(self, *args, **kwargs):
30 super(FakeMethodGenUefiKeys, self).__call__(*args, **kwargs)38 super(FakeMethodCallLog, self).__call__(*args, **kwargs)
3139
32 write_file(self.upload.key, "")40 description = args[0]
33 write_file(self.upload.cert, "")41 cmdl = args[1]
3442 self.callers[description] += 1
3543 if description == "UEFI signing":
36class FakeConfig:44 filename = cmdl[-1]
45 if filename.endswith(".efi"):
46 write_file(filename + ".signed", "")
47
48 elif description == "Kmod signing":
49 filename = cmdl[-1]
50 if filename.endswith(".ko.sig"):
51 write_file(filename, "")
52
53 elif description == "Kmod keygen cert":
54 write_file(self.upload.kmod_x509, "")
55
56 elif description == "Kmod keygen key":
57 write_file(self.upload.kmod_pem, "")
58
59 elif description == "UEFI keygen":
60 write_file(self.upload.uefi_key, "")
61 write_file(self.upload.uefi_cert, "")
62
63 else:
64 raise AssertionError("unknown command executed cmd=(%s)" %
65 " ".join(cmdl))
66
67 return 0
68
69 def caller_count(self, caller):
70 return self.callers.get(caller, 0)
71
72
73class FakeConfigPrimary:
37 """A fake publisher configuration for the main archive."""74 """A fake publisher configuration for the main archive."""
38 def __init__(self, distroroot, signingroot):75 def __init__(self, distroroot, signingroot):
39 self.distroroot = distroroot76 self.distroroot = distroroot
@@ -42,6 +79,15 @@
42 self.signingautokey = False79 self.signingautokey = False
4380
4481
82class FakeConfigCopy:
83 """A fake publisher configuration for a copy archive."""
84 def __init__(self, distroroot):
85 self.distroroot = distroroot
86 self.signingroot = None
87 self.archiveroot = os.path.join(self.distroroot, 'ubuntu')
88 self.signingautokey = False
89
90
45class FakeConfigPPA:91class FakeConfigPPA:
46 """A fake publisher configuration for a PPA."""92 """A fake publisher configuration for a PPA."""
47 def __init__(self, distroroot, signingroot, owner, ppa):93 def __init__(self, distroroot, signingroot, owner, ppa):
@@ -57,7 +103,7 @@
57 super(TestSigning, self).setUp()103 super(TestSigning, self).setUp()
58 self.temp_dir = self.makeTemporaryDirectory()104 self.temp_dir = self.makeTemporaryDirectory()
59 self.signing_dir = self.makeTemporaryDirectory()105 self.signing_dir = self.makeTemporaryDirectory()
60 self.pubconf = FakeConfig(self.temp_dir, self.signing_dir)106 self.pubconf = FakeConfigPrimary(self.temp_dir, self.signing_dir)
61 self.suite = "distroseries"107 self.suite = "distroseries"
62 # CustomUpload.installFiles requires a umask of 0o022.108 # CustomUpload.installFiles requires a umask of 0o022.
63 old_umask = os.umask(0o022)109 old_umask = os.umask(0o022)
@@ -68,28 +114,48 @@
68 'ubuntu-archive', 'testing')114 'ubuntu-archive', 'testing')
69 self.testcase_cn = '/CN=PPA ubuntu-archive testing/'115 self.testcase_cn = '/CN=PPA ubuntu-archive testing/'
70116
71 def setUpKeyAndCert(self, create=True):117 def setUpUefiKeys(self, create=True):
72 self.key = os.path.join(self.signing_dir, "uefi.key")118 self.key = os.path.join(self.signing_dir, "uefi.key")
73 self.cert = os.path.join(self.signing_dir, "uefi.crt")119 self.cert = os.path.join(self.signing_dir, "uefi.crt")
74 if create:120 if create:
75 write_file(self.key, "")121 write_file(self.key, "")
76 write_file(self.cert, "")122 write_file(self.cert, "")
77123
124 def setUpKmodKeys(self, create=True):
125 self.kmod_pem = os.path.join(self.signing_dir, "kmod.pem")
126 self.kmod_x509 = os.path.join(self.signing_dir, "kmod.x509")
127 if create:
128 write_file(self.kmod_pem, "")
129 write_file(self.kmod_x509, "")
130
78 def openArchive(self, loader_type, version, arch):131 def openArchive(self, loader_type, version, arch):
79 self.path = os.path.join(132 self.path = os.path.join(
80 self.temp_dir, "%s_%s_%s.tar.gz" % (loader_type, version, arch))133 self.temp_dir, "%s_%s_%s.tar.gz" % (loader_type, version, arch))
81 self.buffer = open(self.path, "wb")134 self.buffer = open(self.path, "wb")
82 self.archive = LaunchpadWriteTarFile(self.buffer)135 self.archive = LaunchpadWriteTarFile(self.buffer)
83136
137 def process_emulate(self):
138 self.archive.close()
139 self.buffer.close()
140 upload = SigningUpload()
141 # Under no circumstances is it safe to execute actual commands.
142 self.fake_call = FakeMethod(result=0)
143 upload.callLog = FakeMethodCallLog(upload=upload)
144 self.useFixture(MonkeyPatch("subprocess.call", self.fake_call))
145 upload.process(self.pubconf, self.path, self.suite)
146
147 return upload
148
84 def process(self):149 def process(self):
85 self.archive.close()150 self.archive.close()
86 self.buffer.close()151 self.buffer.close()
87 fake_call = FakeMethod()
88 upload = SigningUpload()152 upload = SigningUpload()
89 upload.signUefi = FakeMethod()153 upload.signUefi = FakeMethod()
154 upload.signKmod = FakeMethod()
155 # Under no circumstances is it safe to execute actual commands.
156 fake_call = FakeMethod(result=0)
90 self.useFixture(MonkeyPatch("subprocess.call", fake_call))157 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
91 upload.process(self.pubconf, self.path, self.suite)158 upload.process(self.pubconf, self.path, self.suite)
92 # Under no circumstances is it safe to execute actual commands.
93 self.assertEqual(0, fake_call.call_count)159 self.assertEqual(0, fake_call.call_count)
94160
95 return upload161 return upload
@@ -106,37 +172,165 @@
106 return os.path.join(self.getDistsPath(), "uefi",172 return os.path.join(self.getDistsPath(), "uefi",
107 "%s-%s" % (loader_type, arch))173 "%s-%s" % (loader_type, arch))
108174
109 def test_unconfigured(self):175 def test_archive_copy(self):
110 # If there is no key/cert configuration, processing succeeds but176 # If there is no key/cert configuration, processing succeeds but
111 # nothing is signed. Signing is attempted.177 # nothing is signed.
112 self.pubconf = FakeConfig(self.temp_dir, None)178 self.pubconf = FakeConfigCopy(self.temp_dir)
113 self.openArchive("test", "1.0", "amd64")179 self.openArchive("test", "1.0", "amd64")
114 self.archive.add_file("1.0/empty.efi", "")180 self.archive.add_file("1.0/empty.efi", "")
115 upload = self.process()181 self.archive.add_file("1.0/empty.ko", "")
116 self.assertEqual(1, upload.signUefi.call_count)182 upload = self.process_emulate()
117183 self.assertEqual(0, upload.callLog.caller_count('UEFI keygen'))
118 def test_missing_key_and_cert(self):184 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key'))
119 # If the configured key/cert are missing, processing succeeds but185 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert'))
120 # nothing is signed. Signing is attempted.186 self.assertEqual(0, upload.callLog.caller_count('UEFI signing'))
121 self.openArchive("test", "1.0", "amd64")187 self.assertEqual(0, upload.callLog.caller_count('Kmod signing'))
122 self.archive.add_file("1.0/empty.efi", "")188
123 upload = self.process()189 def test_archive_primary_no_keys(self):
124 self.assertEqual(1, upload.signUefi.call_count)190 # If the configured key/cert are missing, processing succeeds but
125191 # nothing is signed.
126 def test_no_efi_files(self):192 self.openArchive("test", "1.0", "amd64")
193 self.archive.add_file("1.0/empty.efi", "")
194 self.archive.add_file("1.0/empty.ko", "")
195 upload = self.process_emulate()
196 self.assertEqual(0, upload.callLog.caller_count('UEFI keygen'))
197 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key'))
198 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert'))
199 self.assertEqual(0, upload.callLog.caller_count('UEFI signing'))
200 self.assertEqual(0, upload.callLog.caller_count('Kmod signing'))
201
202 def test_archive_primary_keys(self):
203 # If the configured key/cert are missing, processing succeeds but
204 # nothing is signed.
205 self.setUpUefiKeys()
206 self.setUpKmodKeys()
207 self.openArchive("test", "1.0", "amd64")
208 self.archive.add_file("1.0/empty.efi", "")
209 self.archive.add_file("1.0/empty.ko", "")
210 upload = self.process_emulate()
211 self.assertEqual(0, upload.callLog.caller_count('UEFI keygen'))
212 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key'))
213 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert'))
214 self.assertEqual(1, upload.callLog.caller_count('UEFI signing'))
215 self.assertEqual(1, upload.callLog.caller_count('Kmod signing'))
216
217 def test_PPA_creates_keys(self):
218 # If the configured key/cert are missing, processing succeeds but
219 # nothing is signed.
220 self.setUpPPA()
221 self.openArchive("test", "1.0", "amd64")
222 self.archive.add_file("1.0/empty.efi", "")
223 self.archive.add_file("1.0/empty.ko", "")
224 upload = self.process_emulate()
225 self.assertEqual(1, upload.callLog.caller_count('UEFI keygen'))
226 self.assertEqual(1, upload.callLog.caller_count('Kmod keygen key'))
227 self.assertEqual(1, upload.callLog.caller_count('Kmod keygen cert'))
228 self.assertEqual(1, upload.callLog.caller_count('UEFI signing'))
229 self.assertEqual(1, upload.callLog.caller_count('Kmod signing'))
230
231 def test_options_handling_none(self):
232 # If the configured key/cert are missing, processing succeeds but
233 # nothing is signed.
234 self.openArchive("test", "1.0", "amd64")
235 self.archive.add_file("1.0/raw-signing.options", "")
236 upload = self.process_emulate()
237 self.assertContentEqual([], upload.signing_options.keys())
238
239 def test_options_handling_single(self):
240 # If the configured key/cert are missing, processing succeeds but
241 # nothing is signed.
242 self.openArchive("test", "1.0", "amd64")
243 self.archive.add_file("1.0/raw-signing.options", "first\n")
244 upload = self.process_emulate()
245 self.assertContentEqual(['first'], upload.signing_options.keys())
246
247 def test_options_handling_multiple(self):
248 # If the configured key/cert are missing, processing succeeds but
249 # nothing is signed.
250 self.openArchive("test", "1.0", "amd64")
251 self.archive.add_file("1.0/raw-signing.options", "first\nsecond\n")
252 upload = self.process_emulate()
253 self.assertContentEqual(['first', 'second'],
254 upload.signing_options.keys())
255
256 def test_options_tarball(self):
257 # Specifying the "tarball" option should create an tarball in
258 # the tmpdir.
259 self.setUpUefiKeys()
260 self.setUpKmodKeys()
261 self.openArchive("test", "1.0", "amd64")
262 self.archive.add_file("1.0/raw-signing.options", "tarball")
263 self.archive.add_file("1.0/empty.efi", "")
264 self.archive.add_file("1.0/empty.ko", "")
265 self.process_emulate()
266 self.assertFalse(os.path.exists(os.path.join(
267 self.getSignedPath("test", "amd64"), "1.0", "empty.efi")))
268 self.assertFalse(os.path.exists(os.path.join(
269 self.getSignedPath("test", "amd64"), "1.0", "empty.ko")))
270 tarfilename = os.path.join(self.getSignedPath("test", "amd64"),
271 "1.0", "signed.tar.gz")
272 self.assertTrue(os.path.exists(tarfilename))
273 with tarfile.open(tarfilename) as tarball:
274 self.assertContentEqual([
275 '1.0', '1.0/empty.efi', '1.0/empty.efi.signed', '1.0/empty.ko',
276 '1.0/empty.ko.sig', '1.0/raw-signing.options',
277 ], tarball.getnames())
278
279 def test_options_signed_only(self):
280 # Specifying the "signed-only" option should trigger removal of
281 # the source files leaving signatures only.
282 self.setUpUefiKeys()
283 self.setUpKmodKeys()
284 self.openArchive("test", "1.0", "amd64")
285 self.archive.add_file("1.0/raw-signing.options", "signed-only")
286 self.archive.add_file("1.0/empty.efi", "")
287 self.archive.add_file("1.0/empty.ko", "")
288 self.process_emulate()
289 self.assertFalse(os.path.exists(os.path.join(
290 self.getSignedPath("test", "amd64"), "1.0", "empty.efi")))
291 self.assertTrue(os.path.exists(os.path.join(
292 self.getSignedPath("test", "amd64"), "1.0", "empty.efi.signed")))
293 self.assertFalse(os.path.exists(os.path.join(
294 self.getSignedPath("test", "amd64"), "1.0", "empty.ko")))
295 self.assertTrue(os.path.exists(os.path.join(
296 self.getSignedPath("test", "amd64"), "1.0", "empty.ko.sig")))
297
298 def test_options_tarball_signed_only(self):
299 # Specifying the "tarball" option should create an tarball in
300 # the tmpdir. Adding signed-only should trigger removal of the
301 # original files.
302 self.setUpUefiKeys()
303 self.setUpKmodKeys()
304 self.openArchive("test", "1.0", "amd64")
305 self.archive.add_file("1.0/raw-signing.options",
306 "tarball\nsigned-only")
307 self.archive.add_file("1.0/empty.efi", "")
308 self.archive.add_file("1.0/empty.ko", "")
309 self.process_emulate()
310 tarfilename = os.path.join(self.getSignedPath("test", "amd64"),
311 "1.0", "signed.tar.gz")
312 self.assertTrue(os.path.exists(tarfilename))
313 with tarfile.open(tarfilename) as tarball:
314 self.assertContentEqual([
315 '1.0', '1.0/empty.efi.signed', '1.0/empty.ko.sig',
316 '1.0/raw-signing.options',
317 ], tarball.getnames())
318
319 def test_no_signed_files(self):
127 # Tarballs containing no *.efi files are extracted without complaint.320 # Tarballs containing no *.efi files are extracted without complaint.
128 # Nothing is signed.321 # Nothing is signed.
129 self.setUpKeyAndCert()322 self.setUpUefiKeys()
130 self.openArchive("empty", "1.0", "amd64")323 self.openArchive("empty", "1.0", "amd64")
131 self.archive.add_file("1.0/hello", "world")324 self.archive.add_file("1.0/hello", "world")
132 upload = self.process()325 upload = self.process()
133 self.assertTrue(os.path.exists(os.path.join(326 self.assertTrue(os.path.exists(os.path.join(
134 self.getSignedPath("empty", "amd64"), "1.0", "hello")))327 self.getSignedPath("empty", "amd64"), "1.0", "hello")))
135 self.assertEqual(0, upload.signUefi.call_count)328 self.assertEqual(0, upload.signUefi.call_count)
329 self.assertEqual(0, upload.signKmod.call_count)
136330
137 def test_already_exists(self):331 def test_already_exists(self):
138 # If the target directory already exists, processing fails.332 # If the target directory already exists, processing fails.
139 self.setUpKeyAndCert()333 self.setUpUefiKeys()
140 self.openArchive("test", "1.0", "amd64")334 self.openArchive("test", "1.0", "amd64")
141 self.archive.add_file("1.0/empty.efi", "")335 self.archive.add_file("1.0/empty.efi", "")
142 os.makedirs(os.path.join(self.getSignedPath("test", "amd64"), "1.0"))336 os.makedirs(os.path.join(self.getSignedPath("test", "amd64"), "1.0"))
@@ -144,7 +338,7 @@
144338
145 def test_bad_umask(self):339 def test_bad_umask(self):
146 # The umask must be 0o022 to avoid incorrect permissions.340 # The umask must be 0o022 to avoid incorrect permissions.
147 self.setUpKeyAndCert()341 self.setUpUefiKeys()
148 self.openArchive("test", "1.0", "amd64")342 self.openArchive("test", "1.0", "amd64")
149 self.archive.add_file("1.0/dir/file.efi", "foo")343 self.archive.add_file("1.0/dir/file.efi", "foo")
150 os.umask(0o002) # cleanup already handled by setUp344 os.umask(0o002) # cleanup already handled by setUp
@@ -153,8 +347,8 @@
153 def test_correct_uefi_signing_command_executed(self):347 def test_correct_uefi_signing_command_executed(self):
154 # Check that calling signUefi() will generate the expected command348 # Check that calling signUefi() will generate the expected command
155 # when appropriate keys are present.349 # when appropriate keys are present.
156 self.setUpKeyAndCert()350 self.setUpUefiKeys()
157 fake_call = FakeMethod()351 fake_call = FakeMethod(result=0)
158 self.useFixture(MonkeyPatch("subprocess.call", fake_call))352 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
159 upload = SigningUpload()353 upload = SigningUpload()
160 upload.generateUefiKeys = FakeMethod()354 upload.generateUefiKeys = FakeMethod()
@@ -173,8 +367,8 @@
173 def test_correct_uefi_signing_command_executed_no_keys(self):367 def test_correct_uefi_signing_command_executed_no_keys(self):
174 # Check that calling signUefi() will generate no commands when368 # Check that calling signUefi() will generate no commands when
175 # no keys are present.369 # no keys are present.
176 self.setUpKeyAndCert(create=False)370 self.setUpUefiKeys(create=False)
177 fake_call = FakeMethod()371 fake_call = FakeMethod(result=0)
178 self.useFixture(MonkeyPatch("subprocess.call", fake_call))372 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
179 upload = SigningUpload()373 upload = SigningUpload()
180 upload.generateUefiKeys = FakeMethod()374 upload.generateUefiKeys = FakeMethod()
@@ -188,8 +382,8 @@
188 # Check that calling generateUefiKeys() will generate the382 # Check that calling generateUefiKeys() will generate the
189 # expected command.383 # expected command.
190 self.setUpPPA()384 self.setUpPPA()
191 self.setUpKeyAndCert(create=False)385 self.setUpUefiKeys(create=False)
192 fake_call = FakeMethod()386 fake_call = FakeMethod(result=0)
193 self.useFixture(MonkeyPatch("subprocess.call", fake_call))387 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
194 upload = SigningUpload()388 upload = SigningUpload()
195 upload.setTargetDirectory(389 upload.setTargetDirectory(
@@ -205,17 +399,102 @@
205 ]399 ]
206 self.assertEqual(expected_cmd, args)400 self.assertEqual(expected_cmd, args)
207401
208 def test_signs_image(self):402 def test_correct_kmod_signing_command_executed(self):
209 # Each image in the tarball is signed.403 # Check that calling signKmod() will generate the expected command
210 self.setUpKeyAndCert()404 # when appropriate keys are present.
211 self.openArchive("test", "1.0", "amd64")405 self.setUpKmodKeys()
212 self.archive.add_file("1.0/empty.efi", "")406 fake_call = FakeMethod(result=0)
213 upload = self.process()407 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
214 self.assertEqual(1, upload.signUefi.call_count)408 upload = SigningUpload()
409 upload.generateKmodKeys = FakeMethod()
410 upload.setTargetDirectory(
411 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
412 upload.signKmod('t.ko')
413 self.assertEqual(1, fake_call.call_count)
414 # Assert command form.
415 args = fake_call.calls[0][0][0]
416 expected_cmd = [
417 'kmodsign', '-D', 'sha512', self.kmod_pem, self.kmod_x509,
418 't.ko', 't.ko.sig'
419 ]
420 self.assertEqual(expected_cmd, args)
421 self.assertEqual(0, upload.generateKmodKeys.call_count)
422
423 def test_correct_kmod_signing_command_executed_no_keys(self):
424 # Check that calling signKmod() will generate no commands when
425 # no keys are present.
426 self.setUpKmodKeys(create=False)
427 fake_call = FakeMethod(result=0)
428 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
429 upload = SigningUpload()
430 upload.generateKmodKeys = FakeMethod()
431 upload.setTargetDirectory(
432 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
433 upload.signUefi('t.ko')
434 self.assertEqual(0, fake_call.call_count)
435 self.assertEqual(0, upload.generateKmodKeys.call_count)
436
437 def test_correct_kmod_keygen_command_executed(self):
438 # Check that calling generateUefiKeys() will generate the
439 # expected command.
440 self.setUpPPA()
441 self.setUpKmodKeys(create=False)
442 fake_call = FakeMethod(result=0)
443 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
444 upload = SigningUpload()
445 upload.setTargetDirectory(
446 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
447 upload.generateKmodKeys()
448 self.assertEqual(2, fake_call.call_count)
449 # Assert the actual command matches.
450 args = fake_call.calls[0][0][0]
451 # Sanitise the keygen tmp file.
452 if args[11].endswith('.keygen'):
453 args[11] = 'XXX.keygen'
454 expected_cmd = [
455 'openssl', 'req', '-new', '-nodes', '-utf8', '-sha512',
456 '-days', '3650', '-batch', '-x509',
457 '-config', 'XXX.keygen', '-outform', 'PEM',
458 '-out', self.kmod_pem, '-keyout', self.kmod_pem
459 ]
460 self.assertEqual(expected_cmd, args)
461 args = fake_call.calls[1][0][0]
462 expected_cmd = [
463 'openssl', 'x509', '-in', self.kmod_pem, '-outform', 'DER',
464 '-out', self.kmod_x509
465 ]
466 self.assertEqual(expected_cmd, args)
467
468 def test_signs_uefi_image(self):
469 # Each image in the tarball is signed.
470 self.setUpUefiKeys()
471 self.openArchive("test", "1.0", "amd64")
472 self.archive.add_file("1.0/empty.efi", "")
473 upload = self.process()
474 self.assertEqual(1, upload.signUefi.call_count)
475
476 def test_signs_kmod_image(self):
477 # Each image in the tarball is signed.
478 self.setUpKmodKeys()
479 self.openArchive("test", "1.0", "amd64")
480 self.archive.add_file("1.0/empty.ko", "")
481 upload = self.process()
482 self.assertEqual(1, upload.signKmod.call_count)
483
484 def test_signs_combo_image(self):
485 # Each image in the tarball is signed.
486 self.setUpKmodKeys()
487 self.openArchive("test", "1.0", "amd64")
488 self.archive.add_file("1.0/empty.efi", "")
489 self.archive.add_file("1.0/empty.ko", "")
490 self.archive.add_file("1.0/empty2.ko", "")
491 upload = self.process()
492 self.assertEqual(1, upload.signUefi.call_count)
493 self.assertEqual(2, upload.signKmod.call_count)
215494
216 def test_installed(self):495 def test_installed(self):
217 # Files in the tarball are installed correctly.496 # Files in the tarball are installed correctly.
218 self.setUpKeyAndCert()497 self.setUpUefiKeys()
219 self.openArchive("test", "1.0", "amd64")498 self.openArchive("test", "1.0", "amd64")
220 self.archive.add_file("1.0/empty.efi", "")499 self.archive.add_file("1.0/empty.efi", "")
221 self.process()500 self.process()
@@ -231,7 +510,7 @@
231 def test_installed_existing_uefi(self):510 def test_installed_existing_uefi(self):
232 # Files in the tarball are installed correctly.511 # Files in the tarball are installed correctly.
233 os.makedirs(os.path.join(self.getDistsPath(), "uefi"))512 os.makedirs(os.path.join(self.getDistsPath(), "uefi"))
234 self.setUpKeyAndCert()513 self.setUpUefiKeys()
235 self.openArchive("test", "1.0", "amd64")514 self.openArchive("test", "1.0", "amd64")
236 self.archive.add_file("1.0/empty.efi", "")515 self.archive.add_file("1.0/empty.efi", "")
237 self.process()516 self.process()
@@ -247,7 +526,7 @@
247 def test_installed_existing_signing(self):526 def test_installed_existing_signing(self):
248 # Files in the tarball are installed correctly.527 # Files in the tarball are installed correctly.
249 os.makedirs(os.path.join(self.getDistsPath(), "signing"))528 os.makedirs(os.path.join(self.getDistsPath(), "signing"))
250 self.setUpKeyAndCert()529 self.setUpUefiKeys()
251 self.openArchive("test", "1.0", "amd64")530 self.openArchive("test", "1.0", "amd64")
252 self.archive.add_file("1.0/empty.efi", "")531 self.archive.add_file("1.0/empty.efi", "")
253 self.process()532 self.process()
@@ -262,33 +541,68 @@
262541
263 def test_create_uefi_keys_autokey_off(self):542 def test_create_uefi_keys_autokey_off(self):
264 # Keys are not created.543 # Keys are not created.
265 self.setUpKeyAndCert(create=False)544 self.setUpUefiKeys(create=False)
266 self.assertFalse(os.path.exists(self.key))545 self.assertFalse(os.path.exists(self.key))
267 self.assertFalse(os.path.exists(self.cert))546 self.assertFalse(os.path.exists(self.cert))
268 fake_call = FakeMethod()547 fake_call = FakeMethod(result=0)
269 self.useFixture(MonkeyPatch("subprocess.call", fake_call))548 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
270 upload = SigningUpload()549 upload = SigningUpload()
271 upload.generateUefiKeys = FakeMethodGenUefiKeys(upload=upload)550 upload.callLog = FakeMethodCallLog(upload=upload)
272 upload.setTargetDirectory(551 upload.setTargetDirectory(
273 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")552 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
274 upload.signUefi('t.efi')553 upload.signUefi('t.efi')
275 self.assertEqual(0, upload.generateUefiKeys.call_count)554 self.assertEqual(0, upload.callLog.caller_count('UEFI keygen'))
276 self.assertFalse(os.path.exists(self.key))555 self.assertFalse(os.path.exists(self.key))
277 self.assertFalse(os.path.exists(self.cert))556 self.assertFalse(os.path.exists(self.cert))
278557
279 def test_create_uefi_keys_autokey_on(self):558 def test_create_uefi_keys_autokey_on(self):
280 # Keys are created on demand.559 # Keys are created on demand.
281 self.setUpPPA()560 self.setUpPPA()
282 self.setUpKeyAndCert(create=False)561 self.setUpUefiKeys(create=False)
283 self.assertFalse(os.path.exists(self.key))562 self.assertFalse(os.path.exists(self.key))
284 self.assertFalse(os.path.exists(self.cert))563 self.assertFalse(os.path.exists(self.cert))
285 fake_call = FakeMethod()564 fake_call = FakeMethod(result=0)
286 self.useFixture(MonkeyPatch("subprocess.call", fake_call))565 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
287 upload = SigningUpload()566 upload = SigningUpload()
288 upload.generateUefiKeys = FakeMethodGenUefiKeys(upload=upload)567 upload.callLog = FakeMethodCallLog(upload=upload)
289 upload.setTargetDirectory(568 upload.setTargetDirectory(
290 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")569 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
291 upload.signUefi('t.efi')570 upload.signUefi('t.efi')
292 self.assertEqual(1, upload.generateUefiKeys.call_count)571 self.assertEqual(1, upload.callLog.caller_count('UEFI keygen'))
293 self.assertTrue(os.path.exists(self.key))572 self.assertTrue(os.path.exists(self.key))
294 self.assertTrue(os.path.exists(self.cert))573 self.assertTrue(os.path.exists(self.cert))
574
575 def test_create_kmod_keys_autokey_off(self):
576 # Keys are not created.
577 self.setUpKmodKeys(create=False)
578 self.assertFalse(os.path.exists(self.kmod_pem))
579 self.assertFalse(os.path.exists(self.kmod_x509))
580 fake_call = FakeMethod(result=0)
581 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
582 upload = SigningUpload()
583 upload.callLog = FakeMethodCallLog(upload=upload)
584 upload.setTargetDirectory(
585 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
586 upload.signKmod('t.ko')
587 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen key'))
588 self.assertEqual(0, upload.callLog.caller_count('Kmod keygen cert'))
589 self.assertFalse(os.path.exists(self.kmod_pem))
590 self.assertFalse(os.path.exists(self.kmod_x509))
591
592 def test_create_kmod_keys_autokey_on(self):
593 # Keys are created on demand.
594 self.setUpPPA()
595 self.setUpKmodKeys(create=False)
596 self.assertFalse(os.path.exists(self.kmod_pem))
597 self.assertFalse(os.path.exists(self.kmod_x509))
598 fake_call = FakeMethod(result=0)
599 self.useFixture(MonkeyPatch("subprocess.call", fake_call))
600 upload = SigningUpload()
601 upload.callLog = FakeMethodCallLog(upload=upload)
602 upload.setTargetDirectory(
603 self.pubconf, "test_1.0_amd64.tar.gz", "distroseries")
604 upload.signKmod('t.ko')
605 self.assertEqual(1, upload.callLog.caller_count('Kmod keygen key'))
606 self.assertEqual(1, upload.callLog.caller_count('Kmod keygen cert'))
607 self.assertTrue(os.path.exists(self.kmod_pem))
608 self.assertTrue(os.path.exists(self.kmod_x509))