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."""
24
from textwrap import dedent
26
from .helpers import (
37
def get_keyid_from_gpghome(gpg_home):
38
"""Return the public keyid of a given gpg home dir"""
39
output = subprocess.check_output(
40
["gpg", "--home", gpg_home, "--list-keys", "--with-colons"],
41
universal_newlines=True)
42
for line in output.splitlines():
43
if not line.startswith("pub:"):
45
return line.split(":")[4]
46
raise ValueError("Cannot find public key in output: '%s'" % output)
50
"""Tiny wrapper around the debsigs CLI"""
51
def __init__(self, gpghome, keyid):
53
self.gpghome = gpghome
54
self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid
56
def sign(self, filepath, signature_type="origin"):
57
"""Sign the click at filepath"""
58
env = copy.copy(os.environ)
59
env["GNUPGHOME"] = os.path.abspath(self.gpghome)
60
subprocess.check_call(
62
"--sign=%s" % signature_type,
63
"--default-key=%s" % self.keyid,
66
def install_signature_policy(self):
67
"""Install/update the system-wide signature policy"""
70
<!DOCTYPE Policy SYSTEM "http://www.debian.org/debsig/1.0/policy.dtd">
71
<Policy xmlns="http://www.debian.org/debsig/1.0/">
73
<Origin Name="test-origin" id="{keyid}" Description="Example policy"/>
75
<Required Type="origin" File="{filename}" id="{keyid}"/>
79
<Required Type="origin" File="{filename}" id="{keyid}"/>
82
""".format(keyid=self.keyid, filename="origin.pub"))
83
makedirs(os.path.dirname(self.policy))
84
with open(self.policy, "w") as f:
87
"/usr/share/debsig/keyrings/%s/origin.pub" % self.keyid)
88
makedirs(os.path.dirname(self.pubkey_path))
89
shutil.copy(os.path.join(self.gpghome, "pubring.gpg"), self.pubkey_path)
91
def uninstall_signature_policy(self):
92
# FIXME: update debsig-verify so that it can work from a different
93
# root than "/" so that the tests do not have to use the
95
os.remove(self.policy)
96
os.remove(self.pubkey_path)
99
@unittest.skipIf(not is_root(), "This tests needs to run as root")
100
class ClickSignaturesTestCase(ClickTestCase):
101
def assertClickNoSignatureError(self, cmd_args):
102
with self.assertRaises(subprocess.CalledProcessError) as cm:
103
output = subprocess.check_output(
104
[self.click_binary] + cmd_args,
105
stderr=subprocess.STDOUT, universal_newlines=True)
106
output = cm.exception.output
107
expected_error_message = ("debsig: Origin Signature check failed. "
108
"This deb might not be signed.")
109
self.assertIn(expected_error_message, output)
111
def assertClickInvalidSignatureError(self, cmd_args):
112
with self.assertRaises(subprocess.CalledProcessError) as cm:
113
output = subprocess.check_output(
114
[self.click_binary] + cmd_args,
115
stderr=subprocess.STDOUT, universal_newlines=True)
118
output = cm.exception.output
119
expected_error_message = "Signature verification error: "
120
self.assertIn(expected_error_message, output)
123
@unittest.skipIf(not is_root(), "This tests needs to run as root")
124
class TestSignatureVerificationNoSignature(ClickSignaturesTestCase):
125
def test_debsig_verify_no_sig(self):
126
name = "org.example.debsig-no-sig"
127
path_to_click = self._make_click(name, framework="")
128
self.assertClickNoSignatureError(["verify", path_to_click])
130
def test_debsig_install_no_sig(self):
131
name = "org.example.debsig-no-sig"
132
path_to_click = self._make_click(name, framework="")
133
self.assertClickNoSignatureError(["install", path_to_click])
135
def test_debsig_install_can_install_with_sig_override(self):
136
name = "org.example.debsig-no-sig"
137
path_to_click = self._make_click(name, framework="")
138
user = os.environ.get("USER", "root")
139
subprocess.check_call(
140
[self.click_binary, "install",
141
"--allow-unauthenticated", "--user=%s" % user,
144
subprocess.call, [self.click_binary, "unregister",
145
"--user=%s" % user, name])
148
@unittest.skipIf(not is_root(), "This tests needs to run as root")
149
class TestSignatureVerification(ClickSignaturesTestCase):
151
super(TestSignatureVerification, self).setUp()
152
self.user = os.environ.get("USER", "root")
153
# the valid origin keyring
154
self.datadir = os.path.join(os.path.dirname(__file__), "data")
155
origin_keyring_dir = os.path.abspath(
156
os.path.join(self.datadir, "origin-keyring"))
157
keyid = get_keyid_from_gpghome(origin_keyring_dir)
158
self.debsigs = Debsigs(origin_keyring_dir, keyid)
159
self.debsigs.install_signature_policy()
162
self.debsigs.uninstall_signature_policy()
164
def test_debsig_install_valid_signature(self):
165
name = "org.example.debsig-valid-sig"
166
path_to_click = self._make_click(name, framework="")
167
self.debsigs.sign(path_to_click)
168
subprocess.check_call(
169
[self.click_binary, "install",
170
"--user=%s" % self.user,
173
subprocess.call, [self.click_binary, "unregister",
174
"--user=%s" % self.user, name])
175
output = subprocess.check_output(
176
[self.click_binary, "list", "--user=%s" % self.user],
177
universal_newlines=True)
178
self.assertIn(name, output)
180
def test_debsig_install_signature_not_in_keyring(self):
181
name = "org.example.debsig-no-keyring-sig"
182
path_to_click = self._make_click(name, framework="")
183
evil_keyring_dir = os.path.join(self.datadir, "evil-keyring")
184
keyid = get_keyid_from_gpghome(evil_keyring_dir)
185
debsig_bad = Debsigs(evil_keyring_dir, keyid)
186
debsig_bad.sign(path_to_click)
187
# and ensure its really not there
188
self.assertClickInvalidSignatureError(["install", path_to_click])
189
output = subprocess.check_output(
190
[self.click_binary, "list", "--user=%s" % self.user],
191
universal_newlines=True)
192
self.assertNotIn(name, output)
194
def test_debsig_install_not_a_signature(self):
195
name = "org.example.debsig-invalid-sig"
196
path_to_click = self._make_click(name, framework="")
197
invalid_sig = os.path.join(self.temp_dir, "_gpgorigin")
198
with open(invalid_sig, "w") as f:
199
f.write("no-valid-signature")
201
subprocess.check_call(["ar", "-r", path_to_click, invalid_sig])
202
self.assertClickInvalidSignatureError(["install", path_to_click])
203
output = subprocess.check_output(
204
[self.click_binary, "list", "--user=%s" % self.user],
205
universal_newlines=True)
206
self.assertNotIn(name, output)
208
def test_debsig_install_signature_altered_click(self):
209
def modify_ar_member(member):
210
subprocess.check_call(
211
["ar", "-x", path_to_click, "control.tar.gz"],
213
altered_member = os.path.join(self.temp_dir, member)
214
with open(altered_member, "ba") as f:
216
subprocess.check_call(["ar", "-r", path_to_click, altered_member])
218
# ensure that all members we care about are checked by debsig-verify
219
for member in ["control.tar.gz", "data.tar.gz", "debian-binary"]:
220
name = "org.example.debsig-altered-click"
221
path_to_click = self._make_click(name, framework="")
222
self.debsigs.sign(path_to_click)
223
modify_ar_member(member)
224
self.assertClickInvalidSignatureError(["install", path_to_click])
225
output = subprocess.check_output(
226
[self.click_binary, "list", "--user=%s" % self.user],
227
universal_newlines=True)
228
self.assertNotIn(name, output)
230
def make_nasty_data_tar(self, compression):
231
new_data_tar = os.path.join(self.temp_dir, "data.tar." + compression)
232
evilfile = os.path.join(self.temp_dir, "README.evil")
233
with open(evilfile, "w") as f:
234
f.write("I am a nasty README")
235
with tarfile.open(new_data_tar, "w:"+compression) as tar:
239
def test_debsig_install_signature_injected_data_tar(self):
240
name = "org.example.debsig-injected-data-click"
241
path_to_click = self._make_click(name, framework="")
242
self.debsigs.sign(path_to_click)
243
new_data = self.make_nasty_data_tar("bz2")
244
# insert before the real data.tar.gz and ensure this is caught
245
# NOTE: that right now this will not be caught by debsig-verify
246
# but later in audit() by debian.debfile.DebFile()
247
subprocess.check_call(["ar",
252
output = subprocess.check_output(
253
["ar", "-t", path_to_click], universal_newlines=True)
254
self.assertEqual(output.splitlines(),
261
with self.assertRaises(subprocess.CalledProcessError):
262
output = subprocess.check_output(
263
[self.click_binary, "install", path_to_click],
264
stderr=subprocess.STDOUT, universal_newlines=True)
265
output = subprocess.check_output(
266
[self.click_binary, "list", "--user=%s" % self.user],
267
universal_newlines=True)
268
self.assertNotIn(name, output)
270
def test_debsig_install_signature_replaced_data_tar(self):
271
name = "org.example.debsig-replaced-data-click"
272
path_to_click = self._make_click(name, framework="")
273
self.debsigs.sign(path_to_click)
274
new_data = self.make_nasty_data_tar("bz2")
275
# replace data.tar.gz with data.tar.bz2 and ensure this is caught
276
subprocess.check_call(["ar",
281
subprocess.check_call(["ar",
285
output = subprocess.check_output(
286
["ar", "-t", path_to_click], universal_newlines=True)
287
self.assertEqual(output.splitlines(),
294
with self.assertRaises(subprocess.CalledProcessError) as cm:
295
output = subprocess.check_output(
296
[self.click_binary, "install", path_to_click],
297
stderr=subprocess.STDOUT, universal_newlines=True)
298
self.assertIn("Signature verification error", cm.exception.output)
299
output = subprocess.check_output(
300
[self.click_binary, "list", "--user=%s" % self.user],
301
universal_newlines=True)
302
self.assertNotIn(name, output)
304
def test_debsig_install_signature_prepend_sig(self):
305
# this test is probably not really needed, it tries to trick
306
# the system by prepending a valid signature that is not
307
# in the keyring. But given that debsig-verify only reads
308
# the first packet of any given _gpg$foo signature its
309
# equivalent to test_debsig_install_signature_not_in_keyring test
310
name = "org.example.debsig-replaced-data-prepend-sig-click"
311
path_to_click = self._make_click(name, framework="")
312
self.debsigs.sign(path_to_click)
313
new_data = self.make_nasty_data_tar("gz")
314
# replace data.tar.gz
315
subprocess.check_call(["ar",
320
# get previous good _gpgorigin for the old data
321
subprocess.check_call(
322
["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
323
with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f:
324
good_gpg_origin = f.read()
325
# and append a valid signature from a non-keyring key
326
evil_keyring_dir = os.path.join(self.datadir, "evil-keyring")
327
debsig_bad = Debsigs(evil_keyring_dir, "18B38B9AC1B67A0D")
328
debsig_bad.sign(path_to_click)
329
subprocess.check_call(
330
["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
331
with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f:
332
evil_gpg_origin = f.read()
333
with open(os.path.join(self.temp_dir, "_gpgorigin"), "wb") as f:
334
f.write(evil_gpg_origin)
335
f.write(good_gpg_origin)
336
subprocess.check_call(
337
["ar", "-r", path_to_click, "_gpgorigin"], cwd=self.temp_dir)
338
# now ensure that the verification fails as well
339
with self.assertRaises(subprocess.CalledProcessError) as cm:
340
output = subprocess.check_output(
341
[self.click_binary, "install", path_to_click],
342
stderr=subprocess.STDOUT, universal_newlines=True)
343
self.assertIn("Signature verification error", cm.exception.output)
344
output = subprocess.check_output(
345
[self.click_binary, "list", "--user=%s" % self.user],
346
universal_newlines=True)
347
self.assertNotIn(name, output)