1
# Copyright (C) 2014 Canonical Ltd.
2
# Author: Michael Vogt <michael.vogt@ubuntu.com>
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; version 3 of the License.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
"""Integration tests for the click signature checking."""
23
from textwrap import dedent
25
from .helpers import (
38
def get_keyid_from_gpghome(gpg_home):
39
"""Return the public keyid of a given gpg home dir"""
40
output = subprocess.check_output(
41
["gpg", "--home", gpg_home, "--list-keys", "--with-colons"],
42
universal_newlines=True)
43
for line in output.splitlines():
44
if not line.startswith("pub:"):
46
return line.split(":")[4]
47
raise ValueError("Cannot find public key in output: '%s'" % output)
51
"""Tiny wrapper around the debsigs CLI"""
52
def __init__(self, gpghome, keyid):
54
self.gpghome = gpghome
55
self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid
57
def sign(self, filepath, signature_type="origin"):
58
"""Sign the click at filepath"""
59
env = copy.copy(os.environ)
60
env["GNUPGHOME"] = os.path.abspath(self.gpghome)
61
subprocess.check_call(
63
"--sign=%s" % signature_type,
64
"--default-key=%s" % self.keyid,
67
def install_signature_policy(self):
68
"""Install/update the system-wide signature policy"""
71
<!DOCTYPE Policy SYSTEM "http://www.debian.org/debsig/1.0/policy.dtd">
72
<Policy xmlns="http://www.debian.org/debsig/1.0/">
74
<Origin Name="test-origin" id="{keyid}" Description="Example policy"/>
76
<Required Type="origin" File="{filename}" id="{keyid}"/>
80
<Required Type="origin" File="{filename}" id="{keyid}"/>
83
""".format(keyid=self.keyid, filename="origin.pub"))
84
makedirs(os.path.dirname(self.policy))
85
with open(self.policy, "w") as f:
88
"/usr/share/debsig/keyrings/%s/origin.pub" % self.keyid)
89
makedirs(os.path.dirname(self.pubkey_path))
91
os.path.join(self.gpghome, "pubring.gpg"), self.pubkey_path)
93
def uninstall_signature_policy(self):
94
# FIXME: update debsig-verify so that it can work from a different
95
# root than "/" so that the tests do not have to use the
97
os.remove(self.policy)
98
os.remove(self.pubkey_path)
101
class ClickSignaturesTestCase(ClickTestCase):
105
super(ClickSignaturesTestCase, cls).setUpClass()
108
def assertClickNoSignatureError(self, cmd_args):
109
with self.assertRaises(subprocess.CalledProcessError) as cm:
110
output = subprocess.check_output(
111
[self.click_binary] + cmd_args,
112
stderr=subprocess.STDOUT, universal_newlines=True)
113
output = cm.exception.output
114
expected_error_message = ("debsig: Origin Signature check failed. "
115
"This deb might not be signed.")
116
self.assertIn(expected_error_message, output)
118
def assertClickInvalidSignatureError(self, cmd_args):
119
with self.assertRaises(subprocess.CalledProcessError) as cm:
120
output = subprocess.check_output(
121
[self.click_binary] + cmd_args,
122
stderr=subprocess.STDOUT, universal_newlines=True)
125
output = cm.exception.output
126
expected_error_message = "Signature verification error: "
127
self.assertIn(expected_error_message, output)
130
class TestSignatureVerificationNoSignature(ClickSignaturesTestCase):
134
super(TestSignatureVerificationNoSignature, cls).setUpClass()
137
def test_debsig_verify_no_sig(self):
138
name = "org.example.debsig-no-sig"
139
path_to_click = self._make_click(name, framework="")
140
self.assertClickNoSignatureError(["verify", path_to_click])
142
def test_debsig_install_no_sig(self):
143
name = "org.example.debsig-no-sig"
144
path_to_click = self._make_click(name, framework="")
145
self.assertClickNoSignatureError(["install", path_to_click])
147
def test_debsig_install_can_install_with_sig_override(self):
148
name = "org.example.debsig-no-sig"
149
path_to_click = self._make_click(name, framework="")
150
user = os.environ.get("USER", "root")
151
subprocess.check_call(
152
[self.click_binary, "install",
153
"--allow-unauthenticated", "--user=%s" % user,
156
subprocess.call, [self.click_binary, "unregister",
157
"--user=%s" % user, name])
160
class TestSignatureVerification(ClickSignaturesTestCase):
164
super(TestSignatureVerification, cls).setUpClass()
168
super(TestSignatureVerification, self).setUp()
169
self.user = os.environ.get("USER", "root")
170
# the valid origin keyring
171
self.datadir = os.path.join(os.path.dirname(__file__), "data")
172
origin_keyring_dir = os.path.abspath(
173
os.path.join(self.datadir, "origin-keyring"))
174
keyid = get_keyid_from_gpghome(origin_keyring_dir)
175
self.debsigs = Debsigs(origin_keyring_dir, keyid)
176
self.debsigs.install_signature_policy()
179
self.debsigs.uninstall_signature_policy()
181
def test_debsig_install_valid_signature(self):
182
name = "org.example.debsig-valid-sig"
183
path_to_click = self._make_click(name, framework="")
184
self.debsigs.sign(path_to_click)
185
subprocess.check_call(
186
[self.click_binary, "install",
187
"--user=%s" % self.user,
190
subprocess.call, [self.click_binary, "unregister",
191
"--user=%s" % self.user, name])
192
output = subprocess.check_output(
193
[self.click_binary, "list", "--user=%s" % self.user],
194
universal_newlines=True)
195
self.assertIn(name, output)
197
def test_debsig_install_signature_not_in_keyring(self):
198
name = "org.example.debsig-no-keyring-sig"
199
path_to_click = self._make_click(name, framework="")
200
evil_keyring_dir = os.path.join(self.datadir, "evil-keyring")
201
keyid = get_keyid_from_gpghome(evil_keyring_dir)
202
debsig_bad = Debsigs(evil_keyring_dir, keyid)
203
debsig_bad.sign(path_to_click)
204
# and ensure its really not there
205
self.assertClickInvalidSignatureError(["install", path_to_click])
206
output = subprocess.check_output(
207
[self.click_binary, "list", "--user=%s" % self.user],
208
universal_newlines=True)
209
self.assertNotIn(name, output)
211
def test_debsig_install_not_a_signature(self):
212
name = "org.example.debsig-invalid-sig"
213
path_to_click = self._make_click(name, framework="")
214
invalid_sig = os.path.join(self.temp_dir, "_gpgorigin")
215
with open(invalid_sig, "w") as f:
216
f.write("no-valid-signature")
218
subprocess.check_call(["ar", "-r", path_to_click, invalid_sig])
219
self.assertClickInvalidSignatureError(["install", path_to_click])
220
output = subprocess.check_output(
221
[self.click_binary, "list", "--user=%s" % self.user],
222
universal_newlines=True)
223
self.assertNotIn(name, output)
225
def test_debsig_install_signature_altered_click(self):
226
def modify_ar_member(member):
227
subprocess.check_call(
228
["ar", "-x", path_to_click, "control.tar.gz"],
230
altered_member = os.path.join(self.temp_dir, member)
231
with open(altered_member, "ba") as f:
233
subprocess.check_call(["ar", "-r", path_to_click, altered_member])
235
# ensure that all members we care about are checked by debsig-verify
236
for member in ["control.tar.gz", "data.tar.gz", "debian-binary"]:
237
name = "org.example.debsig-altered-click"
238
path_to_click = self._make_click(name, framework="")
239
self.debsigs.sign(path_to_click)
240
modify_ar_member(member)
241
self.assertClickInvalidSignatureError(["install", path_to_click])
242
output = subprocess.check_output(
243
[self.click_binary, "list", "--user=%s" % self.user],
244
universal_newlines=True)
245
self.assertNotIn(name, output)
247
def make_nasty_data_tar(self, compression):
248
new_data_tar = os.path.join(self.temp_dir, "data.tar." + compression)
249
evilfile = os.path.join(self.temp_dir, "README.evil")
250
with open(evilfile, "w") as f:
251
f.write("I am a nasty README")
252
with tarfile.open(new_data_tar, "w:"+compression) as tar:
256
def test_debsig_install_signature_injected_data_tar(self):
257
name = "org.example.debsig-injected-data-click"
258
path_to_click = self._make_click(name, framework="")
259
self.debsigs.sign(path_to_click)
260
new_data = self.make_nasty_data_tar("bz2")
261
# insert before the real data.tar.gz and ensure this is caught
262
# NOTE: that right now this will not be caught by debsig-verify
263
# but later in audit() by debian.debfile.DebFile()
264
subprocess.check_call(["ar",
269
output = subprocess.check_output(
270
["ar", "-t", path_to_click], universal_newlines=True)
271
self.assertEqual(output.splitlines(),
278
with self.assertRaises(subprocess.CalledProcessError):
279
output = subprocess.check_output(
280
[self.click_binary, "install", path_to_click],
281
stderr=subprocess.STDOUT, universal_newlines=True)
282
output = subprocess.check_output(
283
[self.click_binary, "list", "--user=%s" % self.user],
284
universal_newlines=True)
285
self.assertNotIn(name, output)
287
def test_debsig_install_signature_replaced_data_tar(self):
288
name = "org.example.debsig-replaced-data-click"
289
path_to_click = self._make_click(name, framework="")
290
self.debsigs.sign(path_to_click)
291
new_data = self.make_nasty_data_tar("bz2")
292
# replace data.tar.gz with data.tar.bz2 and ensure this is caught
293
subprocess.check_call(["ar",
298
subprocess.check_call(["ar",
302
output = subprocess.check_output(
303
["ar", "-t", path_to_click], universal_newlines=True)
304
self.assertEqual(output.splitlines(),
311
with self.assertRaises(subprocess.CalledProcessError) as cm:
312
output = subprocess.check_output(
313
[self.click_binary, "install", path_to_click],
314
stderr=subprocess.STDOUT, universal_newlines=True)
315
self.assertIn("Signature verification error", cm.exception.output)
316
output = subprocess.check_output(
317
[self.click_binary, "list", "--user=%s" % self.user],
318
universal_newlines=True)
319
self.assertNotIn(name, output)
321
def test_debsig_install_signature_prepend_sig(self):
322
# this test is probably not really needed, it tries to trick
323
# the system by prepending a valid signature that is not
324
# in the keyring. But given that debsig-verify only reads
325
# the first packet of any given _gpg$foo signature it's
326
# equivalent to test_debsig_install_signature_not_in_keyring test
327
name = "org.example.debsig-replaced-data-prepend-sig-click"
328
path_to_click = self._make_click(name, framework="")
329
self.debsigs.sign(path_to_click)
330
new_data = self.make_nasty_data_tar("gz")
331
# replace data.tar.gz
332
subprocess.check_call(["ar",
337
# get previous good _gpgorigin for the old data
338
subprocess.check_call(
339
["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
340
with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f:
341
good_gpg_origin = f.read()
342
# and append a valid signature from a non-keyring key
343
evil_keyring_dir = os.path.join(self.datadir, "evil-keyring")
344
debsig_bad = Debsigs(evil_keyring_dir, "18B38B9AC1B67A0D")
345
debsig_bad.sign(path_to_click)
346
subprocess.check_call(
347
["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
348
with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f:
349
evil_gpg_origin = f.read()
350
with open(os.path.join(self.temp_dir, "_gpgorigin"), "wb") as f:
351
f.write(evil_gpg_origin)
352
f.write(good_gpg_origin)
353
subprocess.check_call(
354
["ar", "-r", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
355
# now ensure that the verification fails as well
356
with self.assertRaises(subprocess.CalledProcessError) as cm:
357
output = subprocess.check_output(
358
[self.click_binary, "install", path_to_click],
359
stderr=subprocess.STDOUT, universal_newlines=True)
360
self.assertIn("Signature verification error", cm.exception.output)
361
output = subprocess.check_output(
362
[self.click_binary, "list", "--user=%s" % self.user],
363
universal_newlines=True)
364
self.assertNotIn(name, output)