Status: | Merged |
---|---|
Approved by: | Michael Vogt |
Approved revision: | 501 |
Merged at revision: | 442 |
Proposed branch: | lp:click/devel |
Merge into: | lp:click |
Diff against target: |
734 lines (+503/-19) 13 files modified
Makefile.am.coverage (+1/-0) click/commands/info.py (+25/-9) click/commands/install.py (+6/-1) click/commands/verify.py (+6/-1) click/install.py (+53/-1) click/tests/test_install.py (+23/-0) debian/changelog (+12/-0) debian/tests/control (+1/-1) tests/integration/test_hook.py (+2/-1) tests/integration/test_info.py (+17/-1) tests/integration/test_install.py (+5/-2) tests/integration/test_signatures.py (+348/-0) tests/integration/test_verify.py (+4/-2) |
To merge this branch: | bzr merge lp:click/devel |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
click hackers | Pending | ||
Review via email: mp+230523@code.launchpad.net |
Commit message
Click 0.4.31: "click info <file in unpacked package>", and basic support for package signing.
Description of the change
[ Michael Vogt ]
* Add "click info" interface to get manifest corresponding to file in
installed package (LP: #1324853).
* Add support for click package gpg signatures (LP: #1330770).
[ Colin Watson ]
* Ugly hack to get coverage reporting working again with gcovr 3.1.
* Pass --allow-
plugin for now. Revert this when a keyring and signing policies are in
place on the phone images and confirmed to work.
To post a comment you must log in.
lp:click/devel
updated
- 501. By Michael Vogt
-
trivial pep8 fix
- 502. By Michael Vogt
-
bump version number for citrain
- 503. By Michael Vogt
-
tests/integration/: fix tests by adding --allow-
unauthenticated - 504. By Michael Vogt
-
bump version number
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile.am.coverage' |
2 | --- Makefile.am.coverage 2014-07-02 16:51:32 +0000 |
3 | +++ Makefile.am.coverage 2014-08-22 17:12:39 +0000 |
4 | @@ -38,6 +38,7 @@ |
5 | generate-coverage-gcovr: |
6 | @echo Generating coverage GCOVR report |
7 | $(GCOVR) -x -r $(top_builddir) -o coverage-c.xml |
8 | + sed -i 's/\(<package .*name=\)""/\1"lib.click..libs"/' coverage-c.xml |
9 | |
10 | clean-coverage-gcovr: clean-gcda |
11 | rm -f coverage-c.xml |
12 | |
13 | === modified file 'click/commands/info.py' |
14 | --- click/commands/info.py 2014-04-03 08:52:02 +0000 |
15 | +++ click/commands/info.py 2014-08-22 17:12:39 +0000 |
16 | @@ -18,8 +18,10 @@ |
17 | from __future__ import print_function |
18 | |
19 | from contextlib import closing |
20 | +import glob |
21 | import json |
22 | from optparse import OptionParser |
23 | +import os |
24 | import sys |
25 | |
26 | from gi.repository import Click |
27 | @@ -28,6 +30,15 @@ |
28 | from click.json_helpers import json_object_to_python |
29 | |
30 | |
31 | +def _load_manifest(manifest_file): |
32 | + manifest = json.load(manifest_file) |
33 | + keys = list(manifest) |
34 | + for key in keys: |
35 | + if key.startswith("_"): |
36 | + del manifest[key] |
37 | + return manifest |
38 | + |
39 | + |
40 | def get_manifest(options, arg): |
41 | if "/" not in arg: |
42 | db = Click.DB() |
43 | @@ -38,15 +49,20 @@ |
44 | if registry.has_package_name(arg): |
45 | return json_object_to_python(registry.get_manifest(arg)) |
46 | |
47 | - with closing(DebFile(filename=arg)) as package: |
48 | - with package.control.get_file( |
49 | - "manifest", encoding="UTF-8") as manifest_file: |
50 | - manifest = json.load(manifest_file) |
51 | - keys = list(manifest) |
52 | - for key in keys: |
53 | - if key.startswith("_"): |
54 | - del manifest[key] |
55 | - return manifest |
56 | + if arg.endswith(".click"): |
57 | + with closing(DebFile(filename=arg)) as package: |
58 | + with package.control.get_file( |
59 | + "manifest", encoding="UTF-8") as manifest_file: |
60 | + return _load_manifest(manifest_file) |
61 | + else: |
62 | + pkgdir = Click.find_package_directory(arg) |
63 | + manifest_path = glob.glob( |
64 | + os.path.join(pkgdir, ".click", "info", "*.manifest")) |
65 | + if len(manifest_path) > 1: |
66 | + raise Exception("Multiple manifest files found in '%s'" % ( |
67 | + manifest_path)) |
68 | + with open(manifest_path[0]) as f: |
69 | + return _load_manifest(f) |
70 | |
71 | |
72 | def run(argv): |
73 | |
74 | === modified file 'click/commands/install.py' |
75 | --- click/commands/install.py 2014-04-03 08:52:02 +0000 |
76 | +++ click/commands/install.py 2014-08-22 17:12:39 +0000 |
77 | @@ -43,6 +43,9 @@ |
78 | parser.add_option( |
79 | "--all-users", default=False, action="store_true", |
80 | help="register package for all users") |
81 | + parser.add_option( |
82 | + "--allow-unauthenticated", default=False, action="store_true", |
83 | + help="allow installing packages with no sigantures") |
84 | options, args = parser.parse_args(argv) |
85 | if len(args) < 1: |
86 | parser.error("need package file name") |
87 | @@ -51,7 +54,9 @@ |
88 | if options.root is not None: |
89 | db.add(options.root) |
90 | package_path = args[0] |
91 | - installer = ClickInstaller(db, options.force_missing_framework) |
92 | + installer = ClickInstaller( |
93 | + db=db, force_missing_framework=options.force_missing_framework, |
94 | + allow_unauthenticated=options.allow_unauthenticated) |
95 | try: |
96 | installer.install( |
97 | package_path, user=options.user, all_users=options.all_users) |
98 | |
99 | === modified file 'click/commands/verify.py' |
100 | --- click/commands/verify.py 2013-09-23 21:24:37 +0000 |
101 | +++ click/commands/verify.py 2014-08-22 17:12:39 +0000 |
102 | @@ -29,10 +29,15 @@ |
103 | parser.add_option( |
104 | "--force-missing-framework", action="store_true", default=False, |
105 | help="ignore missing system framework") |
106 | + parser.add_option( |
107 | + "--allow-unauthenticated", action="store_true", default=False, |
108 | + help="allow installing packages with no sigantures") |
109 | options, args = parser.parse_args(argv) |
110 | if len(args) < 1: |
111 | parser.error("need package file name") |
112 | package_path = args[0] |
113 | - installer = ClickInstaller(None, options.force_missing_framework) |
114 | + installer = ClickInstaller( |
115 | + db=None, force_missing_framework=options.force_missing_framework, |
116 | + allow_unauthenticated=options.allow_unauthenticated) |
117 | installer.audit(package_path, slow=True) |
118 | return 0 |
119 | |
120 | === modified file 'click/install.py' |
121 | --- click/install.py 2014-05-05 13:10:19 +0000 |
122 | +++ click/install.py 2014-08-22 17:12:39 +0000 |
123 | @@ -30,6 +30,7 @@ |
124 | import grp |
125 | import inspect |
126 | import json |
127 | +import logging |
128 | import os |
129 | import pwd |
130 | import shutil |
131 | @@ -74,6 +75,45 @@ |
132 | apt_pkg.init_system() |
133 | |
134 | |
135 | +class DebsigVerifyError(Exception): |
136 | + pass |
137 | + |
138 | + |
139 | +class DebsigVerify: |
140 | + """Tiny wrapper around the debsig-verify commandline""" |
141 | + # from debsig-verify-0.9/debsigs.h |
142 | + DS_SUCCESS = 0 |
143 | + DS_FAIL_NOSIGS = 10 |
144 | + DS_FAIL_UNKNOWN_ORIGIN = 11 |
145 | + DS_FAIL_NOPOLICIES = 12 |
146 | + DS_FAIL_BADSIG = 13 |
147 | + DS_FAIL_INTERNAL = 14 |
148 | + |
149 | + # should be a property, but python does not support support |
150 | + # class properties easily |
151 | + @classmethod |
152 | + def available(cls): |
153 | + return Click.find_on_path("debsig-verify") |
154 | + |
155 | + @classmethod |
156 | + def verify(cls, path, allow_unauthenticated): |
157 | + command = ["debsig-verify"] + [path] |
158 | + try: |
159 | + subprocess.check_output(command, universal_newlines=True) |
160 | + except subprocess.CalledProcessError as e: |
161 | + if (allow_unauthenticated and |
162 | + e.returncode in (DebsigVerify.DS_FAIL_NOSIGS, |
163 | + DebsigVerify.DS_FAIL_UNKNOWN_ORIGIN, |
164 | + DebsigVerify.DS_FAIL_NOPOLICIES)): |
165 | + logging.warning( |
166 | + "Signature check failed, but installing anyway " |
167 | + "as requested") |
168 | + else: |
169 | + raise DebsigVerifyError( |
170 | + "Signature verification error: %s" % e.output) |
171 | + return True |
172 | + |
173 | + |
174 | class ClickInstallerError(Exception): |
175 | pass |
176 | |
177 | @@ -87,9 +127,11 @@ |
178 | |
179 | |
180 | class ClickInstaller: |
181 | - def __init__(self, db, force_missing_framework=False): |
182 | + def __init__(self, db, force_missing_framework=False, |
183 | + allow_unauthenticated=False): |
184 | self.db = db |
185 | self.force_missing_framework = force_missing_framework |
186 | + self.allow_unauthenticated = allow_unauthenticated |
187 | |
188 | def _preload_path(self): |
189 | if "CLICK_PACKAGE_PRELOAD" in os.environ: |
190 | @@ -125,6 +167,16 @@ |
191 | subprocess.check_call(command, env=env, **kwargs) |
192 | |
193 | def audit(self, path, slow=False, check_arch=False): |
194 | + # always do the signature check first |
195 | + if DebsigVerify.available(): |
196 | + try: |
197 | + DebsigVerify.verify(path, self.allow_unauthenticated) |
198 | + except DebsigVerifyError as e: |
199 | + raise ClickInstallerAuditError(str(e)) |
200 | + else: |
201 | + logging.warning( |
202 | + "debsig-verify not available; cannot check signatures") |
203 | + |
204 | with closing(DebFile(filename=path)) as package: |
205 | control_fields = package.control.debcontrol() |
206 | |
207 | |
208 | === modified file 'click/tests/test_install.py' |
209 | --- click/tests/test_install.py 2014-05-19 13:08:57 +0000 |
210 | +++ click/tests/test_install.py 2014-08-22 17:12:39 +0000 |
211 | @@ -75,6 +75,12 @@ |
212 | self.use_temp_dir() |
213 | self.db = Click.DB() |
214 | self.db.add(self.temp_dir) |
215 | + # mock signature checks during the tests |
216 | + self.debsig_patcher = mock.patch("click.install.DebsigVerify") |
217 | + self.debsig_patcher.start() |
218 | + |
219 | + def tearDown(self): |
220 | + self.debsig_patcher.stop() |
221 | |
222 | def make_fake_package(self, control_fields=None, manifest=None, |
223 | control_scripts=None, data_files=None): |
224 | @@ -232,6 +238,23 @@ |
225 | 'Framework "missing" not present on system.*', |
226 | ClickInstaller(self.db).audit, path) |
227 | |
228 | + # FIXME: we really want a unit test with a valid signature too |
229 | + def test_audit_no_signature(self): |
230 | + if not Click.find_on_path("debsig-verify"): |
231 | + self.skipTest("this test needs debsig-verify") |
232 | + path = self.make_fake_package( |
233 | + control_fields={"Click-Version": "0.4"}, |
234 | + manifest={ |
235 | + "name": "test-package", |
236 | + "version": "1.0", |
237 | + "framework": "", |
238 | + }) |
239 | + self.debsig_patcher.stop() |
240 | + self.assertRaisesRegex( |
241 | + ClickInstallerAuditError, "Signature verification error", |
242 | + ClickInstaller(self.db).audit, path) |
243 | + self.debsig_patcher.start() |
244 | + |
245 | @disable_logging |
246 | def test_audit_missing_framework_force(self): |
247 | with self.run_in_subprocess( |
248 | |
249 | === modified file 'debian/changelog' |
250 | --- debian/changelog 2014-08-06 23:33:23 +0000 |
251 | +++ debian/changelog 2014-08-22 17:12:39 +0000 |
252 | @@ -1,3 +1,15 @@ |
253 | +click (0.4.31.2) UNRELEASED; urgency=medium |
254 | + |
255 | + [ Michael Vogt ] |
256 | + * Add "click info" interface to get manifest corresponding to file in |
257 | + installed package (LP: #1324853). |
258 | + * Add support for click package gpg signatures (LP: #1330770). |
259 | + |
260 | + [ Colin Watson ] |
261 | + * Ugly hack to get coverage reporting working again with gcovr 3.1. |
262 | + |
263 | + -- Colin Watson <cjwatson@ubuntu.com> Tue, 12 Aug 2014 14:32:38 +0100 |
264 | + |
265 | click (0.4.30) utopic; urgency=medium |
266 | |
267 | [ Colin Watson ] |
268 | |
269 | === modified file 'debian/tests/control' |
270 | --- debian/tests/control 2014-06-30 14:03:36 +0000 |
271 | +++ debian/tests/control 2014-08-22 17:12:39 +0000 |
272 | @@ -1,3 +1,3 @@ |
273 | Tests: run-tests.sh |
274 | -Depends: @, iputils-ping, click-dev, schroot, debootstrap, sudo, bzr |
275 | +Depends: @, iputils-ping, click-dev, schroot, debootstrap, sudo, bzr, debsigs, debsig-verify |
276 | Restrictions: needs-root allow-stderr |
277 | |
278 | === added directory 'tests/integration/data' |
279 | === added directory 'tests/integration/data/evil-keyring' |
280 | === added file 'tests/integration/data/evil-keyring/pubring.gpg' |
281 | Binary files tests/integration/data/evil-keyring/pubring.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/evil-keyring/pubring.gpg 2014-08-22 17:12:39 +0000 differ |
282 | === added file 'tests/integration/data/evil-keyring/secring.gpg' |
283 | Binary files tests/integration/data/evil-keyring/secring.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/evil-keyring/secring.gpg 2014-08-22 17:12:39 +0000 differ |
284 | === added file 'tests/integration/data/evil-keyring/trustdb.gpg' |
285 | Binary files tests/integration/data/evil-keyring/trustdb.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/evil-keyring/trustdb.gpg 2014-08-22 17:12:39 +0000 differ |
286 | === added directory 'tests/integration/data/origin-keyring' |
287 | === added file 'tests/integration/data/origin-keyring/pubring.gpg' |
288 | Binary files tests/integration/data/origin-keyring/pubring.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/origin-keyring/pubring.gpg 2014-08-22 17:12:39 +0000 differ |
289 | === added file 'tests/integration/data/origin-keyring/secring.gpg' |
290 | Binary files tests/integration/data/origin-keyring/secring.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/origin-keyring/secring.gpg 2014-08-22 17:12:39 +0000 differ |
291 | === added file 'tests/integration/data/origin-keyring/trustdb.gpg' |
292 | Binary files tests/integration/data/origin-keyring/trustdb.gpg 1970-01-01 00:00:00 +0000 and tests/integration/data/origin-keyring/trustdb.gpg 2014-08-22 17:12:39 +0000 differ |
293 | === modified file 'tests/integration/test_hook.py' |
294 | --- tests/integration/test_hook.py 2014-08-03 13:10:48 +0000 |
295 | +++ tests/integration/test_hook.py 2014-08-22 17:12:39 +0000 |
296 | @@ -64,7 +64,8 @@ |
297 | click_pkg_name, framework="", hooks=hooks) |
298 | user = os.environ.get("USER", "root") |
299 | subprocess.check_call( |
300 | - [self.click_binary, "install", "--user=%s" % user, click_pkg], |
301 | + [self.click_binary, "install", "--allow-unauthenticated", |
302 | + "--user=%s" % user, click_pkg], |
303 | universal_newlines=True) |
304 | self.addCleanup( |
305 | subprocess.check_call, |
306 | |
307 | === modified file 'tests/integration/test_info.py' |
308 | --- tests/integration/test_info.py 2014-06-26 12:00:09 +0000 |
309 | +++ tests/integration/test_info.py 2014-08-22 17:12:39 +0000 |
310 | @@ -27,4 +27,20 @@ |
311 | path_to_click = self._make_click(name) |
312 | output = subprocess.check_output([ |
313 | self.click_binary, "info", path_to_click], universal_newlines=True) |
314 | - self.assertEqual(json.loads(output)["name"], name) |
315 | + self.assertEqual(name, json.loads(output)["name"]) |
316 | + |
317 | + def test_info_file_in_package(self): |
318 | + name = "org.example.info" |
319 | + version = "1.0" |
320 | + click_pkg = self._make_click(name=name, version=version, framework="") |
321 | + subprocess.check_call( |
322 | + [self.click_binary, "install", "--allow-unauthenticated", |
323 | + "--all-users", click_pkg]) |
324 | + self.addCleanup( |
325 | + subprocess.check_call, |
326 | + [self.click_binary, "unregister", "--all-users", name]) |
327 | + output = subprocess.check_output( |
328 | + [self.click_binary, "info", |
329 | + "/opt/click.ubuntu.com/%s/%s/README" % (name, version)], |
330 | + universal_newlines=True) |
331 | + self.assertEqual(name, json.loads(output)["name"]) |
332 | |
333 | === modified file 'tests/integration/test_install.py' |
334 | --- tests/integration/test_install.py 2014-06-26 12:02:18 +0000 |
335 | +++ tests/integration/test_install.py 2014-08-22 17:12:39 +0000 |
336 | @@ -60,6 +60,7 @@ |
337 | # install it |
338 | subprocess.check_call([ |
339 | self.click_binary, "install", "--user=%s" % self.USER_1, |
340 | + "--allow-unauthenticated", |
341 | click_pkg], universal_newlines=True) |
342 | self.addCleanup(self.click_unregister, self.USER_1, "foo-1") |
343 | # ensure that user-1 has it |
344 | @@ -82,7 +83,8 @@ |
345 | click_pkg = self._make_click(name="foo-2", framework="") |
346 | # install it |
347 | subprocess.check_call( |
348 | - [self.click_binary, "install", "--all-users", click_pkg], |
349 | + [self.click_binary, "install", "--all-users", |
350 | + "--allow-unauthenticated", click_pkg], |
351 | universal_newlines=True) |
352 | self.addCleanup(self.click_unregister, "@all", "foo-2") |
353 | # ensure all users see it |
354 | @@ -95,7 +97,8 @@ |
355 | def test_pkgdir_after_install(self): |
356 | click_pkg = self._make_click(name="foo-2", version="1.2", framework="") |
357 | subprocess.check_call( |
358 | - [self.click_binary, "install", "--all-users", click_pkg], |
359 | + [self.click_binary, "install", "--all-users", |
360 | + "--allow-unauthenticated", click_pkg], |
361 | universal_newlines=True) |
362 | self.addCleanup(self.click_unregister, "@all", "foo-2") |
363 | # from the path |
364 | |
365 | === added file 'tests/integration/test_signatures.py' |
366 | --- tests/integration/test_signatures.py 1970-01-01 00:00:00 +0000 |
367 | +++ tests/integration/test_signatures.py 2014-08-22 17:12:39 +0000 |
368 | @@ -0,0 +1,348 @@ |
369 | +# Copyright (C) 2014 Canonical Ltd. |
370 | +# Author: Michael Vogt <michael.vogt@ubuntu.com> |
371 | + |
372 | +# This program is free software: you can redistribute it and/or modify |
373 | +# it under the terms of the GNU General Public License as published by |
374 | +# the Free Software Foundation; version 3 of the License. |
375 | +# |
376 | +# This program is distributed in the hope that it will be useful, |
377 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
378 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
379 | +# GNU General Public License for more details. |
380 | +# |
381 | +# You should have received a copy of the GNU General Public License |
382 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
383 | + |
384 | +"""Integration tests for the click signature checking.""" |
385 | + |
386 | +import copy |
387 | +import os |
388 | +import shutil |
389 | +import subprocess |
390 | +import tarfile |
391 | +import unittest |
392 | +from textwrap import dedent |
393 | + |
394 | +from .helpers import ( |
395 | + is_root, |
396 | + ClickTestCase, |
397 | +) |
398 | + |
399 | +def makedirs(path): |
400 | + try: |
401 | + os.makedirs(path) |
402 | + except OSError: |
403 | + pass |
404 | + |
405 | +def get_keyid_from_gpghome(gpg_home): |
406 | + """Return the public keyid of a given gpg home dir""" |
407 | + output = subprocess.check_output( |
408 | + ["gpg", "--home", gpg_home, "--list-keys", "--with-colons"], |
409 | + universal_newlines=True) |
410 | + for line in output.splitlines(): |
411 | + if not line.startswith("pub:"): |
412 | + continue |
413 | + return line.split(":")[4] |
414 | + raise ValueError("Cannot find public key in output: '%s'" % output) |
415 | + |
416 | + |
417 | +class Debsigs: |
418 | + """Tiny wrapper around the debsigs CLI""" |
419 | + def __init__(self, gpghome, keyid): |
420 | + self.keyid = keyid |
421 | + self.gpghome = gpghome |
422 | + self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid |
423 | + |
424 | + def sign(self, filepath, signature_type="origin"): |
425 | + """Sign the click at filepath""" |
426 | + env = copy.copy(os.environ) |
427 | + env["GNUPGHOME"] = os.path.abspath(self.gpghome) |
428 | + subprocess.check_call( |
429 | + ["debsigs", |
430 | + "--sign=%s" % signature_type, |
431 | + "--default-key=%s" % self.keyid, |
432 | + filepath], env=env) |
433 | + |
434 | + def install_signature_policy(self): |
435 | + """Install/update the system-wide signature policy""" |
436 | + xmls = dedent("""\ |
437 | + <?xml version="1.0"?> |
438 | + <!DOCTYPE Policy SYSTEM "http://www.debian.org/debsig/1.0/policy.dtd"> |
439 | + <Policy xmlns="http://www.debian.org/debsig/1.0/"> |
440 | + |
441 | + <Origin Name="test-origin" id="{keyid}" Description="Example policy"/> |
442 | + <Selection> |
443 | + <Required Type="origin" File="{filename}" id="{keyid}"/> |
444 | + </Selection> |
445 | + |
446 | + <Verification> |
447 | + <Required Type="origin" File="{filename}" id="{keyid}"/> |
448 | + </Verification> |
449 | + </Policy> |
450 | + """.format(keyid=self.keyid, filename="origin.pub")) |
451 | + makedirs(os.path.dirname(self.policy)) |
452 | + with open(self.policy, "w") as f: |
453 | + f.write(xmls) |
454 | + self.pubkey_path = ( |
455 | + "/usr/share/debsig/keyrings/%s/origin.pub" % self.keyid) |
456 | + makedirs(os.path.dirname(self.pubkey_path)) |
457 | + shutil.copy(os.path.join(self.gpghome, "pubring.gpg"), self.pubkey_path) |
458 | + |
459 | + def uninstall_signature_policy(self): |
460 | + # FIXME: update debsig-verify so that it can work from a different |
461 | + # root than "/" so that the tests do not have to use the |
462 | + # system root |
463 | + os.remove(self.policy) |
464 | + os.remove(self.pubkey_path) |
465 | + |
466 | + |
467 | +@unittest.skipIf(not is_root(), "This tests needs to run as root") |
468 | +class ClickSignaturesTestCase(ClickTestCase): |
469 | + def assertClickNoSignatureError(self, cmd_args): |
470 | + with self.assertRaises(subprocess.CalledProcessError) as cm: |
471 | + output = subprocess.check_output( |
472 | + [self.click_binary] + cmd_args, |
473 | + stderr=subprocess.STDOUT, universal_newlines=True) |
474 | + output = cm.exception.output |
475 | + expected_error_message = ("debsig: Origin Signature check failed. " |
476 | + "This deb might not be signed.") |
477 | + self.assertIn(expected_error_message, output) |
478 | + |
479 | + def assertClickInvalidSignatureError(self, cmd_args): |
480 | + with self.assertRaises(subprocess.CalledProcessError) as cm: |
481 | + output = subprocess.check_output( |
482 | + [self.click_binary] + cmd_args, |
483 | + stderr=subprocess.STDOUT, universal_newlines=True) |
484 | + print(output) |
485 | + |
486 | + output = cm.exception.output |
487 | + expected_error_message = "Signature verification error: " |
488 | + self.assertIn(expected_error_message, output) |
489 | + |
490 | + |
491 | +@unittest.skipIf(not is_root(), "This tests needs to run as root") |
492 | +class TestSignatureVerificationNoSignature(ClickSignaturesTestCase): |
493 | + def test_debsig_verify_no_sig(self): |
494 | + name = "org.example.debsig-no-sig" |
495 | + path_to_click = self._make_click(name, framework="") |
496 | + self.assertClickNoSignatureError(["verify", path_to_click]) |
497 | + |
498 | + def test_debsig_install_no_sig(self): |
499 | + name = "org.example.debsig-no-sig" |
500 | + path_to_click = self._make_click(name, framework="") |
501 | + self.assertClickNoSignatureError(["install", path_to_click]) |
502 | + |
503 | + def test_debsig_install_can_install_with_sig_override(self): |
504 | + name = "org.example.debsig-no-sig" |
505 | + path_to_click = self._make_click(name, framework="") |
506 | + user = os.environ.get("USER", "root") |
507 | + subprocess.check_call( |
508 | + [self.click_binary, "install", |
509 | + "--allow-unauthenticated", "--user=%s" % user, |
510 | + path_to_click]) |
511 | + self.addCleanup( |
512 | + subprocess.call, [self.click_binary, "unregister", |
513 | + "--user=%s" % user, name]) |
514 | + |
515 | + |
516 | +@unittest.skipIf(not is_root(), "This tests needs to run as root") |
517 | +class TestSignatureVerification(ClickSignaturesTestCase): |
518 | + def setUp(self): |
519 | + super(TestSignatureVerification, self).setUp() |
520 | + self.user = os.environ.get("USER", "root") |
521 | + # the valid origin keyring |
522 | + self.datadir = os.path.join(os.path.dirname(__file__), "data") |
523 | + origin_keyring_dir = os.path.abspath( |
524 | + os.path.join(self.datadir, "origin-keyring")) |
525 | + keyid = get_keyid_from_gpghome(origin_keyring_dir) |
526 | + self.debsigs = Debsigs(origin_keyring_dir, keyid) |
527 | + self.debsigs.install_signature_policy() |
528 | + |
529 | + def tearDown(self): |
530 | + self.debsigs.uninstall_signature_policy() |
531 | + |
532 | + def test_debsig_install_valid_signature(self): |
533 | + name = "org.example.debsig-valid-sig" |
534 | + path_to_click = self._make_click(name, framework="") |
535 | + self.debsigs.sign(path_to_click) |
536 | + subprocess.check_call( |
537 | + [self.click_binary, "install", |
538 | + "--user=%s" % self.user, |
539 | + path_to_click]) |
540 | + self.addCleanup( |
541 | + subprocess.call, [self.click_binary, "unregister", |
542 | + "--user=%s" % self.user, name]) |
543 | + output = subprocess.check_output( |
544 | + [self.click_binary, "list", "--user=%s" % self.user], |
545 | + universal_newlines=True) |
546 | + self.assertIn(name, output) |
547 | + |
548 | + def test_debsig_install_signature_not_in_keyring(self): |
549 | + name = "org.example.debsig-no-keyring-sig" |
550 | + path_to_click = self._make_click(name, framework="") |
551 | + evil_keyring_dir = os.path.join(self.datadir, "evil-keyring") |
552 | + keyid = get_keyid_from_gpghome(evil_keyring_dir) |
553 | + debsig_bad = Debsigs(evil_keyring_dir, keyid) |
554 | + debsig_bad.sign(path_to_click) |
555 | + # and ensure its really not there |
556 | + self.assertClickInvalidSignatureError(["install", path_to_click]) |
557 | + output = subprocess.check_output( |
558 | + [self.click_binary, "list", "--user=%s" % self.user], |
559 | + universal_newlines=True) |
560 | + self.assertNotIn(name, output) |
561 | + |
562 | + def test_debsig_install_not_a_signature(self): |
563 | + name = "org.example.debsig-invalid-sig" |
564 | + path_to_click = self._make_click(name, framework="") |
565 | + invalid_sig = os.path.join(self.temp_dir, "_gpgorigin") |
566 | + with open(invalid_sig, "w") as f: |
567 | + f.write("no-valid-signature") |
568 | + # add a invalid sig |
569 | + subprocess.check_call(["ar", "-r", path_to_click, invalid_sig]) |
570 | + self.assertClickInvalidSignatureError(["install", path_to_click]) |
571 | + output = subprocess.check_output( |
572 | + [self.click_binary, "list", "--user=%s" % self.user], |
573 | + universal_newlines=True) |
574 | + self.assertNotIn(name, output) |
575 | + |
576 | + def test_debsig_install_signature_altered_click(self): |
577 | + def modify_ar_member(member): |
578 | + subprocess.check_call( |
579 | + ["ar", "-x", path_to_click, "control.tar.gz"], |
580 | + cwd=self.temp_dir) |
581 | + altered_member = os.path.join(self.temp_dir, member) |
582 | + with open(altered_member, "ba") as f: |
583 | + f.write(b"\0") |
584 | + subprocess.check_call(["ar", "-r", path_to_click, altered_member]) |
585 | + |
586 | + # ensure that all members we care about are checked by debsig-verify |
587 | + for member in ["control.tar.gz", "data.tar.gz", "debian-binary"]: |
588 | + name = "org.example.debsig-altered-click" |
589 | + path_to_click = self._make_click(name, framework="") |
590 | + self.debsigs.sign(path_to_click) |
591 | + modify_ar_member(member) |
592 | + self.assertClickInvalidSignatureError(["install", path_to_click]) |
593 | + output = subprocess.check_output( |
594 | + [self.click_binary, "list", "--user=%s" % self.user], |
595 | + universal_newlines=True) |
596 | + self.assertNotIn(name, output) |
597 | + |
598 | + def make_nasty_data_tar(self, compression): |
599 | + new_data_tar = os.path.join(self.temp_dir, "data.tar." + compression) |
600 | + evilfile = os.path.join(self.temp_dir, "README.evil") |
601 | + with open(evilfile, "w") as f: |
602 | + f.write("I am a nasty README") |
603 | + with tarfile.open(new_data_tar, "w:"+compression) as tar: |
604 | + tar.add(evilfile) |
605 | + return new_data_tar |
606 | + |
607 | + def test_debsig_install_signature_injected_data_tar(self): |
608 | + name = "org.example.debsig-injected-data-click" |
609 | + path_to_click = self._make_click(name, framework="") |
610 | + self.debsigs.sign(path_to_click) |
611 | + new_data = self.make_nasty_data_tar("bz2") |
612 | + # insert before the real data.tar.gz and ensure this is caught |
613 | + # NOTE: that right now this will not be caught by debsig-verify |
614 | + # but later in audit() by debian.debfile.DebFile() |
615 | + subprocess.check_call(["ar", |
616 | + "-r", |
617 | + "-b", "data.tar.gz", |
618 | + path_to_click, |
619 | + new_data]) |
620 | + output = subprocess.check_output( |
621 | + ["ar", "-t", path_to_click], universal_newlines=True) |
622 | + self.assertEqual(output.splitlines(), |
623 | + ["debian-binary", |
624 | + "_click-binary", |
625 | + "control.tar.gz", |
626 | + "data.tar.bz2", |
627 | + "data.tar.gz", |
628 | + "_gpgorigin"]) |
629 | + with self.assertRaises(subprocess.CalledProcessError): |
630 | + output = subprocess.check_output( |
631 | + [self.click_binary, "install", path_to_click], |
632 | + stderr=subprocess.STDOUT, universal_newlines=True) |
633 | + output = subprocess.check_output( |
634 | + [self.click_binary, "list", "--user=%s" % self.user], |
635 | + universal_newlines=True) |
636 | + self.assertNotIn(name, output) |
637 | + |
638 | + def test_debsig_install_signature_replaced_data_tar(self): |
639 | + name = "org.example.debsig-replaced-data-click" |
640 | + path_to_click = self._make_click(name, framework="") |
641 | + self.debsigs.sign(path_to_click) |
642 | + new_data = self.make_nasty_data_tar("bz2") |
643 | + # replace data.tar.gz with data.tar.bz2 and ensure this is caught |
644 | + subprocess.check_call(["ar", |
645 | + "-d", |
646 | + path_to_click, |
647 | + "data.tar.gz", |
648 | + ]) |
649 | + subprocess.check_call(["ar", |
650 | + "-r", |
651 | + path_to_click, |
652 | + new_data]) |
653 | + output = subprocess.check_output( |
654 | + ["ar", "-t", path_to_click], universal_newlines=True) |
655 | + self.assertEqual(output.splitlines(), |
656 | + ["debian-binary", |
657 | + "_click-binary", |
658 | + "control.tar.gz", |
659 | + "_gpgorigin", |
660 | + "data.tar.bz2", |
661 | + ]) |
662 | + with self.assertRaises(subprocess.CalledProcessError) as cm: |
663 | + output = subprocess.check_output( |
664 | + [self.click_binary, "install", path_to_click], |
665 | + stderr=subprocess.STDOUT, universal_newlines=True) |
666 | + self.assertIn("Signature verification error", cm.exception.output) |
667 | + output = subprocess.check_output( |
668 | + [self.click_binary, "list", "--user=%s" % self.user], |
669 | + universal_newlines=True) |
670 | + self.assertNotIn(name, output) |
671 | + |
672 | + def test_debsig_install_signature_prepend_sig(self): |
673 | + # this test is probably not really needed, it tries to trick |
674 | + # the system by prepending a valid signature that is not |
675 | + # in the keyring. But given that debsig-verify only reads |
676 | + # the first packet of any given _gpg$foo signature its |
677 | + # equivalent to test_debsig_install_signature_not_in_keyring test |
678 | + name = "org.example.debsig-replaced-data-prepend-sig-click" |
679 | + path_to_click = self._make_click(name, framework="") |
680 | + self.debsigs.sign(path_to_click) |
681 | + new_data = self.make_nasty_data_tar("gz") |
682 | + # replace data.tar.gz |
683 | + subprocess.check_call(["ar", |
684 | + "-r", |
685 | + path_to_click, |
686 | + new_data, |
687 | + ]) |
688 | + # get previous good _gpgorigin for the old data |
689 | + subprocess.check_call( |
690 | + ["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir) |
691 | + with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f: |
692 | + good_gpg_origin = f.read() |
693 | + # and append a valid signature from a non-keyring key |
694 | + evil_keyring_dir = os.path.join(self.datadir, "evil-keyring") |
695 | + debsig_bad = Debsigs(evil_keyring_dir, "18B38B9AC1B67A0D") |
696 | + debsig_bad.sign(path_to_click) |
697 | + subprocess.check_call( |
698 | + ["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir) |
699 | + with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f: |
700 | + evil_gpg_origin = f.read() |
701 | + with open(os.path.join(self.temp_dir, "_gpgorigin"), "wb") as f: |
702 | + f.write(evil_gpg_origin) |
703 | + f.write(good_gpg_origin) |
704 | + subprocess.check_call( |
705 | + ["ar", "-r", path_to_click, "_gpgorigin"], cwd=self.temp_dir) |
706 | + # now ensure that the verification fails as well |
707 | + with self.assertRaises(subprocess.CalledProcessError) as cm: |
708 | + output = subprocess.check_output( |
709 | + [self.click_binary, "install", path_to_click], |
710 | + stderr=subprocess.STDOUT, universal_newlines=True) |
711 | + self.assertIn("Signature verification error", cm.exception.output) |
712 | + output = subprocess.check_output( |
713 | + [self.click_binary, "list", "--user=%s" % self.user], |
714 | + universal_newlines=True) |
715 | + self.assertNotIn(name, output) |
716 | + |
717 | |
718 | === modified file 'tests/integration/test_verify.py' |
719 | --- tests/integration/test_verify.py 2014-06-26 12:00:09 +0000 |
720 | +++ tests/integration/test_verify.py 2014-08-22 17:12:39 +0000 |
721 | @@ -22,9 +22,11 @@ |
722 | |
723 | class TestVerify(ClickTestCase): |
724 | def test_verify_ok(self): |
725 | - name = "com.ubuntu.verify-ok" |
726 | + name = "com.example.verify-ok" |
727 | path_to_click = self._make_click(name) |
728 | output = subprocess.check_output([ |
729 | - self.click_binary, "verify", "--force-missing-framework", |
730 | + self.click_binary, "verify", |
731 | + "--force-missing-framework", |
732 | + "--allow-unauthenticated", |
733 | path_to_click], universal_newlines=True) |
734 | self.assertEqual(output, "") |